diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 18a45bb..6864b08 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -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- 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 diff --git a/ci-requirements.md b/ci-requirements.md index 643cde6..3fd321c 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -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