diff --git a/dist/chromium/publish-beta.py b/dist/chromium/publish-beta.py deleted file mode 100755 index a731e9b32..000000000 --- a/dist/chromium/publish-beta.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import json -import os -import re -import requests -import shutil -import subprocess -import sys -import tempfile -import time -import zipfile - -from string import Template - -# - Download target (raw) uBlock0.chromium.zip from GitHub -# - This is referred to as "raw" package -# - This will fail if not a dev build -# - Upload uBlock0.chromium.zip to Chrome store -# - Publish uBlock0.chromium.zip to Chrome store - -# Find path to project root -projdir = os.path.split(os.path.abspath(__file__))[0] -while not os.path.isdir(os.path.join(projdir, '.git')): - projdir = os.path.normpath(os.path.join(projdir, '..')) - -# We need a version string to work with -if len(sys.argv) >= 2 and sys.argv[1]: - version = sys.argv[1] -else: - version = input('Github release version: ') -version.strip() -if not re.search('^\d+\.\d+\.\d+(b|rc)\d+$', version): - print('Error: Invalid version string.') - exit(1) - -cs_extension_id = 'cgbcahbpdhpcegmbfconppldiemgcoii' -tmpdir = tempfile.TemporaryDirectory() -raw_zip_filename = 'uBlock0_' + version + '.chromium.zip' -raw_zip_filepath = os.path.join(tmpdir.name, raw_zip_filename) -github_owner = 'gorhill' -github_repo = 'uBlock' - -# Load/save auth secrets -# The tmp directory is excluded from git -ubo_secrets = dict() -ubo_secrets_filename = os.path.join(projdir, 'tmp', 'ubo_secrets') -if os.path.isfile(ubo_secrets_filename): - with open(ubo_secrets_filename) as f: - ubo_secrets = json.load(f) - -def input_secret(prompt, token): - if token in ubo_secrets: - prompt += ' ✔' - prompt += ': ' - value = input(prompt).strip() - if len(value) == 0: - if token not in ubo_secrets: - print('Token error:', token) - exit(1) - value = ubo_secrets[token] - elif token not in ubo_secrets or value != ubo_secrets[token]: - ubo_secrets[token] = value - exists = os.path.isfile(ubo_secrets_filename) - with open(ubo_secrets_filename, 'w') as f: - json.dump(ubo_secrets, f, indent=2) - if not exists: - os.chmod(ubo_secrets_filename, 0o600) - return value - - -# GitHub API token -github_token = input_secret('Github token', 'github_token') -github_auth = 'token ' + github_token - -# -# Get metadata from GitHub about the release -# - -# https://developer.github.com/v3/repos/releases/#get-a-single-release -print('Downloading release info from GitHub...') -release_info_url = 'https://api.github.com/repos/{0}/{1}/releases/tags/{2}'.format(github_owner, github_repo, version) -headers = { 'Authorization': github_auth, } -response = requests.get(release_info_url, headers=headers) -if response.status_code != 200: - print('Error: Release not found: {0}'.format(response.status_code)) - exit(1) -release_info = response.json() - -# -# Extract URL to raw package from metadata -# - -# Find url for uBlock0.chromium.zip -raw_zip_url = '' -for asset in release_info['assets']: - if asset['name'] == raw_zip_filename: - raw_zip_url = asset['url'] -if len(raw_zip_url) == 0: - print('Error: Release asset URL not found') - exit(1) - -# -# Download raw package from GitHub -# - -# https://developer.github.com/v3/repos/releases/#get-a-single-release-asset -print('Downloading raw zip package from GitHub...') -headers = { - 'Authorization': github_auth, - 'Accept': 'application/octet-stream', -} -response = requests.get(raw_zip_url, headers=headers) -# Redirections are transparently handled: -# http://docs.python-requests.org/en/master/user/quickstart/#redirection-and-history -if response.status_code != 200: - print('Error: Downloading raw package failed -- server error {0}'.format(response.status_code)) - exit(1) -with open(raw_zip_filepath, 'wb') as f: - f.write(response.content) -print('Downloaded raw package saved as {0}'.format(raw_zip_filepath)) - -# -# Upload to Chrome store -# - -# Auth tokens -cs_id = input_secret('Chrome store id', 'cs_id') -cs_secret = input_secret('Chrome store secret', 'cs_secret') -cs_refresh = input_secret('Chrome store refresh token', 'cs_refresh') - -print('Uploading to Chrome store...') -with open(raw_zip_filepath, 'rb') as f: - print('Generating access token...') - auth_url = 'https://accounts.google.com/o/oauth2/token' - auth_payload = { - 'client_id': cs_id, - 'client_secret': cs_secret, - 'grant_type': 'refresh_token', - 'refresh_token': cs_refresh, - } - auth_response = requests.post(auth_url, data=auth_payload) - if auth_response.status_code != 200: - print('Error: Auth failed -- server error {0}'.format(auth_response.status_code)) - print(auth_response.text) - exit(1) - response_dict = auth_response.json() - if 'access_token' not in response_dict: - print('Error: Auth failed -- no access token') - exit(1) - # Prepare access token - cs_auth = 'Bearer ' + response_dict['access_token'] - headers = { - 'Authorization': cs_auth, - 'x-goog-api-version': '2', - } - # Upload - print('Uploading package...') - upload_url = 'https://www.googleapis.com/upload/chromewebstore/v1.1/items/{0}'.format(cs_extension_id) - upload_response = requests.put(upload_url, headers=headers, data=f) - f.close() - if upload_response.status_code != 200: - print('Upload failed -- server error {0}'.format(upload_response.status_code)) - print(upload_response.text) - exit(1) - response_dict = upload_response.json(); - if 'uploadState' not in response_dict or response_dict['uploadState'] != 'SUCCESS': - print('Upload failed -- server error {0}'.format(response_dict['uploadState'])) - exit(1) - print('Upload succeeded.') - # Publish - print('Publishing package...') - publish_url = 'https://www.googleapis.com/chromewebstore/v1.1/items/{0}/publish'.format(cs_extension_id) - headers = { - 'Authorization': cs_auth, - 'x-goog-api-version': '2', - 'Content-Length': '0', - } - publish_response = requests.post(publish_url, headers=headers) - if publish_response.status_code != 200: - print('Error: Chrome store publishing failed -- server error {0}'.format(publish_response.status_code)) - exit(1) - response_dict = publish_response.json(); - if 'status' not in response_dict or response_dict['status'][0] != 'OK': - print('Publishing failed -- server error {0}'.format(response_dict['status'])) - exit(1) - print('Publishing succeeded.') - -print('All done.') diff --git a/dist/chromium/publish-stable.js b/dist/chromium/publish-chromium.js similarity index 91% rename from dist/chromium/publish-stable.js rename to dist/chromium/publish-chromium.js index 74c000b77..b309df140 100644 --- a/dist/chromium/publish-stable.js +++ b/dist/chromium/publish-chromium.js @@ -26,8 +26,7 @@ import process from 'node:process'; /******************************************************************************/ -const secrets = await ghapi.getSecrets(); -const githubAuth = `Bearer ${secrets.github_token}`; +const githubAuth = `Bearer ${process.env.GITHUB_TOKEN}`; const commandLineArgs = ghapi.commandLineArgs; const githubOwner = commandLineArgs.ghowner; const githubRepo = commandLineArgs.ghrepo; @@ -42,10 +41,10 @@ async function publishToCWS(filePath) { const authURL = 'https://accounts.google.com/o/oauth2/token'; const authRequest = new Request(authURL, { body: JSON.stringify({ - client_id: secrets.cs_id, - client_secret: secrets.cs_secret, + client_id: process.env.CWS_ID, + client_secret: process.env.CWS_SECRET, grant_type: 'refresh_token', - refresh_token: secrets.cs_refresh, + refresh_token: process.env.CWS_REFRESH, }), method: 'POST', }); @@ -61,9 +60,6 @@ async function publishToCWS(filePath) { process.exit(1); } const cwsAuth = `Bearer ${responseDict.access_token}`; - if ( responseDict.refresh_token ) { - secrets.cs_refresh = responseDict.refresh_token - } // Read package const data = await fs.readFile(filePath); @@ -86,7 +82,7 @@ async function publishToCWS(filePath) { } const uploadDict = await uploadResponse.json(); if ( uploadDict.uploadState !== 'SUCCESS' ) { - console.error(`Upload failed -- server error ${uploadDict['uploadState']}`); + console.error(`Upload failed -- server error ${JSON.stringify(uploadDict)}`); process.exit(1); } console.log('Upload succeeded.') @@ -121,7 +117,6 @@ async function publishToCWS(filePath) { /******************************************************************************/ async function main() { - if ( secrets === undefined ) { return 'Need secrets'; } if ( githubOwner === '' ) { return 'Need GitHub owner'; } if ( githubRepo === '' ) { return 'Need GitHub repo'; } diff --git a/dist/chromium/publish-stable.py b/dist/chromium/publish-stable.py deleted file mode 100755 index d1adc1474..000000000 --- a/dist/chromium/publish-stable.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import json -import os -import re -import requests -import shutil -import subprocess -import sys -import tempfile -import time -import zipfile - -from string import Template - -# - Download target (raw) uBlock0.chromium.zip from GitHub -# - This is referred to as "raw" package -# - This will fail if not a dev build -# - Upload uBlock0.chromium.zip to Chrome store -# - Publish uBlock0.chromium.zip to Chrome store - -# Find path to project root -projdir = os.path.split(os.path.abspath(__file__))[0] -while not os.path.isdir(os.path.join(projdir, '.git')): - projdir = os.path.normpath(os.path.join(projdir, '..')) - -# We need a version string to work with -if len(sys.argv) >= 2 and sys.argv[1]: - version = sys.argv[1] -else: - version = input('Github release version: ') -version.strip() -if not re.search('^\d+\.\d+\.\d+$', version): - print('Error: Invalid version string.') - exit(1) - -cs_extension_id = 'cjpalhdlnbpafiamejdnhcphjbkeiagm' -tmpdir = tempfile.TemporaryDirectory() -raw_zip_filename = 'uBlock0_' + version + '.chromium.zip' -raw_zip_filepath = os.path.join(tmpdir.name, raw_zip_filename) -github_owner = 'gorhill' -github_repo = 'uBlock' - -# Load/save auth secrets -# The tmp directory is excluded from git -ubo_secrets = dict() -ubo_secrets_filename = os.path.join(projdir, 'tmp', 'ubo_secrets') -if os.path.isfile(ubo_secrets_filename): - with open(ubo_secrets_filename) as f: - ubo_secrets = json.load(f) - -def input_secret(prompt, token): - if token in ubo_secrets: - prompt += ' ✔' - prompt += ': ' - value = input(prompt).strip() - if len(value) == 0: - if token not in ubo_secrets: - print('Token error:', token) - exit(1) - value = ubo_secrets[token] - elif token not in ubo_secrets or value != ubo_secrets[token]: - ubo_secrets[token] = value - exists = os.path.isfile(ubo_secrets_filename) - with open(ubo_secrets_filename, 'w') as f: - json.dump(ubo_secrets, f, indent=2) - if not exists: - os.chmod(ubo_secrets_filename, 0o600) - return value - - -# GitHub API token -github_token = input_secret('Github token', 'github_token') -github_auth = 'token ' + github_token - -# -# Get metadata from GitHub about the release -# - -# https://developer.github.com/v3/repos/releases/#get-a-single-release -print('Downloading release info from GitHub...') -release_info_url = 'https://api.github.com/repos/{0}/{1}/releases/tags/{2}'.format(github_owner, github_repo, version) -headers = { 'Authorization': github_auth, } -response = requests.get(release_info_url, headers=headers) -if response.status_code != 200: - print('Error: Release not found: {0}'.format(response.status_code)) - exit(1) -release_info = response.json() - -# -# Extract URL to raw package from metadata -# - -# Find url for uBlock0.chromium.zip -raw_zip_url = '' -for asset in release_info['assets']: - if asset['name'] == raw_zip_filename: - raw_zip_url = asset['url'] -if len(raw_zip_url) == 0: - print('Error: Release asset URL not found') - exit(1) - -# -# Download raw package from GitHub -# - -# https://developer.github.com/v3/repos/releases/#get-a-single-release-asset -print('Downloading raw zip package from GitHub...') -headers = { - 'Authorization': github_auth, - 'Accept': 'application/octet-stream', -} -response = requests.get(raw_zip_url, headers=headers) -# Redirections are transparently handled: -# http://docs.python-requests.org/en/master/user/quickstart/#redirection-and-history -if response.status_code != 200: - print('Error: Downloading raw package failed -- server error {0}'.format(response.status_code)) - exit(1) -with open(raw_zip_filepath, 'wb') as f: - f.write(response.content) -print('Downloaded raw package saved as {0}'.format(raw_zip_filepath)) - -# -# Upload to Chrome store -# - -# Auth tokens -cs_id = input_secret('Chrome store id', 'cs_id') -cs_secret = input_secret('Chrome store secret', 'cs_secret') -cs_refresh = input_secret('Chrome store refresh token', 'cs_refresh') - -print('Uploading to Chrome store...') -with open(raw_zip_filepath, 'rb') as f: - print('Generating access token...') - auth_url = 'https://accounts.google.com/o/oauth2/token' - auth_payload = { - 'client_id': cs_id, - 'client_secret': cs_secret, - 'grant_type': 'refresh_token', - 'refresh_token': cs_refresh, - } - auth_response = requests.post(auth_url, data=auth_payload) - if auth_response.status_code != 200: - print('Error: Auth failed -- server error {0}'.format(auth_response.status_code)) - print(auth_response.text) - exit(1) - response_dict = auth_response.json() - if 'access_token' not in response_dict: - print('Error: Auth failed -- no access token') - exit(1) - # Prepare access token - cs_auth = 'Bearer ' + response_dict['access_token'] - headers = { - 'Authorization': cs_auth, - 'x-goog-api-version': '2', - } - # Upload - print('Uploading package...') - upload_url = 'https://www.googleapis.com/upload/chromewebstore/v1.1/items/{0}'.format(cs_extension_id) - upload_response = requests.put(upload_url, headers=headers, data=f) - f.close() - if upload_response.status_code != 200: - print('Upload failed -- server error {0}'.format(upload_response.status_code)) - print(upload_response.text) - exit(1) - response_dict = upload_response.json(); - if 'uploadState' not in response_dict or response_dict['uploadState'] != 'SUCCESS': - print('Upload failed -- server error {0}'.format(response_dict['uploadState'])) - exit(1) - print('Upload succeeded.') - # Publish - print('Publishing package...') - publish_url = 'https://www.googleapis.com/chromewebstore/v1.1/items/{0}/publish'.format(cs_extension_id) - headers = { - 'Authorization': cs_auth, - 'x-goog-api-version': '2', - 'Content-Length': '0', - } - publish_response = requests.post(publish_url, headers=headers) - if publish_response.status_code != 200: - print('Error: Chrome store publishing failed -- server error {0}'.format(publish_response.status_code)) - exit(1) - response_dict = publish_response.json(); - if 'status' not in response_dict or response_dict['status'][0] != 'OK': - print('Publishing failed -- server error {0}'.format(response_dict['status'])) - exit(1) - print('Publishing succeeded.') - -print('All done.') diff --git a/dist/edge/publish-edge.js b/dist/edge/publish-edge.js new file mode 100644 index 000000000..6a27168a8 --- /dev/null +++ b/dist/edge/publish-edge.js @@ -0,0 +1,175 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2025-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +import * as fs from 'node:fs/promises'; +import * as ghapi from '../github-api.js'; +import path from 'node:path'; +import process from 'node:process'; + +/******************************************************************************/ + +const githubAuth = `Bearer ${process.env.GITHUB_TOKEN}`; +const commandLineArgs = ghapi.commandLineArgs; +const githubOwner = commandLineArgs.ghowner; +const githubRepo = commandLineArgs.ghrepo; +const githubTag = commandLineArgs.ghtag; +const edgeId = commandLineArgs.edgeid; + +/******************************************************************************/ + +async function sleep(seconds) { + return new Promise(resolve => { + setTimeout(resolve, seconds * 1000); + }); +} + +/******************************************************************************/ + +async function publishToEdgeStore(filePath) { + const edgeApiKey = process.env.EDGE_APIKEY; + const edgeClientId = process.env.EDGE_CLIENTID; + const uploadURL = `https://api.addons.microsoftedge.microsoft.com/v1/products/${edgeId}/submissions/draft/package`; + + // Read package + const data = await fs.readFile(filePath); + + // Upload + console.log(`Uploading package to ${uploadURL}`); + const uploadRequest = new Request(uploadURL, { + body: data, + headers: { + 'Authorization': `ApiKey ${edgeApiKey}`, + 'X-ClientID': edgeClientId, + 'Content-Type': 'application/zip' + }, + method: 'POST', + }); + const uploadResponse = await fetch(uploadRequest); + if ( uploadResponse.status !== 202 ) { + console.log(`Upload failed -- server error ${uploadResponse.status}`); + process.exit(1); + } + const operationId = uploadResponse.headers.get('Location'); + if ( operationId === undefined ) { + console.log(`Upload failed -- missing Location header`); + process.exit(1); + } + console.log(`Upload succeeded`); + + // Check upload status + console.log('Checking upload status...'); + const interval = 60; // check every 60 seconds + let countdown = 60 * 60 / interval; // for at most 60 minutes + for (;;) { + await sleep(interval); + countdown -= 1 + if ( countdown <= 0 ) { + console.log('Error: Microsoft store timed out') + process.exit(1); + } + const uploadStatusRequest = new Request(`${uploadURL}/operations/${operationId}`, { + headers: { + 'Authorization': `ApiKey ${edgeApiKey}`, + 'X-ClientID': edgeClientId, + }, + }); + const uploadStatusResponse = await fetch(uploadStatusRequest); + if ( uploadStatusResponse.status !== 200 ) { + console.log(`Upload status check failed -- server error ${uploadStatusResponse.status}`); + process.exit(1); + } + const uploadStatusDict = await uploadStatusResponse.json(); + const { status } = uploadStatusDict; + if ( status === undefined || status === 'Failed' ) { + console.log(`Upload status check failed -- server error ${status}`); + process.exit(1); + } + if ( status === 'InProgress' ) { continue } + console.log('Package ready to be published.') + break; + } + + // Publish + // https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/update/api/addons-api-reference?tabs=v1-1#publish-the-product-draft-submission + console.log('Publish package...') + const publishURL = `https://api.addons.microsoftedge.microsoft.com/v1/products/${edgeId}/submissions`; + const publishNotes = { + 'Notes': 'See official release notes at ' + } + const publishRequest = new Request(publishURL, { + body: JSON.stringify(publishNotes), + headers: { + 'Authorization': `ApiKey ${edgeApiKey}`, + 'X-ClientID': edgeClientId, + }, + method: 'POST', + }); + const publishResponse = await fetch(publishRequest); + if ( publishResponse.status !== 202 ) { + console.log(`Publish failed -- server error ${publishResponse.status}`); + process.exit(1); + } + if ( publishResponse.headers.get('Location') === undefined ) { + console.log(`Publish failed -- missing Location header`); + process.exit(1); + } + console.log('Publish succeeded.') +} + +/******************************************************************************/ + +async function main() { + if ( githubOwner === '' ) { return 'Need GitHub owner'; } + if ( githubRepo === '' ) { return 'Need GitHub repo'; } + + ghapi.setGithubContext(githubOwner, githubRepo, githubTag, githubAuth); + + const assetInfo = await ghapi.getAssetInfo('edge'); + + console.log(`GitHub owner: "${githubOwner}"`); + console.log(`GitHub repo: "${githubRepo}"`); + console.log(`Release tag: "${githubTag}"`); + console.log(`Release asset: "${assetInfo.name}"`); + + // Fetch asset from GitHub repo + const filePath = await ghapi.downloadAssetFromRelease(assetInfo); + console.log('Asset saved at', filePath); + + // Upload to Edge Store + await publishToEdgeStore(filePath); + + // Clean up + if ( commandLineArgs.keep !== true ) { + const tmpdir = path.dirname(filePath); + console.log(`Removing ${tmpdir}`); + ghapi.shellExec(`rm -rf "${tmpdir}"`); + } + + console.log('Done'); +} + +main().then(result => { + if ( result !== undefined ) { + console.log(result); + process.exit(1); + } + process.exit(0); +}); diff --git a/dist/github-api.js b/dist/github-api.js index 0ba1e1671..6cac6660a 100644 --- a/dist/github-api.js +++ b/dist/github-api.js @@ -32,6 +32,12 @@ function voidFunc() { /******************************************************************************/ +function reportFetchError(response) { + console.log(response.statusText); +} + +/******************************************************************************/ + let githubOwner = ''; let githubRepo = ''; let githubTag = ''; @@ -46,37 +52,6 @@ export function setGithubContext(owner, repo, tag, auth) { /******************************************************************************/ -let pathToSecrets = ''; - -export async function getSecrets() { - const homeDir = os.homedir(); - let currentDir = process.cwd(); - let fileName = ''; - for (;;) { - fileName = `${currentDir}/ubo_secrets`; - const stat = await fs.stat(fileName).catch(voidFunc); - if ( stat !== undefined ) { break; } - currentDir = path.resolve(currentDir, '..'); - if ( currentDir.startsWith(homeDir) === false ) { - pathToSecrets = homeDir; - return; - } - } - console.log(`Found secrets in ${fileName}`); - const text = await fs.readFile(fileName, { encoding: 'utf8' }).catch(voidFunc); - if ( text === undefined ) { return {}; } - const secrets = JSON.parse(text); - pathToSecrets = fileName; - return secrets; -} - -export async function saveSecrets(secrets) { - if ( pathToSecrets === '' ) { return; } - return fs.writeFile(pathToSecrets, JSON.stringify(secrets, null, 2)); -} - -/******************************************************************************/ - export async function getRepoRoot() { const homeDir = os.homedir(); let currentDir = process.cwd(); @@ -101,7 +76,7 @@ export async function getReleaseInfo() { }); const response = await fetch(request).catch(voidFunc); if ( response === undefined ) { return; } - if ( response.ok !== true ) { return; } + if ( response.ok !== true ) { return reportFetchError(response); } const releaseInfo = await response.json().catch(voidFunc); if ( releaseInfo === undefined ) { return; } return releaseInfo; diff --git a/package.json b/package.json index 5346626be..f00997e24 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "uBlock", "version": "1.0.0", "description": "npm dev tools", + "type": "module", "scripts": { "lint": "eslint --no-warn-ignored --ignore-pattern \"**/lib/\" --ignore-pattern \"**/npm/\" -- \"./src/js/*.js\" \"./src/js/**/*.js\" \"./**/*.json\" \"./platform/**/*.js\"", "test": "echo \"Error: no test specified\" && exit 1"