build: define the Docker build in docker-bake.hcl #17

Merged
andrew merged 1 commits from chore/docker-bake into main 2026-09-23 17:30:05 -04:00
Member

Moves the Docker build definition into docker-bake.hcl, so what CI builds is version-controlled with the code and reviewable in a diff, instead of living in the with: block of two workflow files.

.gitea/workflows/develop.yaml   build-push-action + inline tags ─┐
.gitea/workflows/docker.yaml    build-push-action + inline tags ─┤
                                                                 ↓
                                        docker-bake.hcl  — one target, one place

The with: block of a caller is still required, but it shrinks to identifiers that can't move anywhere useful (image-target from a repo variable, mode, build-backend). The interesting parts — Dockerfile target, platforms, tags, build args — are now in the bake file.

Tags are byte-identical to today

Verified with buildx bake --print for both modes:

Trigger Before (workflow) After (bake)
push to main digitaladapt/preauth:develop digitaladapt/preauth:develop
tag v1.2.0 :latest + :1.2.0 :latest + :1.2.0

Platforms stay linux/amd64,linux/arm64, context stays ..

Dockerfile: one functional change

The final stage gains AS app so bake can target it. Naming the last stage is a no-op for a plain docker build — the last stage is still the default target — so docker build . is unaffected.

Three things found while doing this

1. buildx bake --print does not read the Dockerfile. It resolves the bake file and prints tags, but accepts a target naming no stage. preauth's final stage was unnamed, so target = "app" would have failed at build time in CI — after the push — with --print cheerfully reporting success. That's the trap in this change, and it's why the Dockerfile is touched at all. There's no existing check for it; I've opened private/ci PR with scripts/validate-bake.py to close that gap.

2. The bake backend sets no layer cache, and the action backend it replaces does. docker-publish.yaml's action path sets cache-from/cache-to: type=gha; its bake path sets neither, and neither existing bake user in the portfolio (context-shuttle, task-weaver) sets one either — so every bake build in the portfolio has been running cold. preauth compiles APCu from source via pecl in both stages, so switching backends without noticing would have been a real regression rather than a wash. The bake file now declares the gha cache itself, with the local-build override documented (there's no GHA cache service outside CI).

3. ARG MAX_REQUESTS was dead in CI. Documented in the Dockerfile since the Symfony 8.1 upgrade and described in readme.md and CHANGELOG.md, but no workflow ever passed it — so CI has been silently building with the 500 default the whole time. It's now an explicit named variable in the bake file, overridable per build. Worth a look at whether 500 is actually the value you want in production images.

Verification

Check Result
Dockerfile stage/target contract ✓ target = "app" ↔ AS app
buildx bake --print (both modes) ✓ parses, resolves, tags correct
Negative control (stage renamed) ✓ fails as it should
phpunit 313 tests pass
conformance 31/34 with the staged callers

No daemon on my side, so I could not build the image — --print is parse-and-resolve only. The first real proof will be the first CI run after the callers land.

What's deliberately not here

The two caller files that select build-backend: 'bake'. .gitea/workflows/ is protected by a pre-receive hook that only admits changes via a trusted ref — confirmed by a real rejected push, not assumed. They're staged in the working tree, 149 lines total, and they need to land together with the other staged callers from the previous PR (tests.yaml, publish.yaml) — and only after the cross-repo uses: question is settled, since that mechanism has never actually fired on this instance.

Note on the direction you suggested

You mentioned pushing the differing bits into variables so the workflow files never change. The bake file gets you most of the way for the build: everything build-related is now in one reviewable place. What can't move is the trigger (on:) and the uses: identifier — a reusable workflow can't choose its own trigger, and the caller has to name the workflow it calls. So the floor for a caller is roughly: on: block + one uses: + the secrets/inputs it passes. image-target already comes from a repo variable, so per-repo build differences have somewhere to live that isn't a workflow file.

Moves the Docker build definition into `docker-bake.hcl`, so what CI builds is version-controlled with the code and reviewable in a diff, instead of living in the `with:` block of two workflow files. ``` .gitea/workflows/develop.yaml build-push-action + inline tags ─┐ .gitea/workflows/docker.yaml build-push-action + inline tags ─┤ ↓ docker-bake.hcl — one target, one place ``` The `with:` block of a caller is still required, but it shrinks to identifiers that can't move anywhere useful (`image-target` from a repo variable, `mode`, `build-backend`). The interesting parts — Dockerfile target, platforms, tags, build args — are now in the bake file. ## Tags are byte-identical to today Verified with `buildx bake --print` for both modes: | Trigger | Before (workflow) | After (bake) | |---|---|---| | push to `main` | `digitaladapt/preauth:develop` | `digitaladapt/preauth:develop` | | tag `v1.2.0` | `:latest` + `:1.2.0` | `:latest` + `:1.2.0` | Platforms stay `linux/amd64,linux/arm64`, context stays `.`. ## Dockerfile: one functional change The final stage gains `AS app` so bake can target it. **Naming the last stage is a no-op for a plain `docker build`** — the last stage is still the default target — so `docker build .` is unaffected. ## Three things found while doing this **1. `buildx bake --print` does not read the Dockerfile.** It resolves the bake file and prints tags, but accepts a `target` naming no stage. preauth's final stage was unnamed, so `target = "app"` would have failed at build time in CI — *after* the push — with `--print` cheerfully reporting success. That's the trap in this change, and it's why the Dockerfile is touched at all. There's no existing check for it; I've opened `private/ci` PR with `scripts/validate-bake.py` to close that gap. **2. The `bake` backend sets no layer cache, and the `action` backend it replaces does.** `docker-publish.yaml`'s action path sets `cache-from`/`cache-to: type=gha`; its bake path sets neither, and neither existing bake user in the portfolio (`context-shuttle`, `task-weaver`) sets one either — so **every bake build in the portfolio has been running cold.** preauth compiles APCu from source via `pecl` in *both* stages, so switching backends without noticing would have been a real regression rather than a wash. The bake file now declares the gha cache itself, with the local-build override documented (there's no GHA cache service outside CI). **3. `ARG MAX_REQUESTS` was dead in CI.** Documented in the Dockerfile since the Symfony 8.1 upgrade and described in `readme.md` and `CHANGELOG.md`, but no workflow ever passed it — so CI has been silently building with the `500` default the whole time. It's now an explicit named variable in the bake file, overridable per build. Worth a look at whether `500` is actually the value you want in production images. ## Verification | Check | Result | |---|---| | Dockerfile stage/target contract | ✓ `target = "app"` ↔ `AS app` | | `buildx bake --print` (both modes) | ✓ parses, resolves, tags correct | | Negative control (stage renamed) | ✓ fails as it should | | `phpunit` | **313 tests pass** | | conformance | 31/34 with the staged callers | No daemon on my side, so I could not build the image — `--print` is parse-and-resolve only. **The first real proof will be the first CI run after the callers land.** ## What's deliberately not here The two caller files that select `build-backend: 'bake'`. `.gitea/workflows/` is protected by a pre-receive hook that only admits changes via a trusted ref — confirmed by a real rejected push, not assumed. They're staged in the working tree, 149 lines total, and they need to land together with the other staged callers from the previous PR (`tests.yaml`, `publish.yaml`) — and only after the cross-repo `uses:` question is settled, since that mechanism has never actually fired on this instance. ## Note on the direction you suggested You mentioned pushing the differing bits into variables so the workflow files never change. The bake file gets you most of the way for the *build*: everything build-related is now in one reviewable place. What can't move is the trigger (`on:`) and the `uses:` identifier — a reusable workflow can't choose its own trigger, and the caller has to name the workflow it calls. So the floor for a caller is roughly: `on:` block + one `uses:` + the secrets/inputs it passes. `image-target` already comes from a repo variable, so per-repo build differences have somewhere to live that isn't a workflow file.
lyra added 1 commit 2026-09-23 17:02:11 -04:00
build: define the Docker build in docker-bake.hcl
Tests / test (pull_request) Successful in 1m15s
abe94c6238
The build was described in two places kept in step by hand: the Dockerfile, and
the `with:` block of each docker workflow file. This moves the parts CI actually
decides — Dockerfile target, platforms, tags, build args — into docker-bake.hcl,
so the build is version-controlled with the code and reviewable in a diff.

Tags are byte-identical to the current workflows, verified with
`buildx bake --print`:

  main push   -> digitaladapt/preauth:develop
  tag v1.2.0  -> digitaladapt/preauth:latest AND digitaladapt/preauth:1.2.0

Dockerfile: the final stage gains `AS app` so bake can target it. Naming the
last stage is a no-op for a plain `docker build` — it is still the default
target — so `docker build .` behaves exactly as before.

THREE THINGS FOUND WHILE DOING THIS
-----------------------------------

1. `buildx bake --print` does NOT read the Dockerfile, so it accepts a `target`
   naming no stage. preauth's final stage was unnamed, so `target = "app"` would
   have failed at build time in CI *after the push*, with --print reporting
   success. Fixed by naming the stage.

2. The `bake` backend in docker-publish.yaml sets no layer cache, while the
   `action` backend it replaces sets cache-from/cache-to: type=gha. Neither
   existing bake user in the portfolio sets one either, so every bake build in
   the portfolio runs cold. preauth compiles APCu from source (pecl) in both
   stages, so this would have been a real regression. The bake file now sets the
   gha cache, with the override for local builds documented.

3. `ARG MAX_REQUESTS` has been documented in the Dockerfile since the Symfony
   8.1 upgrade, but no workflow ever passed it, so CI silently built with the
   500 default. It is now an explicit named variable, overridable per build.

The workflow callers that select the bake backend are NOT in this commit:
.gitea/workflows/ is protected by a pre-receive hook and only changes via a
trusted ref. They are staged in the working tree.

Verified: Dockerfile stage/target contract holds; bake file parses and resolves
under buildx 0.37; 313 tests pass; conformance 31/34 with callers staged.
andrew approved these changes 2026-09-23 17:29:42 -04:00
andrew merged commit 2b61a43f60 into main 2026-09-23 17:30:05 -04:00
andrew deleted branch chore/docker-bake 2026-09-23 17:30:05 -04:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: public/preauth#17