#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" # dependencies = ["typer>=0.15.0", "httpx>=0.28.0"] # /// #MISE description="Trigger container build workflows via Forgejo API" #USAGE arg "" help="Container name (directory under containers/)" #USAGE flag "--ref " help="Commit SHA or branch to build (defaults to current HEAD)" #USAGE flag "--dry-run" help="Show what would be done without triggering" """Trigger container build workflows via Forgejo API dispatch. Dispatches both Build Container and Build Container (Nix) workflows. Each workflow checks for its build file and skips if not present. """ import subprocess import sys from pathlib import Path import httpx import typer REGISTRY = "registry.ops.eblu.me" FORGE_URL = "https://forge.eblu.me" FORGE_API = f"{FORGE_URL}/api/v1" REPO = "eblume/blumeops" FORGE_ACTIONS = f"{FORGE_URL}/{REPO}/actions" WORKFLOWS = [ "build-container.yaml", "build-container-nix.yaml", ] app = typer.Typer(add_completion=False) def git(*args: str) -> str: result = subprocess.run( ["git", *args], capture_output=True, text=True, check=True ) return result.stdout.strip() def get_forge_token() -> str: result = subprocess.run( ["op", "read", "op://blumeops/w3663ffnvkewbftncqxtcpeavy/api-token"], capture_output=True, text=True, check=True, ) return result.stdout.strip() def list_containers() -> None: typer.echo("Available containers:") for d in sorted(Path("containers").iterdir()): if not d.is_dir(): continue types = [] if (d / "Dockerfile").exists(): types.append("dockerfile") if (d / "default.nix").exists(): types.append("nix") if types: typer.echo(f" - {d.name} ({', '.join(types)})") @app.command() def main( container: str = typer.Argument(help="Container name (directory under containers/)"), ref: str = typer.Option("", "--ref", help="Commit SHA to build (defaults to current HEAD)"), dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without triggering"), ) -> None: """Trigger container build workflows via Forgejo API dispatch.""" container_dir = Path("containers") / container has_dockerfile = (container_dir / "Dockerfile").exists() has_nix = (container_dir / "default.nix").exists() if not has_dockerfile and not has_nix: typer.echo(f"Error: No Dockerfile or default.nix found in '{container_dir}'") typer.echo() list_containers() raise typer.Exit(1) if not ref: ref = git("rev-parse", "HEAD") else: # Resolve short SHAs or branch names to full SHA ref = git("rev-parse", ref) short_sha = ref[:7] image = f"blumeops/{container}" # Show expected builds builds = [] if has_dockerfile: builds.append(f" dockerfile -> {REGISTRY}/{image}:v-{short_sha}") if has_nix: builds.append(f" nix -> {REGISTRY}/{image}:v-{short_sha}-nix") if dry_run: typer.echo("[dry-run mode]") typer.echo(f"Container: {container}") typer.echo(f"Commit: {ref} ({short_sha})") typer.echo(f"Expected builds:") for b in builds: typer.echo(b) typer.echo() if dry_run: typer.echo("[dry-run] Would dispatch workflows:") for wf in WORKFLOWS: typer.echo(f" - {wf}") typer.echo() typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") return token = get_forge_token() headers = { "Authorization": f"token {token}", "Content-Type": "application/json", } for wf in WORKFLOWS: url = f"{FORGE_API}/repos/{REPO}/actions/workflows/{wf}/dispatches" payload = { "ref": "main", "inputs": { "container": container, "ref": ref, }, } resp = httpx.post(url, json=payload, headers=headers, timeout=30) if resp.status_code == 204: typer.echo(f"Dispatched {wf}") else: typer.echo(f"Error dispatching {wf}: {resp.status_code} {resp.text}") raise typer.Exit(1) typer.echo() typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") if __name__ == "__main__": app()