Files
acdl/scripts/attach_release_asset.py
T
Jon Chery 863f482f9c feat(P3b): python-pptx generator — structured editable S&P-themed PPTX (REQ-269,270)
New scripts/render_pptx.py parses the consolidated -marp.md and
produces a structured, editable, S&P-themed PPTX via python-pptx.
16:9; title slide black bg + red top bar; content slides with red H2
titles, bullets, blockquotes, embedded PNGs, native tables, benefit
callouts. Added python-pptx>=0.6.23 to pyproject [slides] optional-dep.
render_slides.sh Step 4 produces it; attach_release_asset.py extended
for dual PPTX. Output: nova-autonomous-cloud-delivery-python.pptx.

---ci---
project: acdl
phase: 3
milestone: v1.23
status: execute
phase_role: execution
---/ci---
2026-08-12 00:21:17 +00:00

96 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""scripts/attach_release_asset.py — upload one or more files as Gitea release
attachments.
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>
REQ-270 (v1.23): supports dual PPTX attachment — the MARP PPTX (primary,
attached first) and the python-pptx PPTX (comparison artifact). Multiple
file paths are accepted; the first is the primary attachment.
Usage:
python3 scripts/attach_release_asset.py <file-path> <release-id>
python3 scripts/attach_release_asset.py <file-path> <file-path-2>... <release-id>
python3 scripts/attach_release_asset.py docs/presentations/nova-autonomous-cloud-delivery.pptx 522
python3 scripts/attach_release_asset.py \
docs/presentations/nova-autonomous-cloud-delivery.pptx \
docs/presentations/nova-autonomous-cloud-delivery-python.pptx 522
The last positional argument is always the release id; every preceding
argument is an asset path (backward compatible with the single-asset call).
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> [<file-path-2>...] <release-id>")
sys.exit(1)
asset_paths = sys.argv[1:-1]
release_id = sys.argv[-1]
for idx, path in enumerate(asset_paths):
result = attach_asset(path, release_id)
primary = " (primary)" if idx == 0 and len(asset_paths) > 1 else ""
print(f"Attached{primary}: {result.get('name')} → release {release_id} (asset id {result.get('id')})")