#!/usr/bin/env python3 """Publish a completed main build, then advance its update channel.""" import base64 import json import os from pathlib import Path import re import subprocess import urllib.error import urllib.parse import urllib.request class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): return None def main(): version = os.environ['VERSION'] channel = os.environ['CHANNEL'] if channel not in ('stable', 'beta', 'windows-beta'): raise ValueError('Unknown release channel') if not re.fullmatch(r'\d+\.\d+\.\d+(-beta\.\d+)?', version): raise ValueError('Invalid release version') if ('beta' in channel) != ('-beta.' in version): raise ValueError('Version must match release channel') if os.environ.get('GITHUB_REF') != 'refs/heads/main': raise ValueError('Releases must build from main') revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() windows = channel.startswith('windows-') tag = ('win-v' if windows else 'v') + version artifact = Path('ClaudeChecker-Installer.exe' if windows else 'ClaudeChecker.zip') notes = Path('windows/RELEASE_NOTES.md' if windows else 'RELEASE_NOTES.md').read_text() feed = {'stable': 'version.json', 'beta': 'version-beta.json', 'windows-beta': 'version-windows-beta.json'}[channel] server = os.environ['GITHUB_SERVER_URL'].rstrip('/') repo = os.environ['GITHUB_REPOSITORY'] opener = urllib.request.build_opener(NoRedirect) def api(path, method='GET', data=None, binary=False): body = data if binary else (json.dumps(data).encode() if data is not None else None) request = urllib.request.Request(f'{server}/api/v1/repos/{repo}{path}', data=body, method=method, headers={'Authorization': 'token ' + os.environ['GITEA_TOKEN'], 'Content-Type': 'application/octet-stream' if binary else 'application/json'}) with opener.open(request, timeout=600) as response: return json.load(response) # Never replace an existing release or advertise a partially uploaded asset. release = api('/releases', 'POST', {'tag_name': tag, 'target_commitish': revision, 'name': tag, 'body': notes + '\n\nBuilt from main: ' + revision, 'draft': True, 'prerelease': 'beta' in channel}) asset = api(f"/releases/{release['id']}/assets?name={urllib.parse.quote(artifact.name)}", 'POST', artifact.read_bytes(), binary=True) if asset['size'] != artifact.stat().st_size: raise ValueError('Uploaded asset size mismatch') api(f"/releases/{release['id']}", 'PATCH', {'draft': False}) content = json.dumps({'version': version, 'url': f'https://git.jamesbone.net/{repo}/releases/download/{tag}/{artifact.name}', 'notes': notes}, indent=2) + '\n' try: current = api(f'/contents/{feed}?ref=main') except urllib.error.HTTPError as error: if error.code != 404: raise current = None payload = {'branch': 'main', 'message': f'Update {channel} feed to {tag}', 'content': base64.b64encode(content.encode()).decode()} if current: payload['sha'] = current['sha'] api(f'/contents/{feed}', 'PUT' if current else 'POST', payload) print(f'Published {tag} and {feed} from {revision}') if __name__ == '__main__': main()