#!/usr/bin/env python3 """Export the frozen preview to its existing empty draft; never publish it.""" import argparse import hashlib import json import re import shutil import subprocess import sys import time from pathlib import Path from urllib.parse import quote REPO = 'Quintine/RustRaptor' TAG = 'v0.1.0-preview.20261020' COMMIT = '7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555' RELEASE_ID = 418286902 UPLOAD_URL = f'https://uploads.github.com/repos/{REPO}/releases/{RELEASE_ID}/assets' FLOOR = 1_800_000_000 def digest(path): with Path(path).open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() def write_json(path, value): with Path(path).open('x', encoding='utf-8') as stream: json.dump(value, stream, indent=2) stream.write('\n') def checked_release(release): if (release.get('id') != RELEASE_ID or release.get('tag_name') != TAG or release.get('target_commitish') != COMMIT or release.get('draft') is not True or release.get('prerelease') is not True): raise ValueError('existing release ID/tag/target/draft state differs') return release def checked_tag(reference, annotated=None): if reference.get('ref') != 'refs/tags/' + TAG: raise ValueError('wrong frozen tag reference') obj = reference.get('object', {}) if obj.get('type') == 'tag': if (not isinstance(annotated, dict) or annotated.get('sha') != obj.get('sha') or annotated.get('tag') != TAG): raise ValueError('annotated tag identity differs') obj = annotated.get('object', {}) if obj.get('type') != 'commit' or obj.get('sha') != COMMIT: raise ValueError('frozen tag no longer points to the tested commit') def checked_remote(remote, name, size, sha256): if (remote.get('name') != name or remote.get('size') != size or remote.get('state') != 'uploaded' or remote.get('digest') != 'sha256:' + sha256): raise ValueError('remote asset name/size/state/SHA-256 differs; retain local export') return remote def checked_receipt(plan, plan_sha, asset, receipt): if (receipt.get('status') != 'ZIP_CRC_AND_COMPLETE_PAYLOAD_SHA256_PASS' or receipt.get('asset') != asset['name'] or receipt.get('kind') != asset['kind'] or receipt.get('checkpoint') != plan['snapshot'] or receipt.get('plan_sha256') != plan_sha or receipt.get('payload_files') != len(asset['files']) or receipt.get('payload_sha256') != {r['name']: r['sha256'] for r in asset['files']} or not re.fullmatch('[0-9a-f]{64}', receipt.get('sha256', '')) or not 0 < receipt.get('bytes', 0) < 2 * 1024**3): raise ValueError('local receipt differs from the one frozen runner plan') class DraftExporter: def __init__(self, package, state, temporary): self.package = package.resolve(strict=True) self.state = state.absolute() self.temporary = temporary.absolute() if any(directory.resolve() != directory for directory in (self.state, self.temporary)): raise ValueError('state and temporary paths must be literal paths without symlink ancestors') for directory in (self.state, self.temporary): if directory.is_relative_to(self.package): raise ValueError('release outputs must remain outside the frozen source tree') directory.mkdir(parents=True, exist_ok=False) self.receipts = self.state / 'receipts' self.remote = self.state / 'remote-receipts' self.receipts.mkdir() self.remote.mkdir() self.plan_path = self.state / 'release-plan.json' self.exporter = self.package / 'tools/export_release.py' self.verified = {} def log(self, event, **details): row = {'time_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'event': event, **details} print(json.dumps(row), flush=True) with (self.state / 'progress.jsonl').open('a') as stream: stream.write(json.dumps(row) + '\n') def api(self, endpoint): return json.loads(subprocess.check_output(['gh', 'api', endpoint], cwd=self.package)) def release(self): reference = self.api(f'repos/{REPO}/git/ref/tags/{TAG}') obj = reference.get('object', {}) annotated = None if obj.get('type') == 'tag': sha = obj.get('sha', '') if not re.fullmatch('[0-9a-f]{40}', sha): raise ValueError('unsafe annotated tag SHA') annotated = self.api(f'repos/{REPO}/git/tags/{sha}') checked_tag(reference, annotated) return checked_release(self.api(f'repos/{REPO}/releases/{RELEASE_ID}')) def snapshot(self): actual = {'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=self.package, text=True).strip(), 'runtime_manifest_sha256': digest(self.package / 'runtime-manifest.json'), 'windows_manifest_sha256': digest(self.package / 'runtime-windows-manifest.json')} if actual['commit'] != COMMIT or subprocess.check_output( ['git', 'status', '--porcelain', '--untracked-files=all'], cwd=self.package): raise ValueError('frozen source commit or cleanliness differs') if hasattr(self, 'plan') and actual != self.plan['snapshot']: raise ValueError('frozen runtime manifests changed') if shutil.disk_usage(self.package).free < FLOOR: raise ValueError('runner workspace crossed the 1.8GB free-space floor') def remote_members(self, release): assets = release.get('assets', []) names = [asset.get('name') for asset in assets] if len(names) != len(set(names)) or set(names) != set(self.verified): raise ValueError('unexpected, missing or duplicate remote assets; no clobber allowed') for asset in assets: receipt = self.verified[asset['name']] checked_remote(asset, asset['name'], receipt['bytes'], receipt['sha256']) return assets def upload(self, path, name, expected_sha, expected_size): self.snapshot() self.remote_members(self.release()) if (path.is_symlink() or not path.is_file() or path.stat().st_size != expected_size or digest(path) != expected_sha): raise ValueError('local upload no longer matches authenticated receipt') self.log('uploading', asset=name, bytes=expected_size, sha256=expected_sha) result = subprocess.run(['gh', 'api', '--method', 'POST', UPLOAD_URL + '?name=' + quote(name, safe=''), '--input', str(path), '-H', 'Content-Type: application/octet-stream'], cwd=self.package, capture_output=True, text=True) (self.state / (name + '.upload.stdout')).write_text(result.stdout) (self.state / (name + '.upload.stderr')).write_text(result.stderr) if result.returncode: raise RuntimeError('upload returned nonzero; retain local file and inspect remote draft before retry') uploaded = checked_remote(json.loads(result.stdout), name, expected_size, expected_sha) release = self.release() matches = [asset for asset in release['assets'] if asset.get('name') == name] if len(matches) != 1 or matches[0].get('id') != uploaded.get('id'): raise ValueError('uploaded asset is not uniquely attached to the guarded draft') checked_remote(matches[0], name, expected_size, expected_sha) self.verified[name] = {'bytes': expected_size, 'sha256': expected_sha} self.remote_members(release) write_json(self.remote / (name + '.json'), matches[0]) self.log('remote_sha256_verified', asset=name, bytes=expected_size, sha256=expected_sha) def run(self): self.snapshot() initial = self.release() if initial.get('assets'): raise ValueError('draft must be completely empty before this one-pass export') write_json(self.state / 'initial-empty-draft.json', initial) subprocess.run([sys.executable, '-B', str(self.exporter), 'plan', '--package', str(self.package), '--expected-commit', COMMIT, '--output', str(self.plan_path)], check=True, cwd=self.package) self.plan = json.loads(self.plan_path.read_text()) plan_sha = digest(self.plan_path) if (len(self.plan['assets']) != 9 or self.plan['snapshot']['commit'] != COMMIT or sorted(asset['kind'] for asset in self.plan['assets']) != ['linux'] + ['shared-data'] * 7 + ['windows']): raise ValueError('frozen preview no longer has exactly the expected nine ZIPs') for asset in self.plan['assets']: self.snapshot() self.remote_members(self.release()) name = asset['name'] path = self.temporary / name receipt_path = self.receipts / (name + '.json') self.log('building', asset=name, uncompressed_bytes=asset['uncompressed_bytes']) subprocess.run([sys.executable, '-B', str(self.exporter), 'archive', '--plan', str(self.plan_path), '--asset', name, '--output', str(path), '--receipt', str(receipt_path)], check=True, cwd=self.package) receipt = json.loads(receipt_path.read_text()) checked_receipt(self.plan, plan_sha, asset, receipt) self.upload(path, name, receipt['sha256'], receipt['bytes']) path.unlink() # Only this just-authenticated disposable archive, after remote proof. self.log('temporary_export_removed', asset=name) sums = self.state / 'SHA256SUMS' manifest = self.state / 'release-manifest.json' subprocess.run([sys.executable, '-B', str(self.exporter), 'checksums', '--plan', str(self.plan_path), '--receipts', str(self.receipts), '--output', str(sums), '--manifest-output', str(manifest), '--tag', TAG], check=True, cwd=self.package) for path in (sums, manifest, self.package / 'release/Install-Linux.sh', self.package / 'release/Install-Windows.ps1'): self.upload(path, path.name, digest(path), path.stat().st_size) final = self.release() self.remote_members(final) if len(final['assets']) != 13: raise ValueError('complete preview must contain nine ZIPs plus four companion assets') write_json(self.state / 'final-13-assets-draft-still-unpublished.json', final) self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=COMMIT) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--package', type=Path, required=True) parser.add_argument('--state', type=Path, required=True) parser.add_argument('--temporary', type=Path, required=True) args = parser.parse_args() DraftExporter(args.package, args.state, args.temporary).run() if __name__ == '__main__': main()