Files
acdl/scripts/attach_release_asset.py
T
Jon Chery 71b6a4fa91 feat(P1): restore S&P Global Energy theme + PPTX automation (REQ-214, REQ-228)
REQ-214: Restore the S&P Global Energy Marp style: block (from commit
ae0cb58 / v1.9.2 P45) to the unified deck. Colors: H1/H2 #D6002A (red-core),
title-slide bg #1B1B1B (grey-90) + 8px #D6002A top accent, body #1B1B1B,
blockquote border #D6002A, table headers #F0F0F0, font 'Akkurat Pro' with
web-safe fallbacks. Nova header/footer text preserved (rebrand not touched).
HTML re-rendered (229 S&P color refs confirmed).

REQ-228: scripts/render_deck.sh (HTML + PPTX render + git add) +
scripts/attach_release_asset.py (Gitea release asset upload via API). PPTX
is now a first-class committed binary (D-141, no LFS). README updated:
'PPTX not committed' → 'PPTX committed + attached'. PPTX committed (3.6 MiB,
19 slides).

---ci---
project: acdl
phase: 1
milestone: v1.18
status: execute
requirements:
  covered: [REQ-214, REQ-228]
  partial: []
---/ci---
2026-08-06 15:05:01 +00:00

80 lines
2.8 KiB
Python
Executable File

#!/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=<filename>, attachment=<file bytes>
Usage:
python3 scripts/attach_release_asset.py <file-path> <release-id>
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 <file-path> <release-id>")
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')})")