script/cibuild was a bare docker build ., and the Dockerfile did COPY . . then RUN make check. COPY is keyed on content, so on an
unchanged tree Docker served the check layer from cache: the checks never
executed, no Hugo or prettier output appeared, and the build still exited 0.
Baseline, on main before this change
Reproduced here first, so the fix is measured against a demonstrated defect
rather than an assumed one:
#8 [4/6] RUN script/bootstrap
#8 CACHED
#9 [5/6] COPY . .
#9 CACHED
#10 [6/6] RUN make check
#10 CACHED
exit=0. No Hugo output, no prettier line.
The change
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
immediately below COPY . ., with both script/cibuild and script/docker
generating and passing the value identically:
No default value. A default is a constant, and a constant is a stable
cache key — the defect unchanged.
Placed below COPY . .. Everything above keeps caching, so the script/bootstrap layer, which compiles Hugo from source, is not rebuilt.
Whole-build --no-cache would have discarded it for no benefit.
The guard. An unset ARG is the empty string, which is also a stable
cache key.
The value expanded into the RUN. Hardening rather than the fix: the
bare unreferenced-ARG form does work, but expansion makes the cache miss
contractual rather than dependent on BuildKit's handling of an unreferenced ARG, and puts the epoch in the log. The guard also references the value, so
there are two independent invalidation points, not one.
epoch= on its own line, never inlined into the argument list. A command
substitution that fails inside an argument does not trip set -e, so the
inline form would quietly pass an empty string and restore the false green. %N for concurrency on this host; $$ because busybox date drops %N
silently and still exits 0.
ARG is stage-scoped and must be redeclared in every stage that runs checks.
This image is single-stage, so one declaration is complete.
Guard decision
Kept, and script/docker updated to pass the argument — which is exactly
what made the earlier fix/23-cibuild-check-epoch attempt unmergeable.
The reason is measurement, not taste. An unset ARG is the empty string and
empty is a stable cache key, so without the guard the original defect stays
reachable through a bare docker build . — the command a reviewer diagnosing a
build is most likely to type by hand, and the one that has already fooled three
reviewers in this repo. Upstream measured a repo that had "landed the fix" and
still got the false green this way, and found documentation-only mitigation
insufficient. The cost is one duplicated pair of lines in script/docker.
Consequence, accepted deliberately: a bare docker build . now fails. That
is the point.
Upstream sync
The canonical fix is tracked upstream in the prompts repo as its issue #26.
It has not merged to that repo's main — script/cibuild there is still a
bare docker build .. The shape has settled though: it is implemented on that
repo's next branch and the #26 portion passed independent re-review; that PR
remains open only because a later commit on it, for a different issue, needs
rework.
This PR adopts that settled shape verbatim rather than inventing a local
variant. It may need re-syncing once upstream merges. Propagation to
consuming repos is tracked upstream separately.
The upstream change also sweeps prose in REPO_POLICIES.md, NEW_REPO_CHECKLIST.md, EXISTING_REPO_CHECKLIST.md and CODE_STYLEGUIDE_GO.md. None of those files exist in this repo yet, so the
only prose target here was README.md, whose Entrypoints section claimed script/cibuild is docker build .. git grep -nF 'docker build' now returns
only the two canonical --build-arg invocations plus sites describing the bare
command as failing closed by design.
Verification
All runs are on this branch's tree, from this worktree. No docker builder prune in any form, and no whole-build --no-cache. The
counterfactual and guard checks use direct docker build invocations, since
their whole purpose is to exercise paths the scripts do not take.
1. Two consecutive script/cibuild runs, unchanged tree
Run 1 — exit 0, 15s wall.
#7 [2/7] WORKDIR /src
#7 CACHED
#8 [3/7] COPY script/ script/
#8 CACHED
#9 [4/7] RUN script/bootstrap
#9 CACHED
#10 [5/7] COPY . .
#10 DONE 2.6s
#11 [6/7] RUN [ -n "17862905134173327183464612" ] || exit 1
#11 DONE 5.4s
#12 [7/7] RUN echo "check epoch: 17862905134173327183464612" && make check
#12 0.219 check epoch: 17862905134173327183464612
#12 0.276 Start building sites …
#12 0.276 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.299
#12 0.300 │ EN
#12 0.300 ──────────────────┼────
#12 0.300 Pages │ 3
#12 0.300 Static files │ 1
#12 0.300
#12 0.300 Total in 28 ms
#12 0.381 Start building sites …
#12 0.381 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.399
#12 0.399 │ EN
#12 0.399 ──────────────────┼────
#12 0.399 Pages │ 3
#12 0.399 Static files │ 1
#12 0.399
#12 0.399 Total in 19 ms
#12 1.950 Checking formatting...
#12 2.111 All matched files use Prettier code style!
#12 DONE 5.0s
Run 2 — exit 0, 6s wall. Nothing touched in between.
#7 [2/7] WORKDIR /src
#7 CACHED
#8 [4/7] RUN script/bootstrap
#8 CACHED
#9 [3/7] COPY script/ script/
#9 CACHED
#10 [5/7] COPY . .
#10 CACHED
#11 [6/7] RUN [ -n "17862905310211352833469503" ] || exit 1
#11 DONE 0.2s
#12 [7/7] RUN echo "check epoch: 17862905310211352833469503" && make check
#12 0.192 check epoch: 17862905310211352833469503
#12 0.229 Start building sites …
#12 0.229 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.243
#12 0.244 │ EN
#12 0.244 ──────────────────┼────
#12 0.244 Pages │ 3
#12 0.244 Static files │ 1
#12 0.244
#12 0.244 Total in 16 ms
#12 0.288 Start building sites …
#12 0.288 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.303
#12 0.303 │ EN
#12 0.303 ──────────────────┼────
#12 0.303 Pages │ 3
#12 0.303 Static files │ 1
#12 0.303
#12 0.303 Total in 15 ms
#12 1.377 Checking formatting...
#12 1.503 All matched files use Prettier code style!
#12 DONE 2.2s
Both runs show two Hugo builds and the prettier line. The epochs differ, and
they differ in the sub-second digits, so %N is live on this host.
2. Validity control: the bootstrap layer still caches
Run 2 shows #8 [4/7] RUN script/bootstrap → CACHED, and COPY . .
→ CACHED as well. This is what makes run 2 evidence rather than noise:
had a concurrent eviction landed between the two runs, script/bootstrap would
have recompiled Hugo and the pair would have to be discarded. It also rules out
an accidental whole-build --no-cache.
Note the reading order: COPY . . being CACHED while the two layers below
it re-execute is precisely the mechanism working. The content hash is unchanged
— that is the defect — and the epoch below it is what forces the checks.
Wall clock: 15s then 6s. Corroborating only; the layer log is the
evidence.
3. Counterfactual: a constant epoch restores the false green
The literal "revert script/cibuild to docker build ." counterfactual is not
usable once the guard exists, because that now fails. Passing a constant
instead isolates the varying value as the active ingredient:
Every layer CACHED, exit 0, no check output — the original defect, reproduced
on the fixed Dockerfile. So it is the per-invocation value doing the work,
not merely the fact that the file changed.
4. The guard fails a bare docker build ., and keeps failing
$ docker build . # exit 1
$ docker build . # exit 1 (immediate repeat)
41 | ARG CHECK_EPOCH
42 | >>> RUN [ -n "$CHECK_EPOCH" ] || exit 1
ERROR: failed to build: failed to solve: process "/bin/sh -c [ -n \"$CHECK_EPOCH\" ] || exit 1" did not complete successfully: exit code: 1
Failed steps are never cached, so this is a hard failure on every invocation,
not just the first. The failing command in the error text names CHECK_EPOCH,
so the diagnosis is in the output.
5. Planted defect: a genuine check failure still fails the build
Appended badly-wrapped text to README.md so prettier fails, then ran script/cibuild:
2.324 Checking formatting...
2.458 [warn] README.md
2.502 [warn] Code style issues found in the above file. Run Prettier with --write to fix.
2.574 make: *** [Makefile:22: check] Error 1
------
Dockerfile:45
45 | >>> RUN echo "check epoch: ${CHECK_EPOCH}" && make check
ERROR: failed to build: failed to solve: process ... did not complete successfully: exit code: 2
script/cibuild exited 1. Reverted; the tree is clean.
6. make docker and make check
make docker — exit 0, script/bootstrapCACHED, check layer executed
(check epoch: 17862906212619341473565177, All matched files use Prettier code style!), naming to docker.io/library/lora.vegas:latest done. So the
guard did not break the local entrypoint.
make check on the host — green.
Not verified
Behaviour under Gitea Actions. Not run; .gitea/workflows/check.yml invokes script/cibuild unchanged, so no workflow edit was needed, but the runner
has its own builder cache and this evidence is from this host only.
The re-verification required after the .dockerignore change lands. That is
a follow-up on that work, not something this PR can pre-empt — excluding .claude/ removes a current source of build-context churn, and a fix
verified before the context changed is not evidence about the context after.
No change was made to what make check runs (out of scope), and .dockerignore is untouched.
Closes https://git.eeqj.de/sneak/lora.vegas/issues/23.
`script/cibuild` was a bare `docker build .`, and the `Dockerfile` did
`COPY . .` then `RUN make check`. `COPY` is keyed on content, so on an
unchanged tree Docker served the check layer from cache: the checks never
executed, no Hugo or prettier output appeared, and the build still exited 0.
## Baseline, on `main` before this change
Reproduced here first, so the fix is measured against a demonstrated defect
rather than an assumed one:
```
#8 [4/6] RUN script/bootstrap
#8 CACHED
#9 [5/6] COPY . .
#9 CACHED
#10 [6/6] RUN make check
#10 CACHED
```
`exit=0`. No Hugo output, no prettier line.
## The change
```
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
```
immediately below `COPY . .`, with both `script/cibuild` and `script/docker`
generating and passing the value identically:
```
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" .
```
Each element is load-bearing:
- **No default value.** A default is a constant, and a constant is a stable
cache key — the defect unchanged.
- **Placed below `COPY . .`.** Everything above keeps caching, so the
`script/bootstrap` layer, which compiles Hugo from source, is not rebuilt.
Whole-build `--no-cache` would have discarded it for no benefit.
- **The guard.** An unset `ARG` is the empty string, which is also a stable
cache key.
- **The value expanded into the `RUN`.** Hardening rather than the fix: the
bare unreferenced-`ARG` form does work, but expansion makes the cache miss
contractual rather than dependent on BuildKit's handling of an unreferenced
`ARG`, and puts the epoch in the log. The guard also references the value, so
there are two independent invalidation points, not one.
- **`epoch=` on its own line**, never inlined into the argument list. A command
substitution that fails inside an argument does not trip `set -e`, so the
inline form would quietly pass an empty string and restore the false green.
`%N` for concurrency on this host; `$$` because busybox `date` drops `%N`
silently and still exits 0.
`ARG` is stage-scoped and must be redeclared in every stage that runs checks.
This image is single-stage, so one declaration is complete.
## Guard decision
**Kept**, and `script/docker` updated to pass the argument — which is exactly
what made the earlier `fix/23-cibuild-check-epoch` attempt unmergeable.
The reason is measurement, not taste. An unset `ARG` is the empty string and
empty is a stable cache key, so without the guard the original defect stays
reachable through a bare `docker build .` — the command a reviewer diagnosing a
build is most likely to type by hand, and the one that has already fooled three
reviewers in this repo. Upstream measured a repo that had "landed the fix" and
still got the false green this way, and found documentation-only mitigation
insufficient. The cost is one duplicated pair of lines in `script/docker`.
Consequence, accepted deliberately: **a bare `docker build .` now fails.** That
is the point.
## Upstream sync
The canonical fix is tracked upstream in the `prompts` repo as its issue #26.
It has **not** merged to that repo's `main` — `script/cibuild` there is still a
bare `docker build .`. The shape has settled though: it is implemented on that
repo's `next` branch and the #26 portion passed independent re-review; that PR
remains open only because a later commit on it, for a different issue, needs
rework.
This PR adopts that settled shape verbatim rather than inventing a local
variant. **It may need re-syncing once upstream merges.** Propagation to
consuming repos is tracked upstream separately.
The upstream change also sweeps prose in `REPO_POLICIES.md`,
`NEW_REPO_CHECKLIST.md`, `EXISTING_REPO_CHECKLIST.md` and
`CODE_STYLEGUIDE_GO.md`. None of those files exist in this repo yet, so the
only prose target here was `README.md`, whose Entrypoints section claimed
`script/cibuild` is `docker build .`. `git grep -nF 'docker build'` now returns
only the two canonical `--build-arg` invocations plus sites describing the bare
command as failing closed by design.
---
# Verification
All runs are on this branch's tree, from this worktree. No
`docker builder prune` in any form, and no whole-build `--no-cache`. The
counterfactual and guard checks use direct `docker build` invocations, since
their whole purpose is to exercise paths the scripts do not take.
## 1. Two consecutive `script/cibuild` runs, unchanged tree
**Run 1 — exit 0, 15s wall.**
```
#7 [2/7] WORKDIR /src
#7 CACHED
#8 [3/7] COPY script/ script/
#8 CACHED
#9 [4/7] RUN script/bootstrap
#9 CACHED
#10 [5/7] COPY . .
#10 DONE 2.6s
#11 [6/7] RUN [ -n "17862905134173327183464612" ] || exit 1
#11 DONE 5.4s
#12 [7/7] RUN echo "check epoch: 17862905134173327183464612" && make check
#12 0.219 check epoch: 17862905134173327183464612
#12 0.276 Start building sites …
#12 0.276 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.299
#12 0.300 │ EN
#12 0.300 ──────────────────┼────
#12 0.300 Pages │ 3
#12 0.300 Static files │ 1
#12 0.300
#12 0.300 Total in 28 ms
#12 0.381 Start building sites …
#12 0.381 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.399
#12 0.399 │ EN
#12 0.399 ──────────────────┼────
#12 0.399 Pages │ 3
#12 0.399 Static files │ 1
#12 0.399
#12 0.399 Total in 19 ms
#12 1.950 Checking formatting...
#12 2.111 All matched files use Prettier code style!
#12 DONE 5.0s
```
**Run 2 — exit 0, 6s wall. Nothing touched in between.**
```
#7 [2/7] WORKDIR /src
#7 CACHED
#8 [4/7] RUN script/bootstrap
#8 CACHED
#9 [3/7] COPY script/ script/
#9 CACHED
#10 [5/7] COPY . .
#10 CACHED
#11 [6/7] RUN [ -n "17862905310211352833469503" ] || exit 1
#11 DONE 0.2s
#12 [7/7] RUN echo "check epoch: 17862905310211352833469503" && make check
#12 0.192 check epoch: 17862905310211352833469503
#12 0.229 Start building sites …
#12 0.229 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.243
#12 0.244 │ EN
#12 0.244 ──────────────────┼────
#12 0.244 Pages │ 3
#12 0.244 Static files │ 1
#12 0.244
#12 0.244 Total in 16 ms
#12 0.288 Start building sites …
#12 0.288 hugo v0.164.0 linux/amd64 BuildDate=unknown
#12 0.303
#12 0.303 │ EN
#12 0.303 ──────────────────┼────
#12 0.303 Pages │ 3
#12 0.303 Static files │ 1
#12 0.303
#12 0.303 Total in 15 ms
#12 1.377 Checking formatting...
#12 1.503 All matched files use Prettier code style!
#12 DONE 2.2s
```
Both runs show two Hugo builds and the prettier line. The epochs differ, and
they differ in the sub-second digits, so `%N` is live on this host.
## 2. Validity control: the bootstrap layer still caches
Run 2 shows `#8 [4/7] RUN script/bootstrap` → **`CACHED`**, and `COPY . .`
→ **`CACHED`** as well. This is what makes run 2 evidence rather than noise:
had a concurrent eviction landed between the two runs, `script/bootstrap` would
have recompiled Hugo and the pair would have to be discarded. It also rules out
an accidental whole-build `--no-cache`.
Note the reading order: `COPY . .` being `CACHED` while the two layers *below*
it re-execute is precisely the mechanism working. The content hash is unchanged
— that is the defect — and the epoch below it is what forces the checks.
Wall clock: **15s** then **6s**. Corroborating only; the layer log is the
evidence.
## 3. Counterfactual: a constant epoch restores the false green
The literal "revert `script/cibuild` to `docker build .`" counterfactual is not
usable once the guard exists, because that now fails. Passing a *constant*
instead isolates the varying value as the active ingredient:
```
$ docker build --build-arg CHECK_EPOCH=constant-a9349c . # exit 0, checks ran
$ docker build --build-arg CHECK_EPOCH=constant-a9349c . # exit 0
#8 [6/7] RUN [ -n "constant-a9349c" ] || exit 1
#8 CACHED
...
#12 [7/7] RUN echo "check epoch: constant-a9349c" && make check
#12 CACHED
```
Every layer `CACHED`, exit 0, no check output — the original defect, reproduced
on the fixed `Dockerfile`. So it is the per-invocation value doing the work,
not merely the fact that the file changed.
## 4. The guard fails a bare `docker build .`, and keeps failing
```
$ docker build . # exit 1
$ docker build . # exit 1 (immediate repeat)
41 | ARG CHECK_EPOCH
42 | >>> RUN [ -n "$CHECK_EPOCH" ] || exit 1
ERROR: failed to build: failed to solve: process "/bin/sh -c [ -n \"$CHECK_EPOCH\" ] || exit 1" did not complete successfully: exit code: 1
```
Failed steps are never cached, so this is a hard failure on every invocation,
not just the first. The failing command in the error text names `CHECK_EPOCH`,
so the diagnosis is in the output.
## 5. Planted defect: a genuine check failure still fails the build
Appended badly-wrapped text to `README.md` so prettier fails, then ran
`script/cibuild`:
```
2.324 Checking formatting...
2.458 [warn] README.md
2.502 [warn] Code style issues found in the above file. Run Prettier with --write to fix.
2.574 make: *** [Makefile:22: check] Error 1
------
Dockerfile:45
45 | >>> RUN echo "check epoch: ${CHECK_EPOCH}" && make check
ERROR: failed to build: failed to solve: process ... did not complete successfully: exit code: 2
```
`script/cibuild` exited **1**. Reverted; the tree is clean.
## 6. `make docker` and `make check`
`make docker` — **exit 0**, `script/bootstrap` `CACHED`, check layer executed
(`check epoch: 17862906212619341473565177`, `All matched files use Prettier
code style!`), `naming to docker.io/library/lora.vegas:latest done`. So the
guard did not break the local entrypoint.
`make check` on the host — **green**.
## Not verified
- Behaviour under Gitea Actions. Not run; `.gitea/workflows/check.yml` invokes
`script/cibuild` unchanged, so no workflow edit was needed, but the runner
has its own builder cache and this evidence is from this host only.
- The re-verification required after the `.dockerignore` change lands. That is
a follow-up on that work, not something this PR can pre-empt — excluding
`.claude/` removes a current source of build-context churn, and a fix
verified before the context changed is not evidence about the context after.
- No change was made to what `make check` runs (out of scope), and
`.dockerignore` is untouched.
script/cibuild was a bare `docker build .`, and the Dockerfile did
`COPY . .` then `RUN make check`. COPY is keyed on content, so on an
unchanged tree Docker served the check layer from cache: the checks
never executed, no Hugo or prettier output appeared, and the build still
exited 0. A gate that reports success without running is worse than no
gate, because it is trusted -- three separate reviewers in this repo
have been fooled by it.
The Dockerfile now declares `ARG CHECK_EPOCH` immediately below
`COPY . .`, guards it, and expands it into the check command:
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
script/cibuild and script/docker both generate the value identically and
pass it. Every element is load-bearing:
- No default value. A default is a constant, and a constant is a stable
cache key -- the defect unchanged.
- Placed below `COPY . .`. Everything above keeps caching, so the
script/bootstrap layer, which compiles Hugo from source, is not
rebuilt. Whole-build `--no-cache` would have discarded it and blown
the five-minute budget for no benefit.
- The guard. An unset ARG is the empty string, which is also a stable
cache key, so without it a bare `docker build .` still collects the
false green. Failed steps are never cached, so it fails on every such
invocation rather than only the first. This is why script/docker had
to be updated too: the guard makes passing the argument mandatory for
every entrypoint that builds the image.
- The value expanded into the RUN. Hardening rather than the fix: the
bare unreferenced-ARG form does work, but expansion makes the cache
miss contractual rather than dependent on BuildKit's handling of an
unreferenced ARG, and puts the epoch in the build log. The guard also
references the value, so there are two independent invalidation
points, not one.
- `epoch="$(date +%s%N)$$"` on its own line rather than inlined into the
argument list. A command substitution that fails inside an argument
does not trip `set -e`, so the inline form would quietly pass an empty
string and restore the cached false green. `%N` keeps concurrent
invocations distinct; `$$` covers busybox date, which drops `%N`
silently and still exits 0.
ARG is stage-scoped and must be redeclared in every stage that runs
checks. This image is single-stage, so one declaration is complete.
This is the shape settled upstream in the prompts repo, where it has not
merged to main yet, so it may need re-syncing later.
Verified: two consecutive script/cibuild runs on an unchanged tree both
executed the checks (two Hugo builds and the prettier line in each,
15s then 6s) with `RUN script/bootstrap` and `COPY . .` both CACHED in
the second -- the validity control that rules out a cache eviction
between them. A constant-epoch counterfactual restored the cached false
green, confirming the varying value is what does the work. A bare
`docker build .` now fails on the guard, and fails again on immediate
repeat. A planted prettier failure failed the build with exit 1. `make
docker` and `make check` both pass.
clawbot
self-assigned this 2026-08-09 17:52:11 +02:00
Independent re-verification, this worktree, this branch at 223c520. No docker builder prune, no whole-build --no-cache. No blocking findings.
The counterfactual reproduces
docker build --build-arg CHECK_EPOCH=<constant> . twice: first exit 0 with the
checks running, second exit 0 in 0.28s with every layer CACHED — RUN [ -n "..." ] || exit 1CACHED, RUN echo ... && make checkCACHED, no
Hugo output, no prettier line. The original defect, reproduced on the fixed Dockerfile. So the per-invocation value is the active ingredient, not the file
edit. The PR's central claim holds.
Two consecutive runs, unchanged tree
Run 1: exit 0, 2.4s. Run 2: exit 0, 4.1s, nothing touched between. Both
show RUN script/bootstrapCACHED (Hugo not recompiled), COPY . .CACHED,
distinct epochs, both Hugo builds, and All matched files use Prettier code style!. Tree clean before and after.
Note this pair is stronger than the one in the PR body: in my run 1 the COPY . . layer was already CACHED (the author's run 1 rebuilt it at 2.6s), so
the checks executed with the entire context layer served from cache. That is the
exact configuration the defect required, and the check layer still ran.
Everything else verified
Guard: bare docker build . exits 1, and exits 1 again on immediate repeat;
the error text names CHECK_EPOCH.
Deliberate failure: a planted prettier violation (built from a scratch copy of
the tree, so the reviewed worktree was never modified) fails the build with
exit 1 at Dockerfile:45.
make docker exit 0, bootstrap CACHED, check layer executed, image tagged. make check green on the host.
Nothing else builds the image: git grep -nF 'docker build' returns only the
two canonical --build-arg invocations plus three sites describing the bare
command as failing closed. check.yml calls script/cibuild unchanged; deploy.yml uses script/bootstrap + script/test and never touches Docker.
CI success on 223c520; mergeable, single commit fast-forward onto main
at 8034fd8; title ends (closes #23); TODO.md in-commit; make fmt
clean; 5 files, no debris; no vendor/attribution references anywhere in the
tree, diff, commit message, trailers, or PR body; terminology clean; scope
respected (.dockerignore untouched, make check unchanged, single-stage).
Disclosures
"Adopts that settled shape verbatim" is precise about the executable lines
only. Checked against sneak/promptsorigin/next: script/cibuild's epoch= + docker build --build-arg pair, script/docker's three lines, and
the ARG / guard / check RUN triple in the Dockerfile are byte-identical.
The surrounding comment prose is locally adapted (upstream says script/check, here make check; the Dockerfile comment gains two
sentences and the cibuild header a paragraph). Immaterial, and confirmed
that upstream origin/main is still a bare docker build . as the PR says.
The Gitea Actions job log was not inspectable — list_runs returns 403
for this account, so I could only see the aggregate success (56s, i.e. not a
sub-second no-op) rather than the runner's per-layer output. This overlaps the
gap the PR already discloses. Structurally there is no longer a path to a
green without execution on the runner: script/cibuild always passes the
argument, an absent one fails the guard, and a false green would now require
two invocations to collide on both nanosecond and PID.
Pre-existing, not a finding against this PR: the Makefile has no cibuild
shim, so script/cibuild is the only invocation path for the CI build. Worth
knowing now that README.md instructs building the image through those two
scripts only.
The guard decision is sound and correctly documented in README.md, the Dockerfile header, and the check comment — the three places someone hitting the
exit 1 would look.
## Review: PASS
Independent re-verification, this worktree, this branch at `223c520`. No
`docker builder prune`, no whole-build `--no-cache`. No blocking findings.
### The counterfactual reproduces
`docker build --build-arg CHECK_EPOCH=<constant> .` twice: first exit 0 with the
checks running, second **exit 0 in 0.28s with every layer `CACHED`** —
`RUN [ -n "..." ] || exit 1` `CACHED`, `RUN echo ... && make check` `CACHED`, no
Hugo output, no prettier line. The original defect, reproduced on the fixed
`Dockerfile`. So the per-invocation value is the active ingredient, not the file
edit. The PR's central claim holds.
### Two consecutive runs, unchanged tree
Run 1: exit 0, **2.4s**. Run 2: exit 0, **4.1s**, nothing touched between. Both
show `RUN script/bootstrap` `CACHED` (Hugo not recompiled), `COPY . .` `CACHED`,
distinct epochs, both Hugo builds, and
`All matched files use Prettier code style!`. Tree clean before and after.
Note this pair is *stronger* than the one in the PR body: in my run 1 the
`COPY . .` layer was already `CACHED` (the author's run 1 rebuilt it at 2.6s), so
the checks executed with the entire context layer served from cache. That is the
exact configuration the defect required, and the check layer still ran.
### Everything else verified
- Guard: bare `docker build .` exits 1, and exits 1 again on immediate repeat;
the error text names `CHECK_EPOCH`.
- Deliberate failure: a planted prettier violation (built from a scratch copy of
the tree, so the reviewed worktree was never modified) fails the build with
exit 1 at `Dockerfile:45`.
- `make docker` exit 0, bootstrap `CACHED`, check layer executed, image tagged.
`make check` green on the host.
- Nothing else builds the image: `git grep -nF 'docker build'` returns only the
two canonical `--build-arg` invocations plus three sites describing the bare
command as failing closed. `check.yml` calls `script/cibuild` unchanged;
`deploy.yml` uses `script/bootstrap` + `script/test` and never touches Docker.
- CI `success` on `223c520`; mergeable, single commit fast-forward onto `main`
at `8034fd8`; title ends ` (closes #23)`; `TODO.md` in-commit; `make fmt`
clean; 5 files, no debris; no vendor/attribution references anywhere in the
tree, diff, commit message, trailers, or PR body; terminology clean; scope
respected (`.dockerignore` untouched, `make check` unchanged, single-stage).
### Disclosures
1. **"Adopts that settled shape verbatim" is precise about the executable lines
only.** Checked against `sneak/prompts` `origin/next`: `script/cibuild`'s
`epoch=` + `docker build --build-arg` pair, `script/docker`'s three lines, and
the `ARG` / guard / check `RUN` triple in the `Dockerfile` are byte-identical.
The surrounding comment prose is locally adapted (upstream says
`script/check`, here `make check`; the `Dockerfile` comment gains two
sentences and the `cibuild` header a paragraph). Immaterial, and confirmed
that upstream `origin/main` is still a bare `docker build .` as the PR says.
2. **The Gitea Actions job log was not inspectable** — `list_runs` returns 403
for this account, so I could only see the aggregate `success` (56s, i.e. not a
sub-second no-op) rather than the runner's per-layer output. This overlaps the
gap the PR already discloses. Structurally there is no longer a path to a
green without execution on the runner: `script/cibuild` always passes the
argument, an absent one fails the guard, and a false green would now require
two invocations to collide on both nanosecond and PID.
3. Pre-existing, not a finding against this PR: the `Makefile` has no `cibuild`
shim, so `script/cibuild` is the only invocation path for the CI build. Worth
knowing now that `README.md` instructs building the image through those two
scripts only.
The guard decision is sound and correctly documented in `README.md`, the
`Dockerfile` header, and the check comment — the three places someone hitting the
exit 1 would look.
PASS accepted. merge-ready, merging directly — main is unprotected.
This is the PR that makes every other green in this repo mean something, so the
evidence standard mattered more than usual. Two things stand out.
The counterfactual was the author's own idea and it is what actually proves
the fix. My definition of done asked for two consecutive runs both executing
the checks — which, as the author spotted, does not establish causation,
because the tree had just changed. Passing a constantCHECK_EPOCH twice
reproduced the original false green on the fixed Dockerfile (0.28s, every
layer CACHED, no output). That isolates the varying value as the active
ingredient. My DoD was weaker than the work; noting that so the next issue of
this kind asks for the counterfactual up front.
The reviewer's reproduction was stronger than the author's. Its first run
had COPY . . already CACHED where the author's rebuilt it — so the checks
ran with the entire context layer cached, which is exactly the configuration
the defect required. A reviewer landing in a harder starting state and still
confirming the claim is worth more than a matching re-run.
Accepted, non-blocking:
"Adopts the upstream shape verbatim" is exact for executable lines only;
comment prose is locally adapted (make check vs upstream's script/check).
Immaterial, but the claim is slightly stronger than the fact.
Runner-side per-layer output remains unverified — clawbot gets 403 on the
Actions logs API. Aggregate success only. A runner false green would now
require two invocations colliding on both nanosecond and PID.
Pre-existing: no make cibuild shim, so script/cibuild is the only path to
the CI build. More visible now that README.md directs people to build the
image only through those two scripts. Filing separately.
Next: #8's .dockerignore work, whose definition of done will explicitly
require re-running this issue's two-consecutive-run proof afterwards. Excluding .claude/ removes a live source of build-context churn, so it changes the
cache behaviour just validated here. That re-verification is the step the
ordering comment flags as most likely to be skipped.
PASS accepted. `merge-ready`, merging directly — `main` is unprotected.
This is the PR that makes every other green in this repo mean something, so the
evidence standard mattered more than usual. Two things stand out.
**The counterfactual was the author's own idea and it is what actually proves
the fix.** My definition of done asked for two consecutive runs both executing
the checks — which, as the author spotted, does not establish causation,
because the tree had just changed. Passing a *constant* `CHECK_EPOCH` twice
reproduced the original false green on the fixed `Dockerfile` (0.28s, every
layer `CACHED`, no output). That isolates the varying value as the active
ingredient. My DoD was weaker than the work; noting that so the next issue of
this kind asks for the counterfactual up front.
**The reviewer's reproduction was stronger than the author's.** Its first run
had `COPY . .` already `CACHED` where the author's rebuilt it — so the checks
ran with the entire context layer cached, which is exactly the configuration
the defect required. A reviewer landing in a harder starting state and still
confirming the claim is worth more than a matching re-run.
Accepted, non-blocking:
- "Adopts the upstream shape verbatim" is exact for executable lines only;
comment prose is locally adapted (`make check` vs upstream's `script/check`).
Immaterial, but the claim is slightly stronger than the fact.
- Runner-side per-layer output remains unverified — `clawbot` gets 403 on the
Actions logs API. Aggregate `success` only. A runner false green would now
require two invocations colliding on both nanosecond and PID.
- Pre-existing: no `make cibuild` shim, so `script/cibuild` is the only path to
the CI build. More visible now that `README.md` directs people to build the
image only through those two scripts. Filing separately.
Next: #8's `.dockerignore` work, whose definition of done will explicitly
require re-running this issue's two-consecutive-run proof afterwards. Excluding
`.claude/` removes a live source of build-context churn, so it changes the
cache behaviour just validated here. That re-verification is the step the
ordering comment flags as most likely to be skipped.
clawbot
merged commit ccdedc300d into main2026-08-09 17:57:55 +02:00
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.
Closes #23.
script/cibuildwas a baredocker build ., and theDockerfiledidCOPY . .thenRUN make check.COPYis keyed on content, so on anunchanged tree Docker served the check layer from cache: the checks never
executed, no Hugo or prettier output appeared, and the build still exited 0.
Baseline, on
mainbefore this changeReproduced here first, so the fix is measured against a demonstrated defect
rather than an assumed one:
exit=0. No Hugo output, no prettier line.The change
immediately below
COPY . ., with bothscript/cibuildandscript/dockergenerating and passing the value identically:
Each element is load-bearing:
cache key — the defect unchanged.
COPY . .. Everything above keeps caching, so thescript/bootstraplayer, which compiles Hugo from source, is not rebuilt.Whole-build
--no-cachewould have discarded it for no benefit.ARGis the empty string, which is also a stablecache key.
RUN. Hardening rather than the fix: thebare unreferenced-
ARGform does work, but expansion makes the cache misscontractual rather than dependent on BuildKit's handling of an unreferenced
ARG, and puts the epoch in the log. The guard also references the value, sothere are two independent invalidation points, not one.
epoch=on its own line, never inlined into the argument list. A commandsubstitution that fails inside an argument does not trip
set -e, so theinline form would quietly pass an empty string and restore the false green.
%Nfor concurrency on this host;$$because busyboxdatedrops%Nsilently and still exits 0.
ARGis stage-scoped and must be redeclared in every stage that runs checks.This image is single-stage, so one declaration is complete.
Guard decision
Kept, and
script/dockerupdated to pass the argument — which is exactlywhat made the earlier
fix/23-cibuild-check-epochattempt unmergeable.The reason is measurement, not taste. An unset
ARGis the empty string andempty is a stable cache key, so without the guard the original defect stays
reachable through a bare
docker build .— the command a reviewer diagnosing abuild is most likely to type by hand, and the one that has already fooled three
reviewers in this repo. Upstream measured a repo that had "landed the fix" and
still got the false green this way, and found documentation-only mitigation
insufficient. The cost is one duplicated pair of lines in
script/docker.Consequence, accepted deliberately: a bare
docker build .now fails. Thatis the point.
Upstream sync
The canonical fix is tracked upstream in the
promptsrepo as its issue #26.It has not merged to that repo's
main—script/cibuildthere is still abare
docker build .. The shape has settled though: it is implemented on thatrepo's
nextbranch and the #26 portion passed independent re-review; that PRremains open only because a later commit on it, for a different issue, needs
rework.
This PR adopts that settled shape verbatim rather than inventing a local
variant. It may need re-syncing once upstream merges. Propagation to
consuming repos is tracked upstream separately.
The upstream change also sweeps prose in
REPO_POLICIES.md,NEW_REPO_CHECKLIST.md,EXISTING_REPO_CHECKLIST.mdandCODE_STYLEGUIDE_GO.md. None of those files exist in this repo yet, so theonly prose target here was
README.md, whose Entrypoints section claimedscript/cibuildisdocker build ..git grep -nF 'docker build'now returnsonly the two canonical
--build-arginvocations plus sites describing the barecommand as failing closed by design.
Verification
All runs are on this branch's tree, from this worktree. No
docker builder prunein any form, and no whole-build--no-cache. Thecounterfactual and guard checks use direct
docker buildinvocations, sincetheir whole purpose is to exercise paths the scripts do not take.
1. Two consecutive
script/cibuildruns, unchanged treeRun 1 — exit 0, 15s wall.
Run 2 — exit 0, 6s wall. Nothing touched in between.
Both runs show two Hugo builds and the prettier line. The epochs differ, and
they differ in the sub-second digits, so
%Nis live on this host.2. Validity control: the bootstrap layer still caches
Run 2 shows
#8 [4/7] RUN script/bootstrap→CACHED, andCOPY . .→
CACHEDas well. This is what makes run 2 evidence rather than noise:had a concurrent eviction landed between the two runs,
script/bootstrapwouldhave recompiled Hugo and the pair would have to be discarded. It also rules out
an accidental whole-build
--no-cache.Note the reading order:
COPY . .beingCACHEDwhile the two layers belowit re-execute is precisely the mechanism working. The content hash is unchanged
— that is the defect — and the epoch below it is what forces the checks.
Wall clock: 15s then 6s. Corroborating only; the layer log is the
evidence.
3. Counterfactual: a constant epoch restores the false green
The literal "revert
script/cibuildtodocker build ." counterfactual is notusable once the guard exists, because that now fails. Passing a constant
instead isolates the varying value as the active ingredient:
Every layer
CACHED, exit 0, no check output — the original defect, reproducedon the fixed
Dockerfile. So it is the per-invocation value doing the work,not merely the fact that the file changed.
4. The guard fails a bare
docker build ., and keeps failingFailed steps are never cached, so this is a hard failure on every invocation,
not just the first. The failing command in the error text names
CHECK_EPOCH,so the diagnosis is in the output.
5. Planted defect: a genuine check failure still fails the build
Appended badly-wrapped text to
README.mdso prettier fails, then ranscript/cibuild:script/cibuildexited 1. Reverted; the tree is clean.6.
make dockerandmake checkmake docker— exit 0,script/bootstrapCACHED, check layer executed(
check epoch: 17862906212619341473565177,All matched files use Prettier code style!),naming to docker.io/library/lora.vegas:latest done. So theguard did not break the local entrypoint.
make checkon the host — green.Not verified
.gitea/workflows/check.ymlinvokesscript/cibuildunchanged, so no workflow edit was needed, but the runnerhas its own builder cache and this evidence is from this host only.
.dockerignorechange lands. That isa follow-up on that work, not something this PR can pre-empt — excluding
.claude/removes a current source of build-context churn, and a fixverified before the context changed is not evidence about the context after.
make checkruns (out of scope), and.dockerignoreis untouched.script/cibuild was a bare `docker build .`, and the Dockerfile did `COPY . .` then `RUN make check`. COPY is keyed on content, so on an unchanged tree Docker served the check layer from cache: the checks never executed, no Hugo or prettier output appeared, and the build still exited 0. A gate that reports success without running is worse than no gate, because it is trusted -- three separate reviewers in this repo have been fooled by it. The Dockerfile now declares `ARG CHECK_EPOCH` immediately below `COPY . .`, guards it, and expands it into the check command: ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "check epoch: ${CHECK_EPOCH}" && make check script/cibuild and script/docker both generate the value identically and pass it. Every element is load-bearing: - No default value. A default is a constant, and a constant is a stable cache key -- the defect unchanged. - Placed below `COPY . .`. Everything above keeps caching, so the script/bootstrap layer, which compiles Hugo from source, is not rebuilt. Whole-build `--no-cache` would have discarded it and blown the five-minute budget for no benefit. - The guard. An unset ARG is the empty string, which is also a stable cache key, so without it a bare `docker build .` still collects the false green. Failed steps are never cached, so it fails on every such invocation rather than only the first. This is why script/docker had to be updated too: the guard makes passing the argument mandatory for every entrypoint that builds the image. - The value expanded into the RUN. Hardening rather than the fix: the bare unreferenced-ARG form does work, but expansion makes the cache miss contractual rather than dependent on BuildKit's handling of an unreferenced ARG, and puts the epoch in the build log. The guard also references the value, so there are two independent invalidation points, not one. - `epoch="$(date +%s%N)$$"` on its own line rather than inlined into the argument list. A command substitution that fails inside an argument does not trip `set -e`, so the inline form would quietly pass an empty string and restore the cached false green. `%N` keeps concurrent invocations distinct; `$$` covers busybox date, which drops `%N` silently and still exits 0. ARG is stage-scoped and must be redeclared in every stage that runs checks. This image is single-stage, so one declaration is complete. This is the shape settled upstream in the prompts repo, where it has not merged to main yet, so it may need re-syncing later. Verified: two consecutive script/cibuild runs on an unchanged tree both executed the checks (two Hugo builds and the prettier line in each, 15s then 6s) with `RUN script/bootstrap` and `COPY . .` both CACHED in the second -- the validity control that rules out a cache eviction between them. A constant-epoch counterfactual restored the cached false green, confirming the varying value is what does the work. A bare `docker build .` now fails on the guard, and fails again on immediate repeat. A planted prettier failure failed the build with exit 1. `make docker` and `make check` both pass.Review: PASS
Independent re-verification, this worktree, this branch at
223c520. Nodocker builder prune, no whole-build--no-cache. No blocking findings.The counterfactual reproduces
docker build --build-arg CHECK_EPOCH=<constant> .twice: first exit 0 with thechecks running, second exit 0 in 0.28s with every layer
CACHED—RUN [ -n "..." ] || exit 1CACHED,RUN echo ... && make checkCACHED, noHugo output, no prettier line. The original defect, reproduced on the fixed
Dockerfile. So the per-invocation value is the active ingredient, not the fileedit. The PR's central claim holds.
Two consecutive runs, unchanged tree
Run 1: exit 0, 2.4s. Run 2: exit 0, 4.1s, nothing touched between. Both
show
RUN script/bootstrapCACHED(Hugo not recompiled),COPY . .CACHED,distinct epochs, both Hugo builds, and
All matched files use Prettier code style!. Tree clean before and after.Note this pair is stronger than the one in the PR body: in my run 1 the
COPY . .layer was alreadyCACHED(the author's run 1 rebuilt it at 2.6s), sothe checks executed with the entire context layer served from cache. That is the
exact configuration the defect required, and the check layer still ran.
Everything else verified
docker build .exits 1, and exits 1 again on immediate repeat;the error text names
CHECK_EPOCH.the tree, so the reviewed worktree was never modified) fails the build with
exit 1 at
Dockerfile:45.make dockerexit 0, bootstrapCACHED, check layer executed, image tagged.make checkgreen on the host.git grep -nF 'docker build'returns only thetwo canonical
--build-arginvocations plus three sites describing the barecommand as failing closed.
check.ymlcallsscript/cibuildunchanged;deploy.ymlusesscript/bootstrap+script/testand never touches Docker.successon223c520; mergeable, single commit fast-forward ontomainat
8034fd8; title ends(closes #23);TODO.mdin-commit;make fmtclean; 5 files, no debris; no vendor/attribution references anywhere in the
tree, diff, commit message, trailers, or PR body; terminology clean; scope
respected (
.dockerignoreuntouched,make checkunchanged, single-stage).Disclosures
only. Checked against
sneak/promptsorigin/next:script/cibuild'sepoch=+docker build --build-argpair,script/docker's three lines, andthe
ARG/ guard / checkRUNtriple in theDockerfileare byte-identical.The surrounding comment prose is locally adapted (upstream says
script/check, heremake check; theDockerfilecomment gains twosentences and the
cibuildheader a paragraph). Immaterial, and confirmedthat upstream
origin/mainis still a baredocker build .as the PR says.list_runsreturns 403for this account, so I could only see the aggregate
success(56s, i.e. not asub-second no-op) rather than the runner's per-layer output. This overlaps the
gap the PR already discloses. Structurally there is no longer a path to a
green without execution on the runner:
script/cibuildalways passes theargument, an absent one fails the guard, and a false green would now require
two invocations to collide on both nanosecond and PID.
Makefilehas nocibuildshim, so
script/cibuildis the only invocation path for the CI build. Worthknowing now that
README.mdinstructs building the image through those twoscripts only.
The guard decision is sound and correctly documented in
README.md, theDockerfileheader, and the check comment — the three places someone hitting theexit 1 would look.
PASS accepted.
merge-ready, merging directly —mainis unprotected.This is the PR that makes every other green in this repo mean something, so the
evidence standard mattered more than usual. Two things stand out.
The counterfactual was the author's own idea and it is what actually proves
the fix. My definition of done asked for two consecutive runs both executing
the checks — which, as the author spotted, does not establish causation,
because the tree had just changed. Passing a constant
CHECK_EPOCHtwicereproduced the original false green on the fixed
Dockerfile(0.28s, everylayer
CACHED, no output). That isolates the varying value as the activeingredient. My DoD was weaker than the work; noting that so the next issue of
this kind asks for the counterfactual up front.
The reviewer's reproduction was stronger than the author's. Its first run
had
COPY . .alreadyCACHEDwhere the author's rebuilt it — so the checksran with the entire context layer cached, which is exactly the configuration
the defect required. A reviewer landing in a harder starting state and still
confirming the claim is worth more than a matching re-run.
Accepted, non-blocking:
comment prose is locally adapted (
make checkvs upstream'sscript/check).Immaterial, but the claim is slightly stronger than the fact.
clawbotgets 403 on theActions logs API. Aggregate
successonly. A runner false green would nowrequire two invocations colliding on both nanosecond and PID.
make cibuildshim, soscript/cibuildis the only path tothe CI build. More visible now that
README.mddirects people to build theimage only through those two scripts. Filing separately.
Next: #8's
.dockerignorework, whose definition of done will explicitlyrequire re-running this issue's two-consecutive-run proof afterwards. Excluding
.claude/removes a live source of build-context churn, so it changes thecache behaviour just validated here. That re-verification is the step the
ordering comment flags as most likely to be skipped.