diff --git a/publish.py b/publish.py index da8c5018e..51a50a75b 100755 --- a/publish.py +++ b/publish.py @@ -3,6 +3,7 @@ # License: GPL v3 Copyright: 2017, Kovid Goyal import argparse +import datetime import io import json import mimetypes @@ -15,8 +16,8 @@ import subprocess import sys import tempfile import time -from contextlib import suppress -from typing import IO, Any, Dict, Iterable, List, Optional, cast +from contextlib import contextmanager, suppress +from typing import IO, Any, Dict, Generator, Iterable, List, Optional, cast import requests @@ -33,6 +34,7 @@ if ap is not None: appname = ap.group(1) ALL_ACTIONS = 'man html build tag sdist upload website'.split() +NIGHTLY_ACTIONS = 'man html build upload_nightly' def call(*cmd: str, cwd: Optional[str] = None) -> None: @@ -215,48 +217,56 @@ class GitHub(Base): # {{{ ): self.files, self.reponame, self.version, self.username, self.password, self.replace = ( files, reponame, version, username, password, replace) - self.current_tag_name = 'v' + self.version + self.current_tag_name = self.version if self.version == 'nightly' else ('v' + self.version) + self.is_nightly = self.current_tag_name == 'nightly' self.requests = s = requests.Session() s.auth = (self.username, self.password) s.headers.update({'Accept': 'application/vnd.github.v3+json'}) + def patch(self, url: str, fail_msg: str, **data: Any) -> None: + rdata = json.dumps(data) + try: + r = self.requests.patch(url, data=rdata) + except Exception: + time.sleep(15) + r = self.requests.patch(url, data=rdata) + if r.status_code != 200: + self.fail(r, fail_msg) + def __call__(self) -> None: releases = self.releases() # self.clean_older_releases(releases) release = self.create_release(releases) upload_url = release['upload_url'].partition('{')[0] + url_base = self.API + f'repos/{self.username}/{self.reponame}/releases/' + asset_url = url_base + 'assets/{}' existing_assets = self.existing_assets(release['id']) - for path, desc in self.files.items(): - self.info('') - url = self.API + 'repos/%s/%s/releases/assets/{}' % (self.username, - self.reponame) - fname = os.path.basename(path) - if fname in existing_assets: - self.info('Deleting %s from GitHub with id: %s' % - (fname, existing_assets[fname])) - r = self.requests.delete(url.format(existing_assets[fname])) + if self.is_nightly: + for fname in existing_assets: + self.info(f'Deleting {fname} from GitHub') + r = self.requests.delete(asset_url.format(existing_assets[fname])) if r.status_code != 204: self.fail(r, 'Failed to delete %s from GitHub' % fname) + purl = url_base + release["id"] + now = str(datetime.datetime.utcnow()).split('.')[0] + ' UTC' + with open('.git/refs/heads/master') as f: + commit = f.read().strip() + self.patch(purl, 'Failed to update nightly release description', + body=f'Nightly release, generated on: {now} from commit: {commit}') + for path, desc in self.files.items(): + self.info('') + fname = os.path.basename(path) + if self.is_nightly: + fname = fname.replace(version, 'nightly') + if fname in existing_assets: + self.info(f'Deleting {fname} from GitHub with id: {existing_assets[fname]}') + r = self.requests.delete(asset_url.format(existing_assets[fname])) + if r.status_code != 204: + self.fail(r, f'Failed to delete {fname} from GitHub') r = self.do_upload(upload_url, path, desc, fname) if r.status_code != 201: - self.fail(r, 'Failed to upload file: %s' % fname) - try: - r = self.requests.patch( - url.format(r.json()['id']), - data=json.dumps({ - 'name': fname, - 'label': desc - })) - except Exception: - time.sleep(15) - r = self.requests.patch( - url.format(r.json()['id']), - data=json.dumps({ - 'name': fname, - 'label': desc - })) - if r.status_code != 200: - self.fail(r, 'Failed to set label for %s' % fname) + self.fail(r, f'Failed to upload file: {fname}') + self.patch(asset_url.format(r.json()['id']), f'Failed to set label for {fname}', name=fname, label=desc) def clean_older_releases(self, releases: Iterable[Dict[str, Any]]) -> None: for release in releases: @@ -320,6 +330,8 @@ class GitHub(Base): # {{{ # Check for existing release if release['tag_name'] == self.current_tag_name: return release + if self.is_nightly: + raise SystemExit('No existing nightly release found on GitHub') url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame) r = self.requests.post( url, @@ -346,7 +358,7 @@ def get_github_data() -> Dict[str, str]: return {'username': un, 'password': pw} -def run_upload(args: Any) -> None: +def files_for_upload() -> Dict[str, str]: files = {} signatures = {} for f, desc in { @@ -367,31 +379,81 @@ def run_upload(args: Any) -> None: for f in files: if not os.path.exists(f): raise SystemExit(f'The release artifact {f} does not exist') + + +def run_upload(args: Any) -> None: gd = get_github_data() + files = files_for_upload() gh = GitHub(files, appname, version, gd['username'], gd['password']) gh() +def run_upload_nightly(args: Any) -> None: + gd = get_github_data() + files = files_for_upload() + gh = GitHub(files, appname, 'nightly', gd['username'], gd['password']) + gh() + + +def current_branch() -> str: + return subprocess.check_output(['git', 'symbolic-ref', '--short', 'HEAD']).decode('utf-8').strip() + + def require_git_master(branch: str = 'master') -> None: - b = subprocess.check_output(['git', 'symbolic-ref', '--short', 'HEAD']).decode('utf-8').strip() - if b != branch: + if current_branch() != branch: raise SystemExit('You must be in the {} git branch'.format(branch)) +def safe_read(path: str) -> str: + with suppress(FileNotFoundError): + with open(path) as f: + return f.read() + return '' + + +@contextmanager +def change_to_git_master() -> Generator[None, None, None]: + stash_ref_before = safe_read('.git/refs/stash') + subprocess.check_call(['git', 'stash']) + try: + branch_before = current_branch() + if branch_before != 'master': + subprocess.check_call(['git', 'switch', 'master']) + try: + yield + finally: + if branch_before != 'master': + subprocess.check_call(['git', 'switch', branch_before]) + finally: + if stash_ref_before != safe_read('.git/refs/stash'): + subprocess.check_call(['git', 'stash', 'pop']) + + def require_penv() -> None: if 'PENV' not in os.environ: raise SystemExit('The PENV env var is not present, required for uploading releases') +def exec_actions(actions: Iterable[str], args: Any) -> None: + for action in actions: + print('Running', action) + cwd = os.getcwd() + globals()['run_' + action](args) + os.chdir(cwd) + + def main() -> None: - require_git_master() - require_penv() parser = argparse.ArgumentParser(description='Publish kitty') parser.add_argument( '--only', default=False, action='store_true', help='Only run the specified action, by default the specified action and all sub-sequent actions are run') + parser.add_argument( + '--nightly', + default=False, + action='store_true', + help='Upload a nightly release, ignores all other arguments') parser.add_argument( 'action', default='all', @@ -399,6 +461,12 @@ def main() -> None: choices=list(ALL_ACTIONS) + ['all'], help='The action to start with') args = parser.parse_args() + require_penv() + if args.nightly: + with change_to_git_master(): + exec_actions(NIGHTLY_ACTIONS, args) + return + require_git_master() if args.action == 'all': actions = list(ALL_ACTIONS) else: @@ -415,11 +483,7 @@ def main() -> None: return if actions == ['website']: actions.insert(0, 'html') - for action in actions: - print('Running', action) - cwd = os.getcwd() - globals()['run_' + action](args) - os.chdir(cwd) + exec_actions(actions, args) if __name__ == '__main__':