github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/misc/gen_release.py (about) 1 #!/usr/bin/env python3 2 """Script to create Github releases & generate release notes.""" 3 4 import json 5 import logging 6 import os 7 import subprocess 8 import sys 9 import zipfile 10 11 from third_party.python import colorlog, requests 12 from third_party.python.absl import app, flags 13 14 logging.root.handlers[0].setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)s: %(message)s')) 15 16 17 flags.DEFINE_string('github_token', None, 'Github API token') 18 flags.DEFINE_string('signer', None, 'Release signer binary') 19 flags.DEFINE_bool('dry_run', False, "Don't actually do the release, just print it.") 20 flags.mark_flag_as_required('github_token') 21 FLAGS = flags.FLAGS 22 23 24 PRERELEASE_MESSAGE = """ 25 This is a prerelease version of Please. Bugs and partially-finished features may abound. 26 27 Caveat usor! 28 """ 29 30 31 class ReleaseGen: 32 33 def __init__(self, github_token:str, dry_run:bool=False): 34 self.url = 'https://api.github.com' 35 self.releases_url = self.url + '/repos/thought-machine/please/releases' 36 self.upload_url = self.releases_url.replace('api.', 'uploads.') + '/<id>/assets?name=' 37 self.session = requests.Session() 38 self.session.verify = '/etc/ssl/certs/ca-certificates.crt' 39 if not dry_run: 40 self.session.headers.update({ 41 'Accept': 'application/vnd.github.v3+json', 42 'Authorization': 'token ' + github_token, 43 }) 44 self.version = self.read_file('VERSION').strip() 45 self.version_name = 'Version ' + self.version 46 self.is_prerelease = 'a' in self.version or 'b' in self.version 47 self.known_content_types = { 48 '.gz': 'application/gzip', 49 '.xz': 'application/x-xz', 50 '.asc': 'text/plain', 51 } 52 53 def needs_release(self): 54 """Returns true if the current version is not yet released to Github.""" 55 url = self.releases_url + '/tags/v' + self.version 56 logging.info('Checking %s for release...', url) 57 response = self.session.get(url) 58 return response.status_code == 404 59 60 def release(self): 61 """Submits a new release to Github.""" 62 data = { 63 'tag_name': 'v' + self.version, 64 'target_commitish': os.environ.get('CIRCLE_SHA1'), 65 'name': 'Please v' + self.version, 66 'body': '\n'.join(self.get_release_notes()), 67 'prerelease': self.is_prerelease, 68 'draft': not self.is_prerelease, 69 } 70 if FLAGS.dry_run: 71 logging.info('Would post the following to Github: %s', json.dumps(data, indent=4)) 72 return 73 logging.info('Creating release: %s', json.dumps(data, indent=4)) 74 response = self.session.post(self.releases_url, json=data) 75 response.raise_for_status() 76 data = response.json() 77 self.upload_url = data['upload_url'].replace('{?name,label}', '?name=') 78 logging.info('Release id %s created', data['id']) 79 80 def upload(self, artifact:str): 81 """Uploads the given artifact to the new release.""" 82 # Artifact names aren't unique between OSs; make them so. 83 arch = 'darwin_amd64' if 'darwin' in artifact else 'linux_amd64' 84 filename = os.path.basename(artifact).replace(self.version, self.version + '_' + arch) 85 _, ext = os.path.splitext(filename) 86 content_type = self.known_content_types[ext] 87 url = self.upload_url + filename 88 if FLAGS.dry_run: 89 logging.info('Would upload %s to %s as %s', filename, url, content_type) 90 return 91 logging.info('Uploading %s to %s as %s', filename, url, content_type) 92 with open(artifact, 'rb') as f: 93 self.session.headers.update({'Content-Type': content_type}) 94 response = self.session.post(url, data=f) 95 response.raise_for_status() 96 print('%s uploaded' % filename) 97 98 def sign(self, artifact:str) -> str: 99 """Creates a detached ASCII-armored signature for an artifact.""" 100 # We expect the PLZ_GPG_KEY and GPG_PASSWORD env vars to be set. 101 out = artifact + '.asc' 102 if FLAGS.dry_run: 103 logging.info('Would sign %s into %s', artifact, out) 104 else: 105 subprocess.check_call([FLAGS.signer, '-o', out, '-i', artifact]) 106 return out 107 108 def get_release_notes(self): 109 """Yields the changelog notes for a given version.""" 110 found_version = False 111 for line in self.read_file('ChangeLog').split('\n'): 112 if line.startswith(self.version_name): 113 found_version = True 114 yield 'This is Please v%s' % self.version 115 elif line.startswith('------'): 116 continue 117 elif found_version: 118 if line.startswith('Version '): 119 return 120 elif line.startswith(' '): 121 # Markdown comes out nicer if we remove some of the spacing. 122 line = line[3:] 123 yield line 124 if self.is_prerelease: 125 logging.warning("No release notes found, continuing anyway since it's a prerelease") 126 yield PRERELEASE_MESSAGE.strip() 127 else: 128 raise Exception("Couldn't find release notes for " + self.version_name) 129 130 def read_file(self, filename): 131 """Read a file from the .pex.""" 132 with zipfile.ZipFile(sys.argv[0]) as zf: 133 return zf.read(filename).decode('utf-8') 134 135 136 def main(argv): 137 r = ReleaseGen(FLAGS.github_token, dry_run=FLAGS.dry_run) 138 if not r.needs_release(): 139 logging.info('Current version has already been released, nothing to be done!') 140 return 141 # Check we can sign the artifacts before trying to create a release. 142 signatures = [r.sign(artifact) for artifact in argv[1:]] 143 r.release() 144 for artifact, signature in zip(argv[1:], signatures): 145 r.upload(artifact) 146 r.upload(signature) 147 148 149 if __name__ == '__main__': 150 app.run(main)