Adopt commit-based container tags #232
13 changed files with 363 additions and 258 deletions
Adopt commit-based container tags with path-triggered CI
Replace git-tag-triggered container builds with path-based triggers on main and workflow_dispatch. Tags now encode the upstream app version and commit SHA (vX.Y.Z-<sha>) for full traceability. The manual container-tag-and-release task is replaced by container-build-and-release which dispatches workflows via the Forgejo API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
commit
d6152b8238
|
|
@ -18,11 +18,15 @@ class BlumeopsCi:
|
|||
src: dagger.Directory,
|
||||
container_name: str,
|
||||
version: str,
|
||||
commit_sha: str,
|
||||
registry: str = "registry.ops.eblu.me",
|
||||
) -> str:
|
||||
"""Build and push to registry. Returns the image ref."""
|
||||
"""Build and push to registry. Returns the image ref.
|
||||
|
||||
Tag format: {version}-{commit_sha} (e.g. v1.0.0-abc1234)
|
||||
"""
|
||||
ctr = self.build(src, container_name)
|
||||
ref = f"{registry}/blumeops/{container_name}:{version}"
|
||||
ref = f"{registry}/blumeops/{container_name}:{version}-{commit_sha}"
|
||||
return await ctr.publish(ref)
|
||||
|
||||
@function
|
||||
|
|
|
|||
|
|
@ -1,82 +1,138 @@
|
|||
# Nix container build workflow
|
||||
# Triggers on tags matching: <container>-v<version>
|
||||
# Builds from containers/<container>/default.nix if it exists, skips otherwise
|
||||
# Pushes to Zot registry via skopeo with -nix image tag suffix
|
||||
#
|
||||
# Examples:
|
||||
# nettest-v1.0.0 -> builds containers/nettest/default.nix, pushes :v1.0.0-nix
|
||||
# devpi-v2.1.0 -> skips (no default.nix)
|
||||
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
|
||||
# Detects which containers changed, builds from default.nix, and pushes via
|
||||
# skopeo with commit-SHA-based tags: vX.Y.Z-<sha>-nix
|
||||
name: Build Container (Nix)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*-v[0-9]*'
|
||||
branches: [main]
|
||||
paths: ['containers/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
container:
|
||||
description: 'Container name (directory under containers/)'
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: 'Commit SHA to build (defaults to current HEAD)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
detect:
|
||||
runs-on: nix-container-builder
|
||||
outputs:
|
||||
containers: ${{ steps.list.outputs.containers }}
|
||||
steps:
|
||||
- name: Parse tag
|
||||
id: parse
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed containers
|
||||
id: list
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
echo "Tag: $TAG"
|
||||
|
||||
# Extract container name (everything before -v)
|
||||
# e.g., "nettest-v1.0.0" -> "nettest", "my-app-v2.0.0" -> "my-app"
|
||||
CONTAINER="${TAG%-v[0-9]*}"
|
||||
VERSION="${TAG#"${CONTAINER}"-}"
|
||||
|
||||
echo "container=$CONTAINER" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Container: $CONTAINER"
|
||||
echo "Version: $VERSION"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
CONTAINERS='["${{ inputs.container }}"]'
|
||||
else
|
||||
CONTAINERS=$(git diff --name-only HEAD~1 HEAD -- containers/ \
|
||||
| cut -d/ -f2 | sort -u \
|
||||
| jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
fi
|
||||
echo "containers=$CONTAINERS" >> "$GITHUB_OUTPUT"
|
||||
echo "Containers to build: $CONTAINERS"
|
||||
|
||||
build:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.containers != '[]'
|
||||
runs-on: nix-container-builder
|
||||
strategy:
|
||||
matrix:
|
||||
container: ${{ fromJson(needs.detect.outputs.containers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check if nix container exists
|
||||
- name: Check for default.nix
|
||||
id: check
|
||||
run: |
|
||||
CONTAINER="${{ steps.parse.outputs.container }}"
|
||||
CONTEXT="containers/$CONTAINER"
|
||||
|
||||
if [ -f "$CONTEXT/default.nix" ]; then
|
||||
echo "Found $CONTEXT/default.nix"
|
||||
if [ -f "containers/${{ matrix.container }}/default.nix" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No default.nix found at $CONTEXT/default.nix — skipping"
|
||||
echo "No default.nix for ${{ matrix.container }} — skipping"
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract version and SHA
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: meta
|
||||
run: |
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
NIX_FILE="containers/$CONTAINER/default.nix"
|
||||
|
||||
# Try extracting version = "..." from the nix file (e.g. ntfy)
|
||||
VERSION=$(grep -m1 '^\s*version\s*=\s*"' "$NIX_FILE" \
|
||||
| sed 's/.*"\(.*\)".*/\1/' || true)
|
||||
|
||||
# Fall back to CONTAINER_APP_VERSION from Dockerfile (e.g. nettest)
|
||||
if [ -z "$VERSION" ] && [ -f "containers/$CONTAINER/Dockerfile" ]; then
|
||||
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
|
||||
"containers/$CONTAINER/Dockerfile" \
|
||||
| sed 's/^ARG CONTAINER_APP_VERSION=//')
|
||||
fi
|
||||
|
||||
# Last resort: dagger call nix-version for nixpkgs packages (e.g. authentik)
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(dagger call nix-version --package="$CONTAINER")
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not determine version for $CONTAINER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REF="${{ inputs.ref }}"
|
||||
if [ -z "$REF" ]; then
|
||||
REF="${GITHUB_SHA}"
|
||||
fi
|
||||
SHORT_SHA=$(echo "$REF" | head -c 7)
|
||||
|
||||
# Ensure version starts with 'v'
|
||||
case "$VERSION" in
|
||||
v*) ;;
|
||||
*) VERSION="v${VERSION}" ;;
|
||||
esac
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Version: $VERSION, SHA: $SHORT_SHA"
|
||||
|
||||
- name: Resolve nixpkgs
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: nixpkgs
|
||||
run: |
|
||||
# Resolve nixpkgs from the flake registry for <nixpkgs> lookup
|
||||
NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path')
|
||||
echo "Resolved nixpkgs: $NIXPKGS_PATH"
|
||||
echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build with nix
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: build
|
||||
env:
|
||||
NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}"
|
||||
run: |
|
||||
CONTAINER="${{ steps.parse.outputs.container }}"
|
||||
echo "Building containers/$CONTAINER/default.nix"
|
||||
echo "Building containers/${{ matrix.container }}/default.nix"
|
||||
echo "NIX_PATH=$NIX_PATH"
|
||||
nix-build "containers/$CONTAINER/default.nix" -o result
|
||||
nix-build "containers/${{ matrix.container }}/default.nix" -o result
|
||||
echo "Build complete: $(readlink result)"
|
||||
|
||||
- name: Push to registry
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
run: |
|
||||
CONTAINER="${{ steps.parse.outputs.container }}"
|
||||
VERSION="${{ steps.parse.outputs.version }}"
|
||||
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION-nix"
|
||||
CONTAINER="${{ matrix.container }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
SHORT_SHA="${{ steps.meta.outputs.sha }}"
|
||||
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:${VERSION}-${SHORT_SHA}-nix"
|
||||
|
||||
echo "Pushing to $IMAGE"
|
||||
skopeo copy \
|
||||
|
|
|
|||
|
|
@ -1,77 +1,105 @@
|
|||
# Generic container build workflow
|
||||
# Triggers on tags matching: <container>-v<version>
|
||||
# Builds from containers/<container>/Dockerfile if it exists
|
||||
#
|
||||
# Uses Dagger to build and push images to the Zot registry.
|
||||
#
|
||||
# Examples:
|
||||
# nettest-v1.0.0 -> builds containers/nettest/
|
||||
# devpi-v2.1.0 -> builds containers/devpi/
|
||||
# foo-v1.0.0 -> skips if containers/foo/ doesn't exist
|
||||
# Dockerfile container build workflow
|
||||
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
|
||||
# Detects which containers changed, extracts version from CONTAINER_APP_VERSION,
|
||||
# and publishes with commit-SHA-based tags: vX.Y.Z-<sha>
|
||||
name: Build Container
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*-v[0-9]*'
|
||||
branches: [main]
|
||||
paths: ['containers/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
container:
|
||||
description: 'Container name (directory under containers/)'
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: 'Commit SHA to build (defaults to current HEAD)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
detect:
|
||||
runs-on: k8s
|
||||
outputs:
|
||||
containers: ${{ steps.list.outputs.containers }}
|
||||
steps:
|
||||
- name: Parse tag
|
||||
id: parse
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed containers
|
||||
id: list
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
echo "Tag: $TAG"
|
||||
|
||||
# Extract container name (everything before -v)
|
||||
# e.g., "nettest-v1.0.0" -> "nettest", "my-app-v2.0.0" -> "my-app"
|
||||
CONTAINER="${TAG%-v[0-9]*}"
|
||||
VERSION="${TAG#"${CONTAINER}"-}"
|
||||
|
||||
echo "container=$CONTAINER" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Container: $CONTAINER"
|
||||
echo "Version: $VERSION"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
CONTAINERS='["${{ inputs.container }}"]'
|
||||
else
|
||||
# Diff against parent commit to find changed container dirs
|
||||
CONTAINERS=$(git diff --name-only HEAD~1 HEAD -- containers/ \
|
||||
| cut -d/ -f2 | sort -u \
|
||||
| jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
fi
|
||||
echo "containers=$CONTAINERS" >> "$GITHUB_OUTPUT"
|
||||
echo "Containers to build: $CONTAINERS"
|
||||
|
||||
build:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.containers != '[]'
|
||||
runs-on: k8s
|
||||
strategy:
|
||||
matrix:
|
||||
container: ${{ fromJson(needs.detect.outputs.containers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check if container exists
|
||||
- name: Check for Dockerfile
|
||||
id: check
|
||||
run: |
|
||||
CONTAINER="${{ steps.parse.outputs.container }}"
|
||||
CONTEXT="containers/$CONTAINER"
|
||||
|
||||
if [ -f "$CONTEXT/Dockerfile" ]; then
|
||||
echo "Found $CONTEXT/Dockerfile"
|
||||
if [ -f "containers/${{ matrix.container }}/Dockerfile" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No Dockerfile found at $CONTEXT/Dockerfile"
|
||||
echo "No Dockerfile for ${{ matrix.container }} — skipping"
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Skip if container not found
|
||||
if: steps.check.outputs.exists != 'true'
|
||||
- name: Extract version and SHA
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: meta
|
||||
run: |
|
||||
echo "========================================"
|
||||
echo "Container not found: ${{ steps.parse.outputs.container }}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Tag '${{ github.ref_name }}' does not match any container in containers/"
|
||||
echo ""
|
||||
echo "Available containers:"
|
||||
find containers -maxdepth 1 -mindepth 1 -type d -exec basename {} \; 2>/dev/null | sort | while read -r name; do
|
||||
echo " - $name"
|
||||
done || echo " (none)"
|
||||
echo ""
|
||||
echo "Skipping build."
|
||||
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
|
||||
"containers/${{ matrix.container }}/Dockerfile" \
|
||||
| sed 's/^ARG CONTAINER_APP_VERSION=//')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: No CONTAINER_APP_VERSION found in Dockerfile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use dispatch input ref if provided, otherwise current commit
|
||||
REF="${{ inputs.ref }}"
|
||||
if [ -z "$REF" ]; then
|
||||
REF="${GITHUB_SHA}"
|
||||
fi
|
||||
SHORT_SHA=$(echo "$REF" | head -c 7)
|
||||
|
||||
# Ensure version starts with 'v'
|
||||
case "$VERSION" in
|
||||
v*) ;; # already has v prefix
|
||||
*) VERSION="v${VERSION}" ;;
|
||||
esac
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Version: $VERSION, SHA: $SHORT_SHA"
|
||||
|
||||
- name: Publish
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
run: |
|
||||
dagger call publish \
|
||||
--src=. \
|
||||
--container-name=${{ steps.parse.outputs.container }} \
|
||||
--version=${{ steps.parse.outputs.version }}
|
||||
--container-name=${{ matrix.container }} \
|
||||
--version=${{ steps.meta.outputs.version }} \
|
||||
--commit-sha=${{ steps.meta.outputs.sha }}
|
||||
|
|
|
|||
1
docs/changelog.d/harden-zot-registry.feature.md
Normal file
1
docs/changelog.d/harden-zot-registry.feature.md
Normal 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`.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]].
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
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