71b6a4fa91
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---
80 lines
2.8 KiB
Python
Executable File
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')})") |