Adopt commit-based container tags (#232)

## Summary
- Replace git-tag-triggered container builds with path-based triggers on main and workflow_dispatch
- Image tags now encode upstream app version + commit SHA (`vX.Y.Z-<sha>`) for full traceability
- Replace `container-tag-and-release` task with `container-build-and-release` (dispatches workflows via Forgejo API)
- Update dagger `publish()` to accept `commit_sha` parameter
- Update all docs and references to the new workflow

## Deployment and Testing
- [ ] Merge to main
- [ ] `mise run container-build-and-release <name>` for each container to populate new-format tags
- [ ] Verify tags in registry via `mise run container-list`
- [ ] Existing images untouched — old tags remain available

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/232
This commit is contained in:
Erich Blume 2026-02-20 22:56:20 -08:00
commit ffa8727660
13 changed files with 363 additions and 258 deletions

View file

@ -0,0 +1,142 @@
#!/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 "<container>" help="Container name (directory under containers/)"
#USAGE flag "--ref <ref>" help="Commit SHA 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.ops.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")
short_sha = ref[:7]
image = f"blumeops/{container}"
# Show expected builds
builds = []
if has_dockerfile:
builds.append(f" dockerfile -> {REGISTRY}/{image}:v<version>-{short_sha}")
if has_nix:
builds.append(f" nix -> {REGISTRY}/{image}:v<version>-{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()