#!/usr/bin/env python3 """Taggebundene Vierzielpakete hochladen und direkt auf Gitea veröffentlichen.""" import argparse import json import os from pathlib import Path import re from urllib.error import HTTPError from urllib.parse import quote, urlparse from urllib.request import Request, HTTPRedirectHandler, build_opener import distribution as dist class SafeRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): redirected = super().redirect_request(req, fp, code, msg, headers, newurl) if redirected and urlparse(newurl).netloc != urlparse(req.full_url).netloc: redirected.remove_header('Authorization') return redirected class Gitea: def __init__(self): self.base = os.environ['GITEA_SERVER_URL'].rstrip('/') dist.require(self.base == 'https://git.rfc1437.de', 'Release nur am bestehenden Origin') dist.require(os.environ['GITEA_REPOSITORY'] == 'hugo/TerminalBasic', 'Falsches Release-Repository') self.api = self.base + '/api/v1/repos/hugo/TerminalBasic' self.token = os.environ['TB_RELEASE_TOKEN'] self.open = build_opener(SafeRedirect()).open def request(self, method, path, data=None): raw = isinstance(data, bytes) headers = {'Authorization': 'token ' + self.token, 'Content-Type': 'application/octet-stream' if raw else 'application/json'} payload = data if raw or data is None else json.dumps(data).encode() with self.open(Request(self.api + path, data=payload, headers=headers, method=method), timeout=120) as response: return json.load(response) def release_version(tag): dist.require(re.fullmatch(r'v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)', tag), 'Release-Tag muss vX.Y.Z entsprechen') return tag[1:] def expected(version): return [n for t in dist.TARGETS for n in (dist.name(version, t), dist.name(version, t) + '.sha256')] def publish(api, directory, tag, revision): version = release_version(tag) dist.require(re.fullmatch('[0-9a-f]{40}', revision), 'Tag/Revision ungültig') dist.require(api.request('GET', '/tags/' + quote(tag, safe=''))['commit']['sha'] == revision, 'Tag zeigt auf andere Revision') names = expected(version) for target in dist.TARGETS: path = directory / dist.name(version, target) manifest = dist.verify(path, target, dist.ROOT / 'target/release/tb-template') dist.require(manifest['revision'] == revision and not manifest['source_dirty'], 'Paketrevision falsch/uncommitted') dist.require(path.with_name(path.name + '.sha256').read_text() == f'{dist.digest(path)} {path.name}\n', 'Paketprüfsumme falsch') try: release = api.request('GET', '/releases/tags/' + quote(tag, safe='')) except HTTPError as error: if error.code != 404: raise error.close() release = api.request('POST', '/releases', dict(tag_name=tag, target_commitish=revision, name='Terminal Basic ' + version, draft=True, body=f'Vierzielpakete für Revision {revision}. Upload läuft.')) dist.require(release['draft'], 'Bereits veröffentlichten Release nicht verändern') # Wiederholungen ergänzen fehlende Uploads; vorhandene Assets werden nicht ersetzt. existing = {a['name']: a for a in release.get('assets', [])} dist.require(set(existing) <= set(names), 'Unerwartete Assets im Entwurf') for name in names: data = (directory / name).read_bytes() if name in existing: dist.require(existing[name]['size'] == len(data), f'Abweichende Größe des vorhandenen Assets: {name}') else: api.request('POST', f'/releases/{release["id"]}/assets?name=' + quote(name), data) verify_uploads(api, release['id'], directory, names) api.request('PATCH', f'/releases/{release["id"]}', dict(draft=False, body=f'Terminal Basic {version}, Revision {revision}. Vier Zielpakete mit SHA-256-Prüfsummen.')) return release['id'] def verify_uploads(api, release_id, directory, names): release = api.request('GET', f'/releases/{release_id}') assets = release.get('assets', []) dist.require(len(assets) == len(names) and {a['name'] for a in assets} == set(names), 'Unvollständiger Asset-Upload') for asset in assets: dist.require(asset['size'] == (directory / asset['name']).stat().st_size, f'Uploadgröße falsch: {asset["name"]}') return release def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--packages', type=Path, required=True) parser.add_argument('--tag', required=True); parser.add_argument('--revision', required=True) args = parser.parse_args() publish(Gitea(), args.packages, args.tag, args.revision) if __name__ == '__main__': main()