105 lines
4.8 KiB
Python
105 lines
4.8 KiB
Python
#!/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()
|