diff --git a/dist/chromium/publish-beta.py b/dist/chromium/publish-beta.py new file mode 100755 index 0000000..ed3efea --- /dev/null +++ b/dist/chromium/publish-beta.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +import datetime +import json +import jwt +import os +import re +import requests +import shutil +import subprocess +import sys +import tempfile +import time +import zipfile + +from distutils.version import StrictVersion +from string import Template + +# - Download target (raw) uMatrix.chromium.zip from GitHub +# - This is referred to as "raw" package +# - This will fail if not a dev build +# - Upload uMatrix.chromium.zip to Chrome store +# - Publish uMatrix.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, '..')) + +cs_extension_id = 'eckgcipdkhcfghnmincccnhpdmnbefki' +tmpdir = tempfile.TemporaryDirectory() +raw_zip_filename = 'uMatrix.chromium.zip' +raw_zip_filepath = os.path.join(tmpdir.name, raw_zip_filename) +github_owner = 'gorhill' +github_repo = 'uMatrix' + +# 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) + +# Load/save auth secrets +# The build directory is excluded from git +ubo_secrets = dict() +ubo_secrets_filename = os.path.join(projdir, 'dist', 'build', '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 uMatrix.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/firefox/publish-signed-beta.py b/dist/firefox/publish-signed-beta.py index dc3dfce..8714179 100755 --- a/dist/firefox/publish-signed-beta.py +++ b/dist/firefox/publish-signed-beta.py @@ -62,12 +62,35 @@ if not re.search('^\d+\.\d+\.\d+(b|rc)\d+$', version): print('Error: Invalid version string.') exit(1) +# Load/save auth secrets +# The build directory is excluded from git +ubo_secrets = dict() +ubo_secrets_filename = os.path.join(projdir, 'dist', 'build', '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 -# TODO: support as environment variable? (see os.environ) -github_token = input("Github token: ").strip() -if len(github_token) == 0: - print('Error: invalid GitHub token') - exit(1) +github_token = input_secret('Github token', 'github_token') github_auth = 'token ' + github_token # @@ -145,9 +168,8 @@ with zipfile.ZipFile(raw_xpi_filepath, 'r') as zipin: print('Ask AMO to sign self-hosted xpi package...') with open(unsigned_xpi_filepath, 'rb') as f: - # TODO: support use of env variables for key/secret? - amo_api_key = input("AMO API key: ").strip() - amo_secret = input("AMO API secret: ").strip() + amo_api_key = input_secret('AMO API key', 'amo_api_key') + amo_secret = input_secret('AMO API secret', 'amo_secret') amo_nonce = os.urandom(8).hex() jwt_payload = { 'iss': amo_api_key, @@ -251,7 +273,7 @@ with open(updates_json_filepath) as f: f.close() previous_version = updates_json['addons'][extension_id]['updates'][0]['version'] if LooseVersion(version) > LooseVersion(previous_version): - with open(os.path.join(projdir, 'platform', 'webext', 'updates.template.json')) as f: + with open(os.path.join(projdir, 'dist', 'firefox', 'updates.template.json')) as f: template_json = Template(f.read()) f.close() updates_json = template_json.substitute(version=version)