1 Commits

Author SHA1 Message Date
clawbot
417f142a9f Bust the Docker check-layer cache with a per-invocation CHECK_EPOCH (closes #26)
Some checks failed
check / check (push) Has been cancelled
script/cibuild was a plain `docker build .`, and the Dockerfile does
`COPY . .` followed by `RUN make check`. Docker invalidates a COPY layer
only when the copied content changes, so on an unchanged tree the check
layer was served from cache, the suite never ran, and the build still
exited 0. Measured here: run 1 took 18.5s and ran the suite; run 2 on a
byte-identical tree took 0.286s with `RUN make check` CACHED.

script/cibuild and script/docker now assign a per-invocation nonce on its
own line and pass it as --build-arg CHECK_EPOCH. The Dockerfile declares
ARG CHECK_EPOCH, guards it with `[ -n "$CHECK_EPOCH" ] || exit 1`, and
expands it into the check command. Post-fix, two consecutive runs both
execute make check (17.4s / 8.1s) with `RUN script/bootstrap` still
CACHED, so dependency layers are untouched and the build ceiling is not
at risk.

The guard is what makes a bare `docker build .` — the command
REPO_POLICIES named verbatim — fail closed rather than reuse the empty
and therefore stable cache key; verified failing in 0.455s. Holding the
epoch constant restores the false green (run 2 fully CACHED), which pins
the varying value as the operative mechanism rather than a coincidence.

Because the guard references $CHECK_EPOCH it is itself value-keyed:
BuildKit renders the epoch into that layer's description and re-runs the
layer when the value changes. Each stage therefore has two independent
invalidation points, the guard and the expansion, and the guard always
precedes the check RUN. Both are kept and the prose now records this;
the expansion remains defence in depth and is what puts the epoch in the
build log.

The false guarantee was org-canonical text in more than one document, so
it is corrected everywhere it appeared rather than only where the issue
first found it. REPO_POLICIES.md carried it in two places, and its Go
multistage template had check steps in two stages; ARG is stage-scoped,
so both stages get the treatment or the fleet inherits the half-fixed
shape. CODE_STYLEGUIDE_GO.md restated the guarantee for the bare command
this change makes fail closed. NEW_REPO_CHECKLIST.md specified the
pre-fix script/cibuild verbatim, so every new repo would have been born
with the false green, and EXISTING_REPO_CHECKLIST.md ended on a
`docker build` acceptance item that the guard makes unsatisfiable by
design — an agent working that checklist would have been led to delete
the guard to tick the last box. Both checklists' Dockerfile criteria were
also satisfiable by a Dockerfile whose check layers are still frozen, and
now require the ARG and guard in every check-running stage.
2026-08-09 15:00:49 +00:00
6 changed files with 57 additions and 19 deletions

View File

@@ -20,7 +20,9 @@ COPY . .
# makes a bare `docker build .` fail loudly instead of silently reusing
# the empty (and therefore stable) cache key. Expand the value into the
# command so the cache miss does not depend on BuildKit's handling of an
# unreferenced ARG.
# unreferenced ARG. Both the guard and the check RUN reference the value,
# so both are value-keyed: there are two independent invalidation points
# here, not one. Keep both.
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check

View File

@@ -26,7 +26,10 @@ fmt-check, and commit.
`Dockerfile` (plus the Go multistage template in REPO_POLICIES.md, in both its
lint and builder stages) declares `ARG CHECK_EPOCH` with a guard that makes a
bare `docker build .` fail closed. Corrected the org-canonical text that
asserted a successful build implies all checks pass.
asserted a successful build implies all checks pass, across every document
carrying it: `REPO_POLICIES.md`, both repo checklists (which still told agents
to write the pre-fix `script/cibuild` and ended on an acceptance item the
guard makes unsatisfiable), and the Go styleguide.
- 2026-08-07: Set the canonical `.golangci.yml` to the org-standard v2-schema
config already deployed byte-identical across the org's Go repos (settings
under `linters.settings` so thresholds like lll/funlen/cyclop/dupl actually

View File

@@ -1,6 +1,6 @@
---
title: Code Styleguide — Go
last_modified: 2026-03-18
last_modified: 2026-08-09
---
1. Try to hard wrap long lines at 77 characters or less.
@@ -101,9 +101,16 @@ last_modified: 2026-03-18
`golangci-lint`.
1. Write a `Dockerfile` for every repo, even if it only runs the tests and
linting. `docker build .` should always make sure that the code is in an
able-to-be-compiled state, linted, and any tests run. The Docker build
should fail if linting doesn't pass.
linting. `script/cibuild` and `script/docker` should always make sure that
the code is in an able-to-be-compiled state, linted, and any tests run, and
the build should fail if linting doesn't pass. That guarantee holds only
because those scripts pass a per-invocation `CHECK_EPOCH` build arg that
busts the check layers out of the Docker cache; without it an unchanged tree
serves those layers from cache and the build reports a green it never ran. A
bare `docker build .` fails closed by design, on the `[ -n "$CHECK_EPOCH" ]`
guard — always go through `script/cibuild` or `script/docker`. See
[Repository Policies](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md)
for the canonical form.
1. Every repo must have a `Makefile`. See
[Repository Policies](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md)

View File

@@ -1,6 +1,6 @@
---
title: Existing Repo Checklist
last_modified: 2026-07-06
last_modified: 2026-08-09
---
Use this checklist when beginning work in a repo that may not yet conform to our
@@ -29,10 +29,15 @@ with your task.
if missing
- [ ] `.editorconfig` exists — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`
- [ ] `Dockerfile` and `.dockerignore` exist; Dockerfile runs `make check` as a
build step — fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `docker build .` on
- [ ] `Dockerfile` and `.dockerignore` exist (fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`);
Dockerfile runs `make check` as a build step, and every stage containing a
check-running `RUN` declares `ARG CHECK_EPOCH` with the
`RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
served from cache on an unchanged tree and the build reports a green it
never ran.
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
push — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
- [ ] Language-specific config:
@@ -104,5 +109,6 @@ with your task.
# Final
- [ ] `make check` passes
- [ ] `docker build` succeeds
- [ ] `script/cibuild` succeeds (a bare `docker build .` fails closed by design,
on the `CHECK_EPOCH` guard)
- [ ] Commit and merge fixes before starting your actual task

View File

@@ -1,6 +1,6 @@
---
title: New Repo Checklist
last_modified: 2026-07-06
last_modified: 2026-08-09
---
Use this checklist when creating a new repository from scratch. Follow the steps
@@ -52,7 +52,12 @@ Template files can be fetched from:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
- [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
- All Dockerfiles must run `make check` as a build step
- All Dockerfiles must run `make check` as a build step, and every stage
containing a check-running `RUN` must declare `ARG CHECK_EPOCH` with the
`RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the
`CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is
served from cache on an unchanged tree and the build reports a green it
never ran.
- Server: also builds and runs the application
- Non-server: brings up dev environment and runs `make check`
- Image pinned by sha256 hash with version/date comment
@@ -90,8 +95,14 @@ are thin shims calling them. Model scripts:
- [ ] `script/projectname` — outputs the project name (used by `script/docker`
for the image tag)
- [ ] `script/docker` / `make docker` — builds Docker image, tagged via
`script/projectname` (byte-identical across repos)
- [ ] `script/cibuild` — cd to repo root, `docker build .` (what CI runs)
`script/projectname` (byte-identical across repos); assigns
`epoch="$(date +%s%N)$$"` on its own line and passes
`--build-arg CHECK_EPOCH="$epoch"`
- [ ] `script/cibuild` — cd to repo root, assign `epoch="$(date +%s%N)$$"` on
its own line, then run `docker build --build-arg CHECK_EPOCH="$epoch" .`
(what CI runs). The build arg is mandatory: see the `CHECK_EPOCH` rule in
`REPO_POLICIES.md` for why each element is load-bearing. A bare
`docker build .` fails closed by design.
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
- [ ] `script/install-precommit` — installs the pre-commit hook that runs
`script/precommit`

View File

@@ -128,7 +128,14 @@ style conventions are in separate documents:
that runs checks, immediately above the first such `RUN`.
- Expand the value into the command. This makes the cache miss contractual
rather than dependent on BuildKit's handling of an unreferenced `ARG`, and
it puts the epoch in the build log.
it puts the epoch in the build log. The guard is itself value-keyed, for
the same reason: it references `$CHECK_EPOCH`, so BuildKit renders the
epoch into that layer's description (observed as
`RUN [ -n "1786287053..." ] || exit 1`) and re-runs it whenever the value
changes. Each stage therefore has two independent invalidation points, and
the guard always precedes the check `RUN`. Keep both: the expansion is
defence in depth, and it is what makes the epoch visible in the build
output.
- The `[ -n ... ]` guard is required: an unset `ARG` is empty, and empty is
a stable cache key, so without it a bare `docker build .` still produces
the false green. Failed steps are never cached, so the guard fails on
@@ -222,8 +229,10 @@ style conventions are in separate documents:
below the `ARG` so a bare `docker build .` fails instead of reusing the
empty cache key, and the value is expanded into the first check `RUN` so
the cache miss does not rely on BuildKit's unreferenced-`ARG` handling.
The later `RUN`s in the same stage need no expansion of their own: they
are already invalidated by their busted parent layer.
Both of those lines reference `$CHECK_EPOCH`, so both are value-keyed:
each stage is invalidated at two independent points. The later `RUN`s in
the same stage need no expansion of their own: they are already
invalidated by their busted parent layer.
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
runs `script/cibuild` (which runs