732 lines
27 KiB
Python
Executable File
732 lines
27 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""Sign releases on github, make/upload ppa to launchpad.net
|
|
|
|
NOTE on ppa: To build a ppa you may need to install some more packages.
|
|
On ubuntu:
|
|
|
|
sudo apt-get install devscripts libssl-dev python3-dev \
|
|
debhelper python3-setuptools dh-python
|
|
|
|
NOTE on apk signing: To create a keystore and sign the apk you need to install
|
|
java-8-openjdk, or java-7-openjdk on older systems.
|
|
|
|
To create a keystore run the following command:
|
|
|
|
mkdir ~/.jks && keytool -genkey -v -keystore ~/.jks/keystore \
|
|
-alias electrum.z.cash -keyalg RSA -keysize 2048 \
|
|
-validity 10000
|
|
|
|
Then it shows a warning about the proprietary format and a command to migrate:
|
|
|
|
keytool -importkeystore -srckeystore ~/.jks/keystore \
|
|
-destkeystore ~/.jks/keystore -deststoretype pkcs12
|
|
|
|
Manual signing:
|
|
|
|
jarsigner -verbose \
|
|
-tsa http://sha256timestamp.ws.symantec.com/sha256/timestamp \
|
|
-sigalg SHA1withRSA -digestalg SHA1 \
|
|
-sigfile zclassic-electrum \
|
|
-keystore ~/.jks/keystore \
|
|
Electrum_Zclassic-3.0.6.1-release-unsigned.apk \
|
|
electrum.z.cash
|
|
|
|
Zipalign from Android SDK build tools is also required (set path to bin in
|
|
settings file or with key -z). To install:
|
|
|
|
wget http://dl.google.com/android/android-sdk_r24-linux.tgz \
|
|
&& tar xzf android-sdk_r24-linux.tgz \
|
|
&& rm android-sdk_r24-linux.tgz \
|
|
&& (while sleep 3; do echo "y"; done) \
|
|
| android-sdk-linux/tools/android update sdk -u -a -t \
|
|
'tools, platform-tools-preview, build-tools-23.0.1' \
|
|
&& (while sleep 3; do echo "y"; done) \
|
|
| android-sdk-linux/tools/android update sdk -u -a -t \
|
|
'tools, platform-tools, build-tools-27.0.3'
|
|
|
|
Manual zip aligning:
|
|
|
|
android-sdk-linux/build-tools/27.0.3/zipalign -v 4 \
|
|
Electrum_Zclassic-3.0.6.1-release-unsigned.apk \
|
|
Electrum_Zclassic-3.0.6.1-release.apk
|
|
|
|
|
|
|
|
About script settings:
|
|
|
|
Settings is read from options, then config file is read.
|
|
If setting is already set from options, then it value does
|
|
not changes.
|
|
|
|
Config file can have one repo form or multiple repo form.
|
|
|
|
In one repo form config settings read from root JSON object.
|
|
Keys are "repo", "keyid", "token", "count", "sign_drafts",
|
|
and others, which is corresponding to program options.
|
|
|
|
Example:
|
|
|
|
{
|
|
"repo": "value"
|
|
...
|
|
}
|
|
|
|
In multiple repo form, if root "default_repo" key is set, then code
|
|
try to read "repos" key as list and cycle through it to find suitable
|
|
repo, or if no repo is set before, then "default_repo" is used to match.
|
|
If match found, then that list object is used ad one repo form config.
|
|
|
|
Example:
|
|
|
|
{
|
|
"default_repo": "value"
|
|
"repos": [
|
|
{
|
|
"repo": "value"
|
|
...
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
import os
|
|
import os.path
|
|
import re
|
|
import sys
|
|
import time
|
|
import getpass
|
|
import shutil
|
|
import hashlib
|
|
import tempfile
|
|
import json
|
|
import zipfile
|
|
from subprocess import check_call, CalledProcessError
|
|
from functools import cmp_to_key
|
|
from time import localtime, strftime
|
|
|
|
try:
|
|
import click
|
|
import certifi
|
|
import gnupg
|
|
import dateutil.parser
|
|
import colorama
|
|
from colorama import Fore, Style
|
|
from github_release import (get_releases, gh_asset_download,
|
|
gh_asset_upload, gh_asset_delete)
|
|
from urllib3 import PoolManager
|
|
except ImportError as e:
|
|
print('Import error:', e)
|
|
print('To run script install required packages with the next command:\n\n'
|
|
'pip install githubrelease python-gnupg pyOpenSSL cryptography idna'
|
|
' certifi python-dateutil click colorama requests LinkHeader')
|
|
sys.exit(1)
|
|
|
|
HTTP = PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
|
|
|
|
FNULL = open(os.devnull, 'w')
|
|
|
|
HOME_DIR = os.path.expanduser('~')
|
|
CONFIG_NAME = '.sign-releases'
|
|
SEARCH_COUNT = 1
|
|
SHA_FNAME = 'SHA256SUMS.txt'
|
|
|
|
# make_ppa related definitions
|
|
PPA_SERIES = {
|
|
'trusty': '14.04.1',
|
|
'xenial': '16.04.1',
|
|
'bionic': '18.04.1',
|
|
'cosmic': '18.10.1',
|
|
}
|
|
PEP440_PUBVER_PATTERN = re.compile('^((\d+)!)?'
|
|
'((\d+)(\.\d+)*)'
|
|
'([a-zA-Z]+\d+)?'
|
|
'((\.[a-zA-Z]+\d+)*)$')
|
|
REL_NOTES_PATTERN = re.compile('^#.+?(^[^#].+?)^#.+?', re.M | re.S)
|
|
SDIST_NAME_PATTERN = re.compile('^Electrum-Zclassic-(.*).tar.gz$')
|
|
SDIST_DIR_TEMPLATE = 'Electrum-Zclassic-{version}'
|
|
PPA_SOURCE_NAME = 'electrum-zclassic'
|
|
PPA_ORIG_NAME_TEMPLATE = '%s_{version}.orig.tar.gz' % PPA_SOURCE_NAME
|
|
CHANGELOG_TEMPLATE = """%s ({ppa_version}) {series}; urgency=medium
|
|
{changes} -- {uid} {time}""" % PPA_SOURCE_NAME
|
|
PPA_FILES_TEMPLATE = '%s_{0}{1}' % PPA_SOURCE_NAME
|
|
LP_API_URL='https://api.launchpad.net/1.0'
|
|
LP_SERIES_TEMPLATE = '%s/ubuntu/{0}' % LP_API_URL
|
|
LP_ARCHIVES_TEMPLATE = '%s/~{user}/+archive/ubuntu/{ppa}' % LP_API_URL
|
|
|
|
# sing_apk related definitions
|
|
JKS_KEYSTORE = os.path.join(HOME_DIR, '.jks/keystore')
|
|
JKS_ALIAS = 'electrum.z.cash'
|
|
JKS_STOREPASS = 'JKS_STOREPASS'
|
|
JKS_KEYPASS = 'JKS_KEYPASS'
|
|
KEYTOOL_ARGS = ['keytool', '-list', '-storepass:env', JKS_STOREPASS]
|
|
JARSIGNER_ARGS = [
|
|
'jarsigner', '-verbose',
|
|
'-tsa', 'http://sha256timestamp.ws.symantec.com/sha256/timestamp',
|
|
'-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1',
|
|
'-sigfile', 'zclassic-electrum',
|
|
'-storepass:env', JKS_STOREPASS,
|
|
'-keypass:env', JKS_KEYPASS,
|
|
]
|
|
UNSIGNED_APK_PATTERN = re.compile('^Electrum_Zclassic-(.*)-release-unsigned.apk$')
|
|
SIGNED_APK_TEMPLATE = 'Electrum_Zclassic-{version}-release.apk'
|
|
|
|
|
|
os.environ['QUILT_PATCHES'] = 'debian/patches'
|
|
|
|
|
|
def pep440_to_deb(version):
|
|
"""Convert PEP 440 public version to deb upstream version"""
|
|
ver_match = PEP440_PUBVER_PATTERN.match(version)
|
|
if not ver_match:
|
|
raise Exception('Version "%s" does not comply with PEP 440' % version)
|
|
|
|
g = ver_match.group
|
|
deb_ver = ''
|
|
deb_ver += ('%s:' % g(2)) if g(1) else ''
|
|
deb_ver += g(3)
|
|
deb_ver += ('~%s' % g(6)) if g(6) else ''
|
|
deb_ver += ('%s' % g(7)) if g(7) else ''
|
|
|
|
return deb_ver
|
|
|
|
|
|
def compare_published_times(a, b):
|
|
"""Releases list sorting comparsion function (last published first)"""
|
|
|
|
a = a['published_at']
|
|
b = b['published_at']
|
|
|
|
if not a and not b:
|
|
return 0
|
|
elif not a:
|
|
return -1
|
|
elif not b:
|
|
return 1
|
|
|
|
a = dateutil.parser.parse(a)
|
|
b = dateutil.parser.parse(b)
|
|
|
|
if a > b:
|
|
return -1
|
|
elif b > a:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
|
|
def sha256_checksum(filename, block_size=65536):
|
|
"""Gather sha256 hash on filename"""
|
|
sha256 = hashlib.sha256()
|
|
with open(filename, 'rb') as f:
|
|
for block in iter(lambda: f.read(block_size), b''):
|
|
sha256.update(block)
|
|
return sha256.hexdigest()
|
|
|
|
|
|
def read_config():
|
|
"""Read and parse JSON from config file from HOME dir"""
|
|
config_path = os.path.join(HOME_DIR, CONFIG_NAME)
|
|
if not os.path.isfile(config_path):
|
|
return {}
|
|
|
|
try:
|
|
with open(config_path, 'r') as f:
|
|
data = f.read()
|
|
return json.loads(data)
|
|
except Exception as e:
|
|
print('Error: Cannot read config file:', e)
|
|
return {}
|
|
|
|
|
|
def get_next_ppa_num(ppa, source_package_name, ppa_upstr_version, series_name):
|
|
"""Calculate next ppa num (if older ppa versions whas published earlier)"""
|
|
user, ppa_name = ppa.split('/')
|
|
archives_url = LP_ARCHIVES_TEMPLATE.format(user=user, ppa=ppa_name)
|
|
series_url = LP_SERIES_TEMPLATE.format(series_name)
|
|
query = {
|
|
'ws.op': 'getPublishedSources',
|
|
'distro_series': series_url,
|
|
'order_by_date': 'true',
|
|
'source_name': source_package_name,
|
|
}
|
|
|
|
resp = HTTP.request('GET', archives_url, fields=query)
|
|
if resp.status != 200:
|
|
raise Exception('Launchpad API error %s %s', (resp.status,
|
|
resp.reason))
|
|
data = json.loads(resp.data.decode('utf-8'))
|
|
entries = data['entries']
|
|
if len(entries) == 0:
|
|
return 1
|
|
|
|
for e in entries:
|
|
ppa_version = e['source_package_version']
|
|
version_match = re.match('%s-0ppa(\d+)~ubuntu' % ppa_upstr_version,
|
|
ppa_version)
|
|
if version_match:
|
|
return int(version_match.group(1)) + 1
|
|
|
|
return 1
|
|
|
|
|
|
class ChdirTemporaryDirectory(object):
|
|
"""Create tmp dir, chdir to it and remove on exit"""
|
|
def __enter__(self):
|
|
self.prev_wd = os.getcwd()
|
|
self.name = tempfile.mkdtemp()
|
|
os.chdir(self.name)
|
|
return self.name
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
os.chdir(self.prev_wd)
|
|
shutil.rmtree(self.name)
|
|
|
|
|
|
class SignApp(object):
|
|
def __init__(self, **kwargs):
|
|
"""Get app settings from options, from curdir git, from config file"""
|
|
ask_passphrase = kwargs.pop('ask_passphrase', None)
|
|
self.sign_drafts = kwargs.pop('sign_drafts', False)
|
|
self.force = kwargs.pop('force', False)
|
|
self.tag_name = kwargs.pop('tag_name', None)
|
|
self.repo = kwargs.pop('repo', None)
|
|
self.ppa = kwargs.pop('ppa', None)
|
|
self.ppa_upstream_suffix = kwargs.pop('ppa_upstream_suffix', None)
|
|
self.token = kwargs.pop('token', None)
|
|
self.keyid = kwargs.pop('keyid', None)
|
|
self.count = kwargs.pop('count', None)
|
|
self.dry_run = kwargs.pop('dry_run', False)
|
|
self.no_ppa = kwargs.pop('no_ppa', False)
|
|
self.verbose = kwargs.pop('verbose', False)
|
|
self.jks_keystore = kwargs.pop('jks_keystore', False)
|
|
self.jks_alias = kwargs.pop('jks_alias', False)
|
|
self.zipalign_path = kwargs.pop('zipalign_path', False)
|
|
|
|
self.config = {}
|
|
config_data = read_config()
|
|
|
|
default_repo = config_data.get('default_repo', None)
|
|
if default_repo:
|
|
if not self.repo:
|
|
self.repo = default_repo
|
|
|
|
for config in config_data.get('repos', []):
|
|
config_repo = config.get('repo', None)
|
|
if config_repo and config_repo == self.repo:
|
|
self.config = config
|
|
break
|
|
else:
|
|
self.config = config_data
|
|
|
|
if self.config:
|
|
self.repo = self.repo or self.config.get('repo', None)
|
|
self.ppa = self.ppa or self.config.get('ppa', None)
|
|
self.token = self.token or self.config.get('token', None)
|
|
self.keyid = self.keyid or self.config.get('keyid', None)
|
|
self.count = self.count or self.config.get('count', None) \
|
|
or SEARCH_COUNT
|
|
self.sign_drafts = self.sign_drafts \
|
|
or self.config.get('sign_drafts', False)
|
|
self.no_ppa = self.no_ppa \
|
|
or self.config.get('no_ppa', False)
|
|
self.verbose = self.verbose or self.config.get('verbose', None)
|
|
self.jks_keystore = self.jks_keystore \
|
|
or self.config.get('jks_keystore', JKS_KEYSTORE)
|
|
self.jks_alias = self.jks_alias \
|
|
or self.config.get('jks_alias', JKS_ALIAS)
|
|
self.zipalign_path = self.zipalign_path \
|
|
or self.config.get('zipalign_path', None)
|
|
|
|
if not self.repo:
|
|
print('no repo found, exit')
|
|
sys.exit(1)
|
|
|
|
if self.token:
|
|
os.environ['GITHUB_TOKEN'] = self.token
|
|
|
|
if not os.environ.get('GITHUB_TOKEN', None):
|
|
print('GITHUB_TOKEN environment var not set, exit')
|
|
sys.exit(1)
|
|
|
|
if self.keyid:
|
|
self.keyid = self.keyid.split('/')[-1]
|
|
|
|
self.passphrase = None
|
|
self.gpg = gnupg.GPG()
|
|
|
|
if not self.keyid:
|
|
print('no keyid set, exit')
|
|
sys.exit(1)
|
|
|
|
keylist = self.gpg.list_keys(True, keys=[self.keyid])
|
|
if not keylist:
|
|
print('no key with keyid %s found, exit' % self.keyid)
|
|
sys.exit(1)
|
|
|
|
self.uid = ', '.join(keylist[0].get('uids', ['No uid found']))
|
|
|
|
if ask_passphrase:
|
|
while not self.passphrase:
|
|
self.read_passphrase()
|
|
elif not self.check_key():
|
|
while not self.passphrase:
|
|
self.read_passphrase()
|
|
|
|
if self.zipalign_path:
|
|
try:
|
|
check_call(self.zipalign_path, stderr=FNULL)
|
|
except CalledProcessError:
|
|
pass
|
|
self.read_jks_storepass()
|
|
self.read_jks_keypass()
|
|
|
|
def read_jks_storepass(self):
|
|
"""Read JKS storepass and keypass"""
|
|
while not JKS_STOREPASS in os.environ:
|
|
storepass = getpass.getpass('%sInput %s keystore password:%s ' %
|
|
(Fore.GREEN,
|
|
self.jks_keystore,
|
|
Style.RESET_ALL))
|
|
os.environ[JKS_STOREPASS] = storepass
|
|
try:
|
|
check_call(KEYTOOL_ARGS + ['-keystore', self.jks_keystore],
|
|
stdout=FNULL, stderr=FNULL)
|
|
except CalledProcessError:
|
|
print('%sWrong keystore password%s' %
|
|
(Fore.RED, Style.RESET_ALL))
|
|
del os.environ[JKS_STOREPASS]
|
|
|
|
def read_jks_keypass(self):
|
|
while not JKS_KEYPASS in os.environ:
|
|
keypass = getpass.getpass('%sInput alias password for <%s> '
|
|
'[Enter if same as for keystore]:%s ' %
|
|
(Fore.YELLOW,
|
|
self.jks_alias,
|
|
Style.RESET_ALL))
|
|
if not keypass:
|
|
os.environ[JKS_KEYPASS] = os.environ[JKS_STOREPASS]
|
|
else:
|
|
os.environ[JKS_KEYPASS] = keypass
|
|
|
|
with ChdirTemporaryDirectory() as tmpdir:
|
|
test_file = 'testfile.txt'
|
|
test_zipfile = 'testzip.zip'
|
|
with open(test_file, 'w') as fdw:
|
|
fdw.write('testcontent')
|
|
test_zf = zipfile.ZipFile(test_zipfile, mode='w')
|
|
test_zf.write(test_file)
|
|
test_zf.close()
|
|
|
|
sign_args = ['-keystore', self.jks_keystore,
|
|
test_zipfile, self.jks_alias]
|
|
try:
|
|
check_call(JARSIGNER_ARGS + sign_args, stdout=FNULL)
|
|
except CalledProcessError:
|
|
print('%sWrong key alias password%s' %
|
|
(Fore.RED, Style.RESET_ALL))
|
|
del os.environ[JKS_KEYPASS]
|
|
|
|
def read_passphrase(self):
|
|
"""Read passphrase for gpg key until check_key is passed"""
|
|
passphrase = getpass.getpass('%sInput passphrase for Key: %s %s:%s ' %
|
|
(Fore.GREEN,
|
|
self.keyid,
|
|
self.uid,
|
|
Style.RESET_ALL))
|
|
if self.check_key(passphrase):
|
|
self.passphrase = passphrase
|
|
|
|
def check_key(self, passphrase=None):
|
|
"""Try to sign test string, and if some data signed retun True"""
|
|
signed_data = self.gpg.sign('test message to check passphrase',
|
|
keyid=self.keyid, passphrase=passphrase)
|
|
if signed_data.data and self.gpg.verify(signed_data.data).valid:
|
|
return True
|
|
print('%sWrong passphrase!%s' % (Fore.RED, Style.RESET_ALL))
|
|
return False
|
|
|
|
def sign_file_name(self, name, detach=True):
|
|
"""Sign file with self.keyid, place signature in deteached .asc file"""
|
|
with open(name, 'rb') as fdrb:
|
|
signed_data = self.gpg.sign_file(fdrb,
|
|
keyid=self.keyid,
|
|
passphrase=self.passphrase,
|
|
detach=detach)
|
|
with open('%s.asc' % name, 'wb') as fdw:
|
|
fdw.write(signed_data.data)
|
|
|
|
def sign_release(self, release, other_names, asc_names, is_newest_release):
|
|
"""Download/sign unsigned assets, upload .asc counterparts.
|
|
Create SHA256SUMS.txt with all assets included and upload it
|
|
with SHA256SUMS.txt.asc counterpart.
|
|
"""
|
|
repo = self.repo
|
|
tag = release.get('tag_name', None)
|
|
if not tag:
|
|
print('Release have no tag name, skip release\n')
|
|
return
|
|
|
|
with ChdirTemporaryDirectory() as tmpdir:
|
|
with open(SHA_FNAME, 'w') as fdw:
|
|
sdist_match = None
|
|
for name in other_names:
|
|
if name == SHA_FNAME:
|
|
continue
|
|
|
|
gh_asset_download(repo, tag, name)
|
|
|
|
if not self.no_ppa:
|
|
sdist_match = sdist_match \
|
|
or SDIST_NAME_PATTERN.match(name)
|
|
|
|
apk_match = UNSIGNED_APK_PATTERN.match(name)
|
|
if apk_match:
|
|
unsigned_name = name
|
|
name = self.sign_apk(unsigned_name, apk_match.group(1))
|
|
|
|
gh_asset_upload(repo, tag, name, dry_run=self.dry_run)
|
|
gh_asset_delete(repo, tag, unsigned_name,
|
|
dry_run=self.dry_run)
|
|
|
|
if not '%s.asc' % name in asc_names or self.force:
|
|
self.sign_file_name(name)
|
|
|
|
if self.force:
|
|
gh_asset_delete(repo, tag, '%s.asc' % name,
|
|
dry_run=self.dry_run)
|
|
|
|
gh_asset_upload(repo, tag, '%s.asc' % name,
|
|
dry_run=self.dry_run)
|
|
|
|
sumline = '%s %s\n' % (sha256_checksum(name), name)
|
|
fdw.write(sumline)
|
|
|
|
self.sign_file_name(SHA_FNAME, detach=False)
|
|
|
|
gh_asset_delete(repo, tag, '%s.asc' % SHA_FNAME,
|
|
dry_run=self.dry_run)
|
|
|
|
gh_asset_upload(repo, tag, '%s.asc' % SHA_FNAME,
|
|
dry_run=self.dry_run)
|
|
|
|
if sdist_match and is_newest_release:
|
|
self.make_ppa(sdist_match, tmpdir)
|
|
|
|
def sign_apk(self, unsigned_name, version):
|
|
"""Sign unsigned release apk"""
|
|
if not (JKS_STOREPASS in os.environ and JKS_KEYPASS in os.environ):
|
|
raise Exception('Found unsigned apk and no zipalign path set')
|
|
|
|
name = SIGNED_APK_TEMPLATE.format(version=version)
|
|
|
|
print('Signing apk: %s' % name)
|
|
apk_args = ['-keystore', self.jks_keystore,
|
|
unsigned_name, self.jks_alias]
|
|
if self.verbose:
|
|
check_call(JARSIGNER_ARGS + apk_args)
|
|
check_call([self.zipalign_path, '-v', '4', unsigned_name, name])
|
|
else:
|
|
check_call(JARSIGNER_ARGS + apk_args, stdout=FNULL)
|
|
check_call([self.zipalign_path, '-v', '4', unsigned_name, name],
|
|
stdout=FNULL)
|
|
|
|
return name
|
|
|
|
def make_ppa(self, sdist_match, tmpdir):
|
|
"""Build, sign and upload dsc to launchpad.net ppa from sdist.tar.gz"""
|
|
with ChdirTemporaryDirectory() as ppa_tmpdir:
|
|
sdist_name = sdist_match.group(0)
|
|
version = sdist_match.group(1)
|
|
ppa_upstr_version = pep440_to_deb(version)
|
|
ppa_upstream_suffix = self.ppa_upstream_suffix
|
|
if ppa_upstream_suffix:
|
|
ppa_upstr_version += ('+%s' % ppa_upstream_suffix)
|
|
ppa_orig_name = PPA_ORIG_NAME_TEMPLATE.format(
|
|
version=ppa_upstr_version)
|
|
series = list(map(lambda x: x[0],
|
|
sorted(PPA_SERIES.items(), key=lambda x: x[1])))
|
|
sdist_dir = SDIST_DIR_TEMPLATE.format(version=version)
|
|
sdist_dir = os.path.join(ppa_tmpdir, sdist_dir)
|
|
debian_dir = os.path.join(sdist_dir, 'debian')
|
|
changelog_name = os.path.join(debian_dir, 'changelog')
|
|
relnotes_name = os.path.join(sdist_dir, 'RELEASE-NOTES')
|
|
|
|
print('Found sdist: %s, version: %s' % (sdist_name, version))
|
|
print(' Copying sdist to %s, extracting' % ppa_orig_name)
|
|
shutil.copy(os.path.join(tmpdir, sdist_name),
|
|
os.path.join(ppa_tmpdir, ppa_orig_name))
|
|
check_call(['tar', '-xzvf', ppa_orig_name], stdout=FNULL)
|
|
|
|
with open(relnotes_name, 'r') as rnfd:
|
|
changes = rnfd.read()
|
|
changes_match = REL_NOTES_PATTERN.match(changes)
|
|
if changes_match and len(changes_match.group(1)) > 0:
|
|
changes = changes_match.group(1).split('\n')
|
|
for i in range(len(changes)):
|
|
if changes[i] == '':
|
|
continue
|
|
elif changes[i][0] != ' ':
|
|
changes[i] = ' %s' % changes[i]
|
|
elif len(changes[i]) > 1 and changes[i][1] != ' ':
|
|
changes[i] = ' %s' % changes[i]
|
|
changes = '\n'.join(changes)
|
|
else:
|
|
changes = '\n * Porting to ppa\n\n'
|
|
|
|
os.chdir(sdist_dir)
|
|
print(' Making PPAs for series: %s' % (', '.join(series)))
|
|
now_formatted = strftime('%a, %d %b %Y %H:%M:%S %z', localtime())
|
|
for s in series:
|
|
ppa_num = get_next_ppa_num(self.ppa, PPA_SOURCE_NAME,
|
|
ppa_upstr_version, s)
|
|
rel_version = PPA_SERIES[s]
|
|
ppa_version = '%s-0ppa%s~ubuntu%s' % (ppa_upstr_version,
|
|
ppa_num, rel_version)
|
|
ppa_dsc = os.path.join(ppa_tmpdir, PPA_FILES_TEMPLATE.format(
|
|
ppa_version, '.dsc'))
|
|
ppa_chgs = os.path.join(ppa_tmpdir, PPA_FILES_TEMPLATE.format(
|
|
ppa_version, '_source.changes'))
|
|
changelog = CHANGELOG_TEMPLATE.format(ppa_version=ppa_version,
|
|
series=s,
|
|
changes=changes,
|
|
uid=self.uid,
|
|
time=now_formatted)
|
|
|
|
with open(changelog_name, 'w') as chlfd:
|
|
chlfd.write(changelog)
|
|
|
|
print(' Make %s ppa, Signing with key: %s, %s' %
|
|
(ppa_version, self.keyid, self.uid))
|
|
if self.verbose:
|
|
check_call(['debuild', '-S'])
|
|
else:
|
|
check_call(['debuild', '-S'], stdout=FNULL)
|
|
print(' Upload %s ppa to %s' % (ppa_version, self.ppa))
|
|
if self.dry_run:
|
|
print(' Dry run: dput ppa:%s %s' % (self.ppa, ppa_chgs))
|
|
else:
|
|
check_call(['dput', ('ppa:%s' % self.ppa), ppa_chgs],
|
|
stdout=FNULL)
|
|
print('\n')
|
|
|
|
def search_and_sign_unsinged(self):
|
|
"""Search through last 'count' releases with assets without
|
|
.asc counterparts or releases withouth SHA256SUMS.txt.asc
|
|
"""
|
|
print('Sign releases on repo: %s' % self.repo)
|
|
print(' With key: %s, %s\n' % (self.keyid, self.uid))
|
|
releases = get_releases(self.repo)
|
|
|
|
if self.tag_name:
|
|
releases = [r for r in releases
|
|
if r.get('tag_name', None) == self.tag_name]
|
|
|
|
if len(releases) == 0:
|
|
print('No release with tag "%s" found, exit' % self.tag_name)
|
|
sys.exit(1)
|
|
elif not self.sign_drafts:
|
|
releases = [r for r in releases if not r.get('draft', False)]
|
|
|
|
# cycle through releases sorted by by publication date
|
|
releases.sort(key=cmp_to_key(compare_published_times))
|
|
for r in releases[:self.count]:
|
|
tag_name = r.get('tag_name', 'No tag_name')
|
|
is_draft = r.get('draft', False)
|
|
is_prerelease = r.get('prerelease', False)
|
|
created_at = r.get('created_at', '')
|
|
|
|
msg = 'Found %s%s tagged: %s, created at: %s' % (
|
|
'draft ' if is_draft else '',
|
|
'prerelease' if is_prerelease else 'release',
|
|
tag_name,
|
|
created_at
|
|
)
|
|
|
|
if not is_draft:
|
|
msg += ', published at: %s' % r.get('published_at', '')
|
|
|
|
print(msg)
|
|
|
|
asset_names = [a['name'] for a in r['assets']]
|
|
|
|
if not asset_names:
|
|
print(' No assets found, skip release\n')
|
|
continue
|
|
|
|
asc_names = [a for a in asset_names if a.endswith('.asc')]
|
|
other_names = [a for a in asset_names if not a.endswith('.asc')]
|
|
need_to_sign = False
|
|
|
|
if asset_names and not asc_names:
|
|
need_to_sign = True
|
|
|
|
if not need_to_sign:
|
|
for name in other_names:
|
|
if not '%s.asc' % name in asc_names:
|
|
need_to_sign = True
|
|
break
|
|
|
|
if not need_to_sign:
|
|
need_to_sign = '%s.asc' % SHA_FNAME not in asc_names
|
|
|
|
if need_to_sign or self.force:
|
|
self.sign_release(r, other_names, asc_names, r==releases[0])
|
|
else:
|
|
print(' Seems already signed, skip release\n')
|
|
|
|
|
|
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
|
|
|
|
|
@click.command(context_settings=CONTEXT_SETTINGS)
|
|
@click.option('-a', '--jks-alias',
|
|
help='jks key alias')
|
|
@click.option('-c', '--count', type=int,
|
|
help='Number of recently published releases to sign')
|
|
@click.option('-d', '--sign-drafts', is_flag=True,
|
|
help='Sing draft releases first')
|
|
@click.option('-f', '--force', is_flag=True,
|
|
help='Sing already signed releases')
|
|
@click.option('-g', '--tag-name',
|
|
help='Sing only release tagged with tag name')
|
|
@click.option('-k', '--keyid',
|
|
help='gnupg keyid')
|
|
@click.option('-K', '--jks-keystore',
|
|
help='jks keystore path')
|
|
@click.option('-l', '--ppa',
|
|
help='PPA in format uzername/ppa')
|
|
@click.option('-S', '--ppa-upstream-suffix',
|
|
help='upload upstream source with version suffix (ex p1)')
|
|
@click.option('-L', '--no-ppa', is_flag=True,
|
|
help='Do not make launchpad ppa')
|
|
@click.option('-n', '--dry-run', is_flag=True,
|
|
help='Do not uload signed files')
|
|
@click.option('-p', '--ask-passphrase', is_flag=True,
|
|
help='Ask to enter passphrase')
|
|
@click.option('-r', '--repo',
|
|
help='Repository in format username/reponame')
|
|
@click.option('-s', '--sleep', type=int,
|
|
help='Sleep number of seconds before signing')
|
|
@click.option('-t', '--token',
|
|
help='GigHub access token, to be set as'
|
|
' GITHUB_TOKEN environmet variable')
|
|
@click.option('-v', '--verbose', is_flag=True,
|
|
help='Make more verbose output')
|
|
@click.option('-z', '--zipalign-path',
|
|
help='zipalign path')
|
|
def main(**kwargs):
|
|
app = SignApp(**kwargs)
|
|
|
|
sleep = kwargs.pop('sleep', None)
|
|
if (sleep):
|
|
print('Sleep for %s seconds' % sleep)
|
|
time.sleep(sleep)
|
|
|
|
app.search_and_sign_unsinged()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
colorama.init()
|
|
main()
|