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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 thewith:block of two workflow files.The
with:block of a caller is still required, but it shrinks to identifiers that can't move anywhere useful (image-targetfrom 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 --printfor both modes:maindigitaladapt/preauth:developdigitaladapt/preauth:developv1.2.0:latest+:1.2.0:latest+:1.2.0Platforms stay
linux/amd64,linux/arm64, context stays..Dockerfile: one functional change
The final stage gains
AS appso bake can target it. Naming the last stage is a no-op for a plaindocker build— the last stage is still the default target — sodocker build .is unaffected.Three things found while doing this
1.
buildx bake --printdoes not read the Dockerfile. It resolves the bake file and prints tags, but accepts atargetnaming no stage. preauth's final stage was unnamed, sotarget = "app"would have failed at build time in CI — after the push — with--printcheerfully 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 openedprivate/ciPR withscripts/validate-bake.pyto close that gap.2. The
bakebackend sets no layer cache, and theactionbackend it replaces does.docker-publish.yaml's action path setscache-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 viapeclin 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_REQUESTSwas dead in CI. Documented in the Dockerfile since the Symfony 8.1 upgrade and described inreadme.mdandCHANGELOG.md, but no workflow ever passed it — so CI has been silently building with the500default the whole time. It's now an explicit named variable in the bake file, overridable per build. Worth a look at whether500is actually the value you want in production images.Verification
target = "app"↔AS appbuildx bake --print(both modes)phpunitNo daemon on my side, so I could not build the image —
--printis 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-repouses: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 theuses: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 + oneuses:+ the secrets/inputs it passes.image-targetalready comes from a repo variable, so per-repo build differences have somewhere to live that isn't a workflow file.