-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Start work on script to publish kitty
- Loading branch information
1 parent
3ee99a6
commit aff9a5c
Showing
1 changed file
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,303 @@ | ||
#!/usr/bin/env python3 | ||
# vim:fileencoding=utf-8 | ||
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> | ||
|
||
import argparse | ||
import io | ||
import json | ||
import mimetypes | ||
import os | ||
import pprint | ||
import re | ||
import shlex | ||
import subprocess | ||
import sys | ||
import time | ||
|
||
import requests | ||
|
||
os.chdir(os.path.dirname(os.path.abspath(__file__))) | ||
build_path = os.path.abspath('../build-kitty') | ||
raw = open('kitty/constants.py').read() | ||
nv = re.search( | ||
r'^version\s+=\s+\((\d+), (\d+), (\d+)\)', raw, flags=re.MULTILINE) | ||
version = '%s.%s.%s' % (nv.group(1), nv.group(2), nv.group(3)) | ||
appname = re.search( | ||
r"^appname\s+=\s+'([^']+)'", raw, flags=re.MULTILINE).group(1) | ||
|
||
ALL_ACTIONS = 'build tag upload'.split() | ||
|
||
|
||
def call(*cmd): | ||
if len(cmd) == 1: | ||
cmd = shlex.split(cmd[0]) | ||
ret = subprocess.Popen(cmd).wait() | ||
if ret != 0: | ||
raise SystemExit(ret) | ||
|
||
|
||
def run_build(args): | ||
os.chdir(build_path) | ||
call('./osx kitty') | ||
call('./osx shutdown') | ||
|
||
|
||
def run_tag(args): | ||
call('git tag -s v{0} -m version-{0}'.format(version)) | ||
call('git push origin v{0}'.format(version)) | ||
|
||
|
||
class ReadFileWithProgressReporting(io.BufferedReader): # {{{ | ||
def __init__(self, path, mode='rb'): | ||
io.BufferedReader.__init__(self, open(path, mode)) | ||
self.seek(0, os.SEEK_END) | ||
self._total = self.tell() | ||
self.seek(0) | ||
self.start_time = time.time() | ||
|
||
def __len__(self): | ||
return self._total | ||
|
||
def read(self, size): | ||
data = io.BufferedReader.read(self, size) | ||
if data: | ||
self.report_progress(len(data)) | ||
return data | ||
|
||
def report_progress(self, size): | ||
sys.stdout.buffer.write(b'\x1b[s') | ||
sys.stdout.buffer.write(b'\x1b[K') | ||
frac = float(self.tell()) / self._total | ||
mb_pos = self.tell() / float(1024**2) | ||
mb_tot = self._total / float(1024**2) | ||
kb_pos = self.tell() / 1024.0 | ||
kb_rate = kb_pos / (time.time() - self.start_time) | ||
bit_rate = kb_rate * 1024 | ||
eta = int((self._total - self.tell()) / bit_rate) + 1 | ||
eta_m, eta_s = eta / 60, eta % 60 | ||
sys.stdout.write( | ||
' %.1f%% %.1f/%.1fMB %.1f KB/sec %d minutes, %d seconds left' | ||
% (frac * 100, mb_pos, mb_tot, kb_rate, eta_m, eta_s)) | ||
sys.stdout.buffer.write(b'\x1b[u') | ||
if self.tell() >= self._total: | ||
sys.stdout.write('\n') | ||
t = int(time.time() - self.start_time) + 1 | ||
print('Upload took %d minutes and %d seconds at %.1f KB/sec' % | ||
(t / 60, t % 60, kb_rate)) | ||
sys.stdout.flush() | ||
|
||
|
||
# }}} | ||
|
||
|
||
class Base(object): # {{{ | ||
def __init__(self): | ||
self.d = os.path.dirname | ||
self.j = os.path.join | ||
self.a = os.path.abspath | ||
self.b = os.path.basename | ||
self.s = os.path.splitext | ||
self.e = os.path.exists | ||
|
||
def info(self, *args, **kwargs): | ||
print(*args, **kwargs) | ||
sys.stdout.flush() | ||
|
||
def warn(self, *args, **kwargs): | ||
print('\n' + '_' * 20, 'WARNING', '_' * 20) | ||
print(*args, **kwargs) | ||
print('_' * 50) | ||
sys.stdout.flush() | ||
|
||
|
||
# }}} | ||
|
||
|
||
class GitHub(Base): # {{{ | ||
|
||
API = 'https://api.github.com/' | ||
|
||
def __init__(self, | ||
files, | ||
reponame, | ||
version, | ||
username, | ||
password, | ||
replace=False): | ||
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.requests = s = requests.Session() | ||
s.auth = (self.username, self.password) | ||
s.headers.update({'Accept': 'application/vnd.github.v3+json'}) | ||
|
||
def __call__(self): | ||
releases = self.releases() | ||
# self.clean_older_releases(releases) | ||
release = self.create_release(releases) | ||
upload_url = release['upload_url'].partition('{')[0] | ||
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 r.status_code != 204: | ||
self.fail(r, 'Failed to delete %s from GitHub' % fname) | ||
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) | ||
|
||
def clean_older_releases(self, releases): | ||
for release in releases: | ||
if release.get( | ||
'assets', | ||
None) and release['tag_name'] != self.current_tag_name: | ||
self.info('\nDeleting old released installers from: %s' % | ||
release['tag_name']) | ||
for asset in release['assets']: | ||
r = self.requests.delete( | ||
self.API + 'repos/%s/%s/releases/assets/%s' % ( | ||
self.username, self.reponame, asset['id'])) | ||
if r.status_code != 204: | ||
self.fail( | ||
r, | ||
'Failed to delete obsolete asset: %s for release: %s' | ||
% (asset['name'], release['tag_name'])) | ||
|
||
def do_upload(self, url, path, desc, fname): | ||
mime_type = mimetypes.guess_type(fname)[0] | ||
self.info('Uploading to GitHub: %s (%s)' % (fname, mime_type)) | ||
with ReadFileWithProgressReporting(path) as f: | ||
return self.requests.post( | ||
url, | ||
headers={ | ||
'Content-Type': mime_type, | ||
'Content-Length': str(f._total) | ||
}, | ||
params={'name': fname}, | ||
data=f) | ||
|
||
def fail(self, r, msg): | ||
print(msg, ' Status Code: %s' % r.status_code, file=sys.stderr) | ||
print("JSON from response:", file=sys.stderr) | ||
pprint(dict(r.json()), stream=sys.stderr) | ||
raise SystemExit(1) | ||
|
||
def already_exists(self, r): | ||
error_code = r.json().get('errors', [{}])[0].get('code', None) | ||
return error_code == 'already_exists' | ||
|
||
def existing_assets(self, release_id): | ||
url = self.API + 'repos/%s/%s/releases/%s/assets' % ( | ||
self.username, self.reponame, release_id) | ||
r = self.requests.get(url) | ||
if r.status_code != 200: | ||
self.fail('Failed to get assets for release') | ||
return {asset['name']: asset['id'] for asset in r.json()} | ||
|
||
def releases(self): | ||
url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame | ||
) | ||
r = self.requests.get(url) | ||
if r.status_code != 200: | ||
self.fail(r, 'Failed to list releases') | ||
return r.json() | ||
|
||
def create_release(self, releases): | ||
' Create a release on GitHub or if it already exists, return the existing release ' | ||
for release in releases: | ||
# Check for existing release | ||
if release['tag_name'] == self.current_tag_name: | ||
return release | ||
url = self.API + 'repos/%s/%s/releases' % (self.username, self.reponame | ||
) | ||
r = self.requests.post( | ||
url, | ||
data=json.dumps({ | ||
'tag_name': self.current_tag_name, | ||
'target_commitish': 'master', | ||
'name': 'version %s' % self.version, | ||
'body': 'Release version %s' % self.version, | ||
'draft': False, | ||
'prerelease': False | ||
})) | ||
if r.status_code != 201: | ||
self.fail(r, 'Failed to create release for version: %s' % | ||
self.version) | ||
return r.json() | ||
|
||
|
||
# }}} | ||
|
||
|
||
def get_github_data(): | ||
with open(os.path.expanduser('~/work/env/private/github')) as f: | ||
un, pw = f.read().strip().split(':') | ||
return {'username': un, 'password': pw} | ||
|
||
|
||
def run_upload(args): | ||
files = { | ||
os.path.join(build_path, 'build', f.format(version)): desc | ||
for f, desc in { | ||
'osx/dist/kitty-{}.dmg': 'macOS dmg', | ||
} | ||
} | ||
for f in files: | ||
if not os.path.exists(f): | ||
raise SystemExit('The installer {} does not exist'.format(f)) | ||
gd = get_github_data() | ||
gh = GitHub(files, appname, version, 'kovidgoyal', gd['username'], | ||
gd['password']) | ||
gh() | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser(description='Publish kitty') | ||
parser.add_argument( | ||
'--only', | ||
default=False, | ||
action='store_true', | ||
help='Only run the specified action') | ||
parser.add_argument( | ||
'action', | ||
default='build', | ||
nargs='?', | ||
choices=ALL_ACTIONS, | ||
help='The action to start with') | ||
args = parser.parse_args() | ||
idx = ALL_ACTIONS.index(args.action) | ||
actions = ALL_ACTIONS[idx:] | ||
if args.only: | ||
del actions[1:] | ||
for action in actions: | ||
print('Running', action) | ||
cwd = os.getcwd() | ||
globals()['run_' + action](args) | ||
os.chdir(cwd) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |