From 7b39cbf38748faf6c9d60475da68909958539fbb Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 16 Jul 2026 21:51:23 +0200 Subject: [PATCH 1/2] docs(ci-testing): on-demand image tag race + explicit version bake A /retro session (2026-07-16) found a :profiling- image built on both main-push and tag-push; last-writer-wins made the baked git-ref nondeterministic (a release read ref=main). Separately, InstalledVersions::getPrettyVersion in a .git-less Docker build returned 1.0.0+no-version-set. Add Pattern 6: build the on-demand variant on tags/dispatch only, and bake the version explicitly (COMPOSER_ROOT_VERSION or a baked APP_BUILD_REF). Signed-off-by: Sebastian Mendel --- skills/docker-development/references/ci-testing.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/skills/docker-development/references/ci-testing.md b/skills/docker-development/references/ci-testing.md index 9074067..3b60dcd 100644 --- a/skills/docker-development/references/ci-testing.md +++ b/skills/docker-development/references/ci-testing.md @@ -149,6 +149,20 @@ docker buildx bake --print docker buildx bake -f docker-bake.hcl -f /tmp/metadata-bake.json --print ``` +## Pattern 6: On-demand image tags — build on ONE trigger, and bake the version explicitly + +A prod-like *variant* image (a profiler build, a debug build) is often published under a content-addressed tag like `:profiling-` so operators can switch to it on demand. Two traps appear when that image also surfaces its own build provenance (commit, ref, version) on a status page. + +**Trap A — the tag race.** If the variant builds on *both* `push: main` and `push: tags`, both runs write the SAME `:profiling-` tag (same commit → same sha), and last-writer-wins decides which run's baked git-ref survives. A release deploy can then read `ref=main` instead of `ref=v1.2.3`. Fix: build the on-demand variant on ONE trigger that carries the right provenance — tags (plus manual dispatch), not `main`: + +```yaml +- name: Build and push profiling image + # Tag/dispatch only: a main-push build would race the tag build for :profiling- + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' +``` + +**Trap B — no version in a `.git`-less build.** A Docker build has no `.git`, so anything that derives the version from git or the package's own metadata reads a placeholder — e.g. Composer's `InstalledVersions::getPrettyVersion()` returns `1.0.0+no-version-set`. Bake the version in explicitly: pass a build arg before the dependency install (`COMPOSER_ROOT_VERSION=1.2.3`, or the language's equivalent) so the metadata records it, or have the app read a baked env (`APP_BUILD_REF`) that the workflow sets from `github.ref_name`. With Trap A fixed, that ref is deterministically the release tag. + ## Local boot-test pitfalls When smoke/boot-testing an image by hand (not in the CI matrix): From 0156d4d805d2694aacf7698818416ce8e1a68e2c Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Thu, 16 Jul 2026 22:25:59 +0200 Subject: [PATCH 2/2] docs(ci-testing): show the ARG/ENV mechanism for baking APP_BUILD_REF Gemini review on #46: make Trap B's baked-env solution concrete with the Dockerfile ARG/ENV lines instead of describing it abstractly. Signed-off-by: Sebastian Mendel --- skills/docker-development/references/ci-testing.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skills/docker-development/references/ci-testing.md b/skills/docker-development/references/ci-testing.md index 3b60dcd..d6d7b19 100644 --- a/skills/docker-development/references/ci-testing.md +++ b/skills/docker-development/references/ci-testing.md @@ -161,7 +161,14 @@ A prod-like *variant* image (a profiler build, a debug build) is often published if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' ``` -**Trap B — no version in a `.git`-less build.** A Docker build has no `.git`, so anything that derives the version from git or the package's own metadata reads a placeholder — e.g. Composer's `InstalledVersions::getPrettyVersion()` returns `1.0.0+no-version-set`. Bake the version in explicitly: pass a build arg before the dependency install (`COMPOSER_ROOT_VERSION=1.2.3`, or the language's equivalent) so the metadata records it, or have the app read a baked env (`APP_BUILD_REF`) that the workflow sets from `github.ref_name`. With Trap A fixed, that ref is deterministically the release tag. +**Trap B — no version in a `.git`-less build.** A Docker build has no `.git`, so anything that derives the version from git or the package's own metadata reads a placeholder — e.g. Composer's `InstalledVersions::getPrettyVersion()` returns `1.0.0+no-version-set`. Bake the version in explicitly: pass a build arg before the dependency install (`COMPOSER_ROOT_VERSION=1.2.3`, or the language's equivalent) so the metadata records it, or have the app read a baked env (`APP_BUILD_REF`) that the Dockerfile declares and the workflow sets from `github.ref_name`: + +```dockerfile +ARG APP_BUILD_REF +ENV APP_BUILD_REF=$APP_BUILD_REF +``` + +With Trap A fixed, that ref is deterministically the release tag. ## Local boot-test pitfalls