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 @@
Container builds now trigger automatically on merge to main (path-based) and use commit-SHA-based image tags (`vX.Y.Z-<sha>`) for full traceability. The `container-tag-and-release` task is replaced by `container-build-and-release` which dispatches workflows via the Forgejo API. Added pre-commit hook to keep container versions in sync with `service-versions.yaml`.

View file

@ -71,16 +71,14 @@ When an attempt fails and you discover prerequisites, the branch must be cleaned
The branch between attempts should contain only documentation. Code returns when prerequisites are satisfied and the next attempt succeeds.
### Build artifacts and tags
### Build artifacts
Mikado resets apply to branch code, not build artifacts. Container images in the registry and git tags created by `container-tag-and-release` are independent of branch lifecycle:
Mikado resets apply to branch code, not build artifacts. Container images in the registry are independent of branch lifecycle:
- **Git tags** point to commit SHAs, not branches — they survive branch deletion and force-pushes.
- **Registry images** are build outputs cached in zot — a wrong image is overwritten by the next release.
- **If a build succeeds but deployment fails**, the image is fine; the problem is elsewhere. Document what you learned, bump the version, and try again.
- **If a build fails in CI**, no image is pushed. Delete the git tag (`git tag -d <tag> && git push --delete origin <tag>`) and fix the nix/dockerfile before re-releasing.
Tag freely during leaf node work. The build IS the verification step — deferring it creates a chicken-and-egg where the card can't be marked complete without a built image.
- **Registry images** are build outputs cached in zot — tagged with commit SHAs, so each build is unique and traceable.
- **Automatic builds** trigger when container changes merge to main. Use `mise run container-build-and-release` for manual dispatch.
- **If a build succeeds but deployment fails**, the image is fine; the problem is elsewhere. Document what you learned and try again.
- **If a build fails in CI**, no image is pushed. Fix the nix/dockerfile and re-merge or re-dispatch.
## Card Conventions

View file

@ -18,7 +18,7 @@ Discovered while attempting [[deploy-authentik]]: the deployment references `reg
1. Verify `containers/authentik/default.nix` builds — locally via Dagger (`dagger call build-nix --src=. --container-name=authentik`) or on ringtail (the CI nix builder runs there)
2. The `ak` entrypoint needs bash (included via `bashInteractive`) and orchestrates both `server` and `worker` subcommands
3. Tag and release: `mise run container-tag-and-release authentik v1.0.0`
3. Trigger build: `mise run container-build-and-release authentik`
4. Verify the `-nix` tagged image appears in the registry
## What We Learned

View file

@ -52,20 +52,23 @@ nix-build containers/<name>/default.nix -o result
## 3. Release
Once the image builds cleanly, create a tagged release:
Container builds trigger automatically when changes to `containers/<name>/` are merged to `main`. Both workflows fire and each skips if the relevant build file is absent.
To trigger a manual build (e.g. from a branch or to rebuild at a specific commit):
```bash
mise run container-tag-and-release <name> v1.0.0
mise run container-build-and-release <name>
mise run container-build-and-release <name> --ref <commit-sha>
```
Use `--dry-run` to preview without creating tags.
This creates a single git tag `<name>-v1.0.0` and pushes it. Both Forgejo workflows trigger on the tag — each checks for its build file and skips if not present:
Use `--dry-run` to preview without dispatching.
| Build file | Workflow | Runner | Registry tag |
|------------|----------|--------|--------------|
| `Dockerfile` | `build-container.yaml` | `k8s` (indri) | `:v1.0.0` |
| `default.nix` | `build-container-nix.yaml` | `nix-container-builder` ([[ringtail]]) | `:v1.0.0-nix` |
| `Dockerfile` | `build-container.yaml` | `k8s` (indri) | `:vX.Y.Z-<sha>` |
| `default.nix` | `build-container-nix.yaml` | `nix-container-builder` ([[ringtail]]) | `:vX.Y.Z-<sha>-nix` |
The version (`X.Y.Z`) is extracted from `ARG CONTAINER_APP_VERSION=` in the Dockerfile or `version = "..."` in `default.nix`. The SHA is the short (7-char) commit hash.
Check available images and tags with:
@ -78,7 +81,7 @@ mise run container-list
Change the image reference in `argocd/manifests/<service>/deployment.yaml`:
```yaml
image: registry.ops.eblu.me/blumeops/<name>:v1.0.0
image: registry.ops.eblu.me/blumeops/<name>:vX.Y.Z-abc1234
```
Then deploy per [[deploy-k8s-service]].

View file

@ -1,7 +1,6 @@
---
title: Adopt Commit-Based Container Tags
modified: 2026-02-20
status: active
requires:
- add-container-version-sync-check
tags:
@ -29,9 +28,9 @@ Currently, container builds trigger on git tags matching `<container>-vX.Y.Z`. T
### Triggers
1. **Merged changes to main** — any push to `main` that modifies files under `containers/<name>/` triggers builds for that container
2. **Manual workflow dispatch** — for ad-hoc builds (e.g., testing on a branch). Accepts two inputs:
2. **Manual workflow dispatch** — for ad-hoc builds. Accepts two inputs:
- `container` (required) — which container to build
- `ref` (optional, string) — the source commit SHA to build, defaulting to `HEAD` of `main`
- `ref` (optional, string) — the source commit SHA to build, defaulting to `GITHUB_SHA`
Both the Dockerfile and Nix workflows fire for each trigger, each bailing out if the container lacks the relevant build file (same as today).
@ -40,56 +39,43 @@ Both the Dockerfile and Nix workflows fire for each trigger, each bailing out if
Each container's version is extracted at build time from existing declarations — no separate VERSION file:
- **Dockerfile builds**: parsed from `ARG CONTAINER_APP_VERSION=<value>` in the Dockerfile
- **Nix builds**: extracted via `dagger call nix-version` or `nix eval`
- **Nix builds**: extracted from `version = "..."` in `default.nix`, or `CONTAINER_APP_VERSION` from the Dockerfile, or `dagger call nix-version` for nixpkgs packages
The [[add-container-version-sync-check]] pre-commit check ensures these declarations stay in sync with `service-versions.yaml`. See [[pin-container-versions]] for the work to ensure every container has a parseable version.
### Image Tag Format
The registry image tag encodes the app version and the exact source commit:
| Scenario | Dockerfile tag | Nix tag |
|----------|---------------|---------|
| Main branch build | `vX.Y.Z-<sha>` and `vX.Y.Z-<sha>-main` | `vX.Y.Z-<sha>-nix` and `vX.Y.Z-<sha>-main-nix` |
| Manual dispatch | `vX.Y.Z-<sha>` | `vX.Y.Z-<sha>-nix` |
| Build type | Tag format | Example |
|------------|-----------|---------|
| Dockerfile | `vX.Y.Z-<sha>` | `v2.2.17-abc1234` |
| Nix | `vX.Y.Z-<sha>-nix` | `v2.17.0-abc1234-nix` |
Where:
- `X.Y.Z` is the version of the most relevant bundled app (e.g., miniflux `2.2.5`, navidrome `0.53.3`)
- `<sha>` is the short commit SHA of the source tree used for the build
The `-main` tag indicates a build from the merged main branch, suitable for production deployment. Non-main builds (manual dispatch) omit this suffix.
- `X.Y.Z` is the version of the most relevant bundled app (e.g., miniflux `2.2.17`, navidrome `0.60.3`)
- `<sha>` is the 7-char short commit SHA of the source tree used for the build
### What This Replaces
- The `container-tag-and-release` mise task is **renamed and repurposed** to `container-build-and-release` — it triggers a manual workflow dispatch instead of creating git tags. It sends the current `HEAD` SHA so that it works from any branch, not just main
- The `container-tag-and-release` mise task is **replaced** by `container-build-and-release` — it triggers a manual workflow dispatch instead of creating git tags
- Git tags of the form `<container>-vX.Y.Z` are no longer used to trigger builds
- The `container-list` mise task should be updated to display the new tag format
- The `container-list` mise task displays the new tag format
## Key Files
| File | Change |
|------|--------|
| `.forgejo/workflows/build-container.yaml` | Replace tag trigger with path + dispatch triggers; compute version and SHA; push multiple tags |
| `.forgejo/workflows/build-container.yaml` | Replace tag trigger with path + dispatch triggers; compute version and SHA |
| `.forgejo/workflows/build-container-nix.yaml` | Same trigger changes; add `-nix` suffix to new tag format |
| `.dagger/src/blumeops_ci/main.py` | Accept SHA parameter; publish with new tag format |
| `mise-tasks/container-build-and-release` | Rename from `container-tag-and-release`; trigger workflow dispatch with current HEAD SHA |
| `mise-tasks/container-list` | Update tag display for new format |
| `docs/how-to/deployment/build-container-image.md` | Document new workflow |
| `mise-tasks/container-build-and-release` | New task replacing `container-tag-and-release`; triggers workflow dispatch |
| `mise-tasks/container-list` | Updated tag display for new format |
| `docs/how-to/deployment/build-container-image.md` | Updated documentation |
## Interaction With Other Prereqs
- **[[enforce-tag-immutability]]** — Commit SHA tags are inherently unique, reducing the scope of immutability enforcement to the `-main` rolling tag (if that is treated as mutable/latest) or eliminating it entirely if `-main` tags are also SHA-qualified (as proposed above)
- **[[enforce-tag-immutability]]** — Commit SHA tags are inherently unique, reducing the scope of immutability enforcement
- **[[wire-ci-registry-auth]]** — Auth changes apply regardless of tagging scheme; no conflict
## Verification
- [ ] Push to main modifying `containers/nettest/` triggers both Docker and Nix builds
- [ ] Resulting image tags match `vX.Y.Z-<sha>` and `vX.Y.Z-<sha>-main` format
- [ ] Nix tags have `-nix` suffix
- [ ] Manual workflow dispatch builds with correct tags (no `-main` suffix)
- [ ] `mise run container-list` shows new tag format
- [ ] Existing deployments referencing old tags still work (images not deleted)
## Related
- [[harden-zot-registry]] — Parent goal

View file

@ -91,7 +91,7 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail
| `pr-comments` | Check unresolved PR comments during review |
| `blumeops-tasks` | Find pending tasks from Todoist |
| `container-list` | View available container images and tags |
| `container-tag-and-release` | Release a new container image version |
| `container-build-and-release` | Trigger container build workflows |
| `dns-preview` | Preview DNS changes before applying |
| `dns-up` | Apply DNS changes via Pulumi |
| `tailnet-preview` | Preview Tailscale ACL changes |