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:
parent
0e2c10176d
commit
ffa8727660
13 changed files with 363 additions and 258 deletions
142
mise-tasks/container-build-and-release
Executable file
142
mise-tasks/container-build-and-release
Executable 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()
|
||||
|
|
@ -52,10 +52,11 @@ for dir in "$CONTAINER_DIR"/*/; do
|
|||
done
|
||||
|
||||
echo "---"
|
||||
echo "To release a new version:"
|
||||
echo " mise run container-tag-and-release <container> <version>"
|
||||
echo "To trigger a build:"
|
||||
echo " mise run container-build-and-release <container>"
|
||||
echo ""
|
||||
echo "One tag triggers all applicable workflows (dockerfile and/or nix)."
|
||||
echo "Dispatches both Dockerfile and Nix workflows (each skips if build file absent)."
|
||||
echo "Tags: vX.Y.Z-<sha> (Dockerfile), vX.Y.Z-<sha>-nix (Nix)"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " mise run container-tag-and-release nettest v1.0.0"
|
||||
echo " mise run container-build-and-release nettest"
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = ["typer>=0.15.0"]
|
||||
# ///
|
||||
#MISE description="Release a container image by creating a git tag"
|
||||
#USAGE arg "<container>" help="Container name (directory under containers/)"
|
||||
#USAGE arg "<version>" help="Version in vX.Y.Z format"
|
||||
#USAGE flag "--dry-run" help="Show what would be done without creating tags"
|
||||
"""Release a container image by creating a git tag that triggers CI builds.
|
||||
|
||||
One tag triggers all applicable workflows:
|
||||
- Dockerfile present -> Build Container workflow -> :v<version>
|
||||
- default.nix present -> Build Container (Nix) workflow -> :v<version>-nix
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
REGISTRY = "registry.ops.eblu.me"
|
||||
FORGE_ACTIONS = "https://forge.ops.eblu.me/eblume/blumeops/actions"
|
||||
|
||||
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 git_tag_exists(tag: str) -> bool:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", tag], capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
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/)"),
|
||||
version: str = typer.Argument(help="Version in vX.Y.Z format"),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without creating tags"),
|
||||
) -> None:
|
||||
"""Release a container image by creating a git tag that triggers CI builds."""
|
||||
if not re.match(r"^v\d+\.\d+\.\d+$", version):
|
||||
typer.echo("Error: Version must be in format vX.Y.Z (e.g. v1.0.0)")
|
||||
raise typer.Exit(1)
|
||||
|
||||
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)
|
||||
|
||||
image = f"blumeops/{container}"
|
||||
tag = f"{container}-{version}"
|
||||
|
||||
# Show what workflows will trigger
|
||||
builds = []
|
||||
if has_dockerfile:
|
||||
builds.append(f" dockerfile -> {REGISTRY}/{image}:{version}")
|
||||
if has_nix:
|
||||
builds.append(f" nix -> {REGISTRY}/{image}:{version}-nix")
|
||||
|
||||
if dry_run:
|
||||
typer.echo("[dry-run mode]")
|
||||
typer.echo(f"Container: {container}")
|
||||
typer.echo(f"Tag: {tag}")
|
||||
typer.echo(f"Builds:")
|
||||
for b in builds:
|
||||
typer.echo(b)
|
||||
typer.echo()
|
||||
|
||||
if git_tag_exists(tag):
|
||||
typer.echo(f"Error: Tag '{tag}' already exists")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if dry_run:
|
||||
typer.echo(f"[dry-run] Would create and push tag: {tag}")
|
||||
else:
|
||||
git("tag", tag)
|
||||
git("push", "origin", tag)
|
||||
typer.echo(f"Tag '{tag}' created and pushed")
|
||||
|
||||
typer.echo()
|
||||
typer.echo(f"Monitor builds at: {FORGE_ACTIONS}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
@ -168,7 +168,7 @@ def main(
|
|||
checklist_parts += [
|
||||
"\n[bold]Custom Container (hybrid):[/bold]\n",
|
||||
"• Check base image for updates\n",
|
||||
"• Rebuild container if needed: mise run container-tag-and-release\n",
|
||||
"• Rebuild container if needed: mise run container-build-and-release\n",
|
||||
"• Update ArgoCD manifest with new image tag\n",
|
||||
]
|
||||
elif svc_type == "argocd":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue