#!/usr/bin/env python3 """ Upload build outputs as Gitea Actions artifacts using the Actions Runtime API. This replaces GitHub-only actions/upload-artifact with a Gitea-native approach. Uses ACTIONS_RUNTIME_URL and ACTIONS_RUNTIME_TOKEN provided by Gitea's act_runner to upload artifacts directly to the pipeline service. Usage: python3 scripts/upload_artifacts.py [path_glob2 ...] Environment (set by Gitea act_runner >= 1.21): ACTIONS_RUNTIME_URL — Pipeline service base URL (e.g. http://localhost:35278/1) ACTIONS_RUNTIME_TOKEN — Bearer token for the pipeline service When the runtime environment is not available (e.g. local testing), artifacts are saved to ./artifacts/.zip as a fallback. """ import glob import io import os import sys import urllib.error import urllib.request import zipfile _RUNTIME_UPLOADED = False def _upload_via_runtime(name, payload, runtime_url, runtime_token): url = "{}/_apis/pipeline/artifacts/{}".format(runtime_url.rstrip("/"), name) headers = { "Authorization": "Bearer " + runtime_token, "Content-Type": "application/octet-stream", "Content-Range": "bytes 0-{}/{}".format(len(payload) - 1, len(payload)), "x-tfs-filelength": str(len(payload)), } req = urllib.request.Request(url, data=payload, headers=headers, method="PUT") try: with urllib.request.urlopen(req) as resp: msg = "[upload-artifact] Uploaded '{}' ({} bytes) to Gitea runtime — HTTP {}" print(msg.format(name, len(payload), resp.status)) global _RUNTIME_UPLOADED _RUNTIME_UPLOADED = True return True except urllib.error.HTTPError as e: body = e.read().decode()[:300] if e.fp else "" print("[upload-artifact] Runtime upload failed: HTTP {} — {}".format(e.code, body)) return False def _save_locally(name, payload): dest = os.path.join("artifacts", "{}.zip".format(name)) os.makedirs(os.path.dirname(dest) or ".", exist_ok=True) with open(dest, "wb") as f: f.write(payload) print("[upload-artifact] Saved artifact to {} ({} bytes)".format(dest, len(payload))) def main(): if len(sys.argv) < 3: print("Usage: {} [path_glob ...]".format(sys.argv[0])) sys.exit(1) artifact_name = sys.argv[1] patterns = sys.argv[2:] files = [] for pattern in patterns: matched = glob.glob(pattern, recursive=True) files.extend(matched) files = sorted(set(f for f in files if os.path.isfile(f))) if not files: print("[upload-artifact] No files found for pattern(s): {}".format(patterns)) return buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: for f in files: zf.write(f, os.path.relpath(f)) payload = buf.getvalue() runtime_url = os.environ.get("ACTIONS_RUNTIME_URL", "").rstrip("/") runtime_token = os.environ.get("ACTIONS_RUNTIME_TOKEN", "") if runtime_url and runtime_token: ok = _upload_via_runtime(artifact_name, payload, runtime_url, runtime_token) if not ok: _save_locally(artifact_name, payload) else: print("[upload-artifact] ACTIONS_RUNTIME_URL/TOKEN not available — saving locally.") _save_locally(artifact_name, payload) print("[upload-artifact] Done — {} file(s) in '{}'.".format(len(files), artifact_name)) if __name__ == "__main__": main()