blumeops/.forgejo/workflows/build-container.yaml
Erich Blume c86b5d7772
All checks were successful
Build Container / detect (push) Successful in 3s
Build Container / build-dagger (navidrome) (push) Successful in 22m26s
Native Dagger container builds + Navidrome v0.61.1 (#330)
## Summary
- Move Dagger module from `.dagger/` to repo root (`src/blumeops/`), rename `blumeops-ci` → `blumeops`
- Replace opaque `docker_build()` with native Dagger pipelines that surface full build errors per step
- Migrate navidrome as the first container (`containers/navidrome/container.py`)
- Upgrade navidrome from v0.60.3 to v0.61.1 (major artwork overhaul, SQLite FTS5 search, server-managed transcoding)
- Add `dagger call container-version` for CI version extraction without Dockerfile parsing
- All mise tasks (`container-list`, `container-version-check`, `container-build-and-release`) updated for hybrid mode
- Legacy `docker_build()` fallback preserved for all other containers

## Motivation
When navidrome v0.61.0 added a new Go build tag (`sqlite_fts5`), `docker_build()` showed only "exit code: 1". We had to run `docker build --progress=plain` manually to find `undefined: buildtags.SQLITE_FTS5`. Native Dagger pipelines show the full error inline.

## Container build dispatch needed
After merge, dispatch container build for navidrome:
```
mise run container-build-and-release navidrome --ref 470b4bd
```

## Deploy steps
1. Wait for container build to complete
2. Back up navidrome-data PVC (non-reversible DB migrations)
3. `argocd app set navidrome --revision main && argocd app sync navidrome`
4. Verify at https://dj.ops.eblu.me

## Future
Remaining containers migrate incrementally in follow-up PRs using the same pattern.

Reviewed-on: #330
2026-04-11 17:11:56 -07:00

205 lines
6.7 KiB
YAML

# Unified container build workflow
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
# Detects which containers changed and routes to the correct runner:
# - Dockerfile containers build on k8s (indri) via Dagger
# - Nix containers build on nix-container-builder (ringtail) via nix-build + skopeo
name: Build Container
on:
push:
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:
detect:
runs-on: k8s
outputs:
dagger: ${{ steps.classify.outputs.dagger }}
nix: ${{ steps.classify.outputs.nix }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.sha }}
fetch-depth: 2
- name: Detect and classify changed containers
id: classify
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
CHANGED='["${{ inputs.container }}"]'
else
CHANGED=$(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 "Changed containers: $CHANGED"
# Classify each container by build type (a container can appear in both)
DAGGER='[]'
NIX='[]'
for name in $(echo "$CHANGED" | jq -r '.[]'); do
has_any=false
if [ -f "containers/$name/container.py" ] || [ -f "containers/$name/Dockerfile" ]; then
DAGGER=$(echo "$DAGGER" | jq -c --arg n "$name" '. + [$n]')
has_any=true
fi
if [ -f "containers/$name/default.nix" ]; then
NIX=$(echo "$NIX" | jq -c --arg n "$name" '. + [$n]')
has_any=true
fi
if [ "$has_any" = "false" ]; then
echo "Warning: $name has neither container.py, Dockerfile, nor default.nix — skipping"
fi
done
echo "dagger=$DAGGER" >> "$GITHUB_OUTPUT"
echo "nix=$NIX" >> "$GITHUB_OUTPUT"
echo "Dagger builds: $DAGGER"
echo "Nix builds: $NIX"
build-dagger:
needs: detect
if: needs.detect.outputs.dagger != '[]'
runs-on: k8s
strategy:
matrix:
container: ${{ fromJson(needs.detect.outputs.dagger) }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.sha }}
- name: Extract version and SHA
id: meta
run: |
CONTAINER="${{ matrix.container }}"
# Try native Dagger pipeline (container.py) first, fall back to Dockerfile
if [ -f "containers/$CONTAINER/container.py" ]; then
VERSION=$(dagger call container-version --container-name="$CONTAINER")
elif [ -f "containers/$CONTAINER/Dockerfile" ]; then
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
"containers/$CONTAINER/Dockerfile" \
| sed 's/^ARG CONTAINER_APP_VERSION=//')
fi
if [ -z "$VERSION" ]; then
echo "Error: Could not extract 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: Publish
env:
ZOT_CI_API_KEY: ${{ secrets.ZOT_CI_API_KEY }}
run: |
dagger call publish \
--src=. \
--container-name=${{ matrix.container }} \
--version=${{ steps.meta.outputs.version }} \
--commit-sha=${{ steps.meta.outputs.sha }} \
--registry-password=env:ZOT_CI_API_KEY
build-nix:
needs: detect
if: needs.detect.outputs.nix != '[]'
runs-on: nix-container-builder
strategy:
matrix:
container: ${{ fromJson(needs.detect.outputs.nix) }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.sha }}
- name: Extract version and SHA
id: meta
run: |
CONTAINER="${{ matrix.container }}"
NIX_FILE="containers/$CONTAINER/default.nix"
# Extract version = "..." from the nix file
VERSION=$(grep -m1 '^\s*version\s*=\s*"' "$NIX_FILE" \
| sed 's/.*"\(.*\)".*/\1/' || true)
if [ -z "$VERSION" ]; then
echo "Error: No version declaration found in $NIX_FILE"
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
id: nixpkgs
run: |
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
env:
NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}"
run: |
echo "Building containers/${{ matrix.container }}/default.nix"
echo "NIX_PATH=$NIX_PATH"
nix-build "containers/${{ matrix.container }}/default.nix" -o result
echo "Build complete: $(readlink result)"
- name: Push to registry
env:
ZOT_CI_API_KEY: ${{ secrets.ZOT_CI_API_KEY }}
run: |
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 \
--dest-creds="zot-ci:$ZOT_CI_API_KEY" \
"docker-archive:result" \
"docker://$IMAGE"
echo "Push complete: $IMAGE"