ci: buildx driver, registry layer cache, and a force_build hatch (milestone 326 steps 1-3) #242

Merged
bvandeusen merged 3 commits from dev into main 2026-08-29 22:48:47 -04:00
2 changed files with 72 additions and 3 deletions
Showing only changes of commit e21c9fdd34 - Show all commits
+62 -3
View File
@@ -25,6 +25,26 @@ on:
# Releases still happen (rule 148, on explicit request per rule 2). They
# produce a changelog, not an image.
# The escape hatch for the one thing skip-if-exists makes untestable: a
# build that WOULD be skipped. `agent/` has not changed since 2026-07-17, so
# every push since has correctly declined to build it — which also means the
# agent build path has not run in six weeks and cannot be exercised on
# demand. #3190 lives on exactly that path.
#
# Editing build.yml does not force one either, and that is deliberate: the
# workflow is not shipped bytes, so it is in no artifact's path set. Putting
# it in one would re-version every artifact for a comment change.
#
# ONE input, not one per artifact. Forcing all three is cheap once the
# registry cache is warm (#3114), and three booleans is an interface nobody
# remembers the meaning of.
workflow_dispatch:
inputs:
force_build:
description: 'Rebuild every image even if the published revision matches'
type: boolean
default: false
# Requires repo secret RELEASE_TOKEN — a Forgejo PAT with scopes:
# - write:package, read:package (for docker push to git.fabledsword.com)
# - write:release (for ext-<version> release asset cache)
@@ -498,6 +518,12 @@ jobs:
env:
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator
CHANNEL: ${{ steps.tag.outputs.channel }}
# Empty on a push; the string "true" only from a workflow_dispatch
# that asked for it. `github.event.inputs` rather than the `inputs`
# context — release.yml already uses that form, and it is the one
# this runner is known to evaluate. Read through env rather than
# interpolated into the run block, same rule as release.yml's TAG.
FORCE: ${{ github.event.inputs.force_build }}
run: |
set -eu
DERIVED=$(sh scripts/artifacts.sh revision web)
@@ -537,7 +563,14 @@ jobs:
echo "reuse: NOTE tag is being index-wrapped and reuse is dead."
fi
if [ -n "$PUBLISHED" ] && [ "$PUBLISHED" = "$DERIVED" ]; then
# FORCE is checked here rather than in the build step's `if:`, so
# that one decision drives everything downstream. The repoint step
# keys off `hit` too, and a force that bypassed only the build would
# leave the two disagreeing about what just happened.
if [ "${FORCE:-false}" = "true" ]; then
echo "hit=false" >> "$GITHUB_OUTPUT"
echo "reuse: force_build set — building regardless"
elif [ -n "$PUBLISHED" ] && [ "$PUBLISHED" = "$DERIVED" ]; then
echo "hit=true" >> "$GITHUB_OUTPUT"
echo "reuse: already published — skipping the build"
else
@@ -863,6 +896,12 @@ jobs:
env:
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-ml
CHANNEL: ${{ steps.tag.outputs.channel }}
# Empty on a push; the string "true" only from a workflow_dispatch
# that asked for it. `github.event.inputs` rather than the `inputs`
# context — release.yml already uses that form, and it is the one
# this runner is known to evaluate. Read through env rather than
# interpolated into the run block, same rule as release.yml's TAG.
FORCE: ${{ github.event.inputs.force_build }}
run: |
set -eu
DERIVED=$(sh scripts/artifacts.sh revision ml)
@@ -898,7 +937,14 @@ jobs:
echo "reuse: NOTE tag is being index-wrapped and reuse is dead."
fi
if [ -n "$PUBLISHED" ] && [ "$PUBLISHED" = "$DERIVED" ]; then
# FORCE is checked here rather than in the build step's `if:`, so
# that one decision drives everything downstream. The repoint step
# keys off `hit` too, and a force that bypassed only the build would
# leave the two disagreeing about what just happened.
if [ "${FORCE:-false}" = "true" ]; then
echo "hit=false" >> "$GITHUB_OUTPUT"
echo "reuse: force_build set — building regardless"
elif [ -n "$PUBLISHED" ] && [ "$PUBLISHED" = "$DERIVED" ]; then
echo "hit=true" >> "$GITHUB_OUTPUT"
echo "reuse: already published — skipping the build"
else
@@ -1140,6 +1186,12 @@ jobs:
env:
IMAGE: git.fabledsword.com/bvandeusen/fabledcurator-agent
CHANNEL: ${{ steps.tag.outputs.channel }}
# Empty on a push; the string "true" only from a workflow_dispatch
# that asked for it. `github.event.inputs` rather than the `inputs`
# context — release.yml already uses that form, and it is the one
# this runner is known to evaluate. Read through env rather than
# interpolated into the run block, same rule as release.yml's TAG.
FORCE: ${{ github.event.inputs.force_build }}
run: |
set -eu
DERIVED=$(sh scripts/artifacts.sh revision agent)
@@ -1175,7 +1227,14 @@ jobs:
echo "reuse: NOTE tag is being index-wrapped and reuse is dead."
fi
if [ -n "$PUBLISHED" ] && [ "$PUBLISHED" = "$DERIVED" ]; then
# FORCE is checked here rather than in the build step's `if:`, so
# that one decision drives everything downstream. The repoint step
# keys off `hit` too, and a force that bypassed only the build would
# leave the two disagreeing about what just happened.
if [ "${FORCE:-false}" = "true" ]; then
echo "hit=false" >> "$GITHUB_OUTPUT"
echo "reuse: force_build set — building regardless"
elif [ -n "$PUBLISHED" ] && [ "$PUBLISHED" = "$DERIVED" ]; then
echo "hit=true" >> "$GITHUB_OUTPUT"
echo "reuse: already published — skipping the build"
else
+10
View File
@@ -146,6 +146,16 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
rebuild forever with every lane green. Same failure as #3183, different door.
These jobs run inside a container against a mounted docker socket, so the
buildkit container is a sibling rather than a child.
- **`build.yml` accepts a `workflow_dispatch` with `force_build`**, which
bypasses the reuse check for all three images. It exists because
skip-if-exists makes its own build path untestable: `agent/` has not changed
since 2026-07-17, so the agent build has not run in six weeks and cannot be
exercised on demand — and #3190 lives on exactly that path. Editing
`build.yml` does not force a build either, deliberately: the workflow is not
shipped bytes and is in no artifact's path set. The flag is read through
`github.event.inputs` into an env var rather than interpolated into a run
block, and it is checked inside the reuse step so that one decision drives
both the build and the repoint.
- **`FC_CHANNEL` and `FC_VERSION` are build args, not runtime settings.**
`build.yml` passes them to the web image only — the ml and agent images have
nothing to report them to. `/api/health` returns both, the foot of Settings