#!/usr/bin/env python3 """Stage one target-bound pending manifest against shared immutable app artifacts. No signing, target mutation, publication, or release-feed changes. Target names and access information stay in the operator's private mapping, never in public assets. """ import argparse import json import os from pathlib import Path import subprocess from release_payload import check_source, digest, os_component, regular def prepare(root, version, plan, base_url, release_date=None): check_source(root, version) component = os_component(plan, version, base_url, content_addressed=True) raw = regular(plan) if digest(raw) != component['sha256']: raise ValueError('OS plan changed during preparation') artifacts = root / 'releases' / ('v' + version) backend = artifacts / 'archipelago' frontend = artifacts / ('archipelago-frontend-' + version + '.tar.gz') if not backend.is_file() or not frontend.is_file(): raise ValueError('shared backend/frontend artifacts must already be built') pending = root / 'releases/pending' / ('v' + version) / 'targets' / component['sha256'] pending.mkdir(parents=True, mode=0o700, exist_ok=False) asset = artifacts / ('os-update-plan-' + component['sha256'] + '.json') # An existing identical content-addressed plan is reusable; changed bytes or # symlinks are refused, and existing pending manifests are never overwritten. if os.path.lexists(asset): if regular(asset) != raw: raise ValueError('existing OS-plan asset differs') else: with os.fdopen(os.open(asset, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644), 'wb') as stream: stream.write(raw) stream.flush() os.fsync(stream.fileno()) manifest = pending / 'manifest.json' command = ['bash', str(root / 'scripts/create-release-manifest.sh'), '--version', version, '--backend', str(backend), '--frontend', str(frontend), '--os-plan', str(asset), '--target-specific', '--base-url', base_url, '--output', str(manifest)] if release_date: command.extend(['--date', release_date]) subprocess.run(command, cwd=root, check=True) subprocess.run(['bash', str(root / 'scripts/check-release-manifest.sh'), str(manifest)], cwd=root, check=True) print(json.dumps({'pending_manifest': str(manifest), 'os_plan_sha256': component['sha256'], 'shared_artifacts': True, 'signed': False, 'published': False})) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--version', required=True) parser.add_argument('--os-plan', type=Path, required=True) parser.add_argument('--base-url', default='https://source.archipelago-foundation.org/lfg2025/archy/releases/download') parser.add_argument('--date') args = parser.parse_args() prepare(Path(__file__).resolve().parents[1], args.version, args.os_plan.absolute(), args.base_url, args.date)