#!/usr/bin/env python3 """scripts/attach_release_asset.py — upload a file as a Gitea release attachment. REQ-228 (v1.18): PPTX (and any deck artifact) is attached to the phase's Gitea release. Uses the Gitea API: POST /api/v1/repos/{owner}/{repo}/releases/{id}/assets multipart form: name=, attachment= Usage: python3 scripts/attach_release_asset.py python3 scripts/attach_release_asset.py docs/presentations/nova-no-humans-platform.pptx 522 Token resolution: reads NOVA_GITEA_TOKEN (or ACDL_GITEA_TOKEN) from .env.secrets / .env, matching the ship_phase.sh pattern. Never uses shell env tokens. """ import os import sys import json import urllib.request import urllib.error from pathlib import Path GITEA_BASE = "https://git.cloudinit.dev" OWNER = "continuous-intelligence" REPO = "acdl" def resolve_token() -> str: for fn in (".env.secrets", ".env"): try: for line in Path(fn).read_text().splitlines(): if line.startswith("NOVA_GITEA_TOKEN=") or line.startswith("ACDL_GITEA_TOKEN="): return line.split("=", 1)[1].strip() except (FileNotFoundError, PermissionError): continue raise RuntimeError("No Gitea token found in .env.secrets or .env (NOVA_GITEA_TOKEN/ACDL_GITEA_TOKEN)") def attach_asset(file_path: str, release_id: str) -> dict: token = resolve_token() p = Path(file_path) if not p.is_file(): raise FileNotFoundError(f"Asset file not found: {file_path}") url = f"{GITEA_BASE}/api/v1/repos/{OWNER}/{REPO}/releases/{release_id}/assets" filename = p.name boundary = "----NovaBoundary7MAgYbk" body = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="name"\r\n\r\n' f"{filename}\r\n" f"--{boundary}\r\n" f'Content-Disposition: form-data; name="attachment"; filename="{filename}"\r\n' f"Content-Type: application/octet-stream\r\n\r\n" ).encode() + p.read_bytes() + f"\r\n--{boundary}--\r\n".encode() req = urllib.request.Request( url, data=body, headers={ "Authorization": f"token {token}", "Content-Type": f"multipart/form-data; boundary={boundary}", }, method="POST", ) try: resp = urllib.request.urlopen(req, timeout=60) return json.loads(resp.read()) except urllib.error.HTTPError as e: err = e.read().decode()[:300] raise RuntimeError(f"HTTP {e.code} attaching {filename} to release {release_id}: {err}") from e if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: attach_release_asset.py ") sys.exit(1) result = attach_asset(sys.argv[1], sys.argv[2]) print(f"Attached: {result.get('name')} → release {sys.argv[2]} (asset id {result.get('id')})")