Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the script/ entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.
Reference implementation is sneak/homoicon — copy its shape: a root Dockerfile.lint built FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which COPYs the repo in and runs golangci-lint run --config .golangci.yml ./... as a build step, with script/lint reduced to building it. Linting as a build step means a successful build IS a clean lint, and it works even where the docker daemon is remote and bind mounts are impossible.
This supersedes the per-worktree cache-isolation approach previously scoped here. The controlled experiment run in this repo showed isolation fixes contamination but NOT lock contention — two concurrent runs with entirely separate cache directories still collided. A container per run has its own cache and its own lock, so the whole class goes away. Caching is explicitly waived by the owner.
Two things to get right, both of which would otherwise ship a false green:
A cached build lints nothing.docker build -f Dockerfile.lint . on an unchanged tree returns success in well under a second having run no linter. Force the lint layers to execute.
golangci-lint config verify fetches its JSON schema over an unpinned live HTTPS call, making lint network-dependent and breaking hash-pinning. Decide deliberately whether to include it.
Also remove the golangci-lint install path from script/bootstrap and the native escape hatch in script/lint — nothing runs on the host any more, which also closes the context-gating problem raised earlier.
Definition of done
script/lint runs the linter only in Docker; no host golangci-lint path remains.
Two consecutive script/lint runs on an unchanged tree both demonstrably execute the linter.
Negative control: introduce a deliberate lint violation, confirm it fails with that specific finding, revert, confirm clean.
Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the `script/` entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.
Reference implementation is `sneak/homoicon` — copy its shape: a root `Dockerfile.lint` built `FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`, which COPYs the repo in and runs `golangci-lint run --config .golangci.yml ./...` as a build step, with `script/lint` reduced to building it. Linting as a build step means a successful build IS a clean lint, and it works even where the docker daemon is remote and bind mounts are impossible.
This supersedes the per-worktree cache-isolation approach previously scoped here. The controlled experiment run in this repo showed isolation fixes contamination but NOT lock contention — two concurrent runs with entirely separate cache directories still collided. A container per run has its own cache and its own lock, so the whole class goes away. Caching is explicitly waived by the owner.
Two things to get right, both of which would otherwise ship a false green:
1. **A cached build lints nothing.** `docker build -f Dockerfile.lint .` on an unchanged tree returns success in well under a second having run no linter. Force the lint layers to execute.
2. **`golangci-lint config verify` fetches its JSON schema over an unpinned live HTTPS call**, making lint network-dependent and breaking hash-pinning. Decide deliberately whether to include it.
Also remove the golangci-lint install path from `script/bootstrap` and the native escape hatch in `script/lint` — nothing runs on the host any more, which also closes the context-gating problem raised earlier.
## Definition of done
- `script/lint` runs the linter only in Docker; no host golangci-lint path remains.
- Two consecutive `script/lint` runs on an unchanged tree both demonstrably execute the linter.
- Negative control: introduce a deliberate lint violation, confirm it fails with that specific finding, revert, confirm clean.
- `make check` still green.
Canonical tracking issue: https://git.eeqj.de/sneak/prompts/issues/40
Implementation requirements. Work lands on next; a next -> main PR is opened from the first commit of this cycle.
Shape
New root Dockerfile.lint, FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 (the Debian-based tag, exactly as the owner's ruling names it — not the -alpine variant currently in Dockerfile). It COPYs the repo in and runs golangci-lint run --config .golangci.yml ./... as a build step. script/lint reduces to building it.
Four things this repo has to get right that the reference does not cover
1. A cached build lints nothing. The homoicon reference has no cache-busting; on an unchanged tree its docker build exits 0 in under a second having run no linter. Do not copy that. Reuse the mechanism this repo already established for exactly this in Dockerfile and script/cibuild: ARG CHECK_EPOCH immediately above the lint RUNs, a RUN [ -n "$CHECK_EPOCH" ] || exit 1 guard, the value expanded into the check command itself, no default value, and script/lint computing epoch="$(date +%s%N)$$" as a bare assignment on its own line (inline in the argument, a failing substitution does not abort under set -eu and yields a constant empty epoch — the exact false green being prevented). The ARG goes below COPY go.mod go.sum / go mod download so dependency layers still cache.
2. The Dockerfile lint stage cannot survive as-is. It runs make lint, which will now shell out to docker build — docker-in-docker, inside a BuildKit step, with no daemon. Recommended resolution: delete the lint stage from Dockerfile entirely (along with ENV VAULTIK_LINT_IN_CONTAINER=1 and the builder's COPY --from=lint /src/go.sum /dev/null), move make fmt-check next to make test in the builder stage, and have script/cibuild build Dockerfile.lint first with a fresh epoch and then Dockerfile with a fresh epoch, failing on either. Rationale: Dockerfile.lint then becomes the single source of truth for the linter version. The alternative — keeping a lint stage that calls golangci-lint directly — reintroduces two independently-bumpable digest pins, which is the drift issue #78 was filed over. If you take the alternative, say why in the PR body. Note the consequence either way: script/docker builds only the product image and so no longer lints; script/cibuild is the gate and script/check runs script/lint. Say that plainly in a comment rather than leaving it to be discovered.
3. golangci-lint config verify fetches its JSON schema over an unpinned live HTTPS call. Recommendation: omit it, and say in Dockerfile.lint why. It makes the lint gate network-dependent on an unpinned remote resource, against the hash-pinning policy, and an upstream outage or an egress-less runner then produces a red that is not a lint verdict — the false-red class this repo has spent several issues eliminating. Include it only on evidence that it runs fully offline at this pinned version; if you test that, test it with the network actually off (docker run --network none) and record the output.
4. script/lint-fix cannot delegate to a build step — a build cannot write fixes back to the host tree. Recommendation: keep it, reimplemented as a bind-mounted docker run against the image reference parsed out of Dockerfile.lint, with a comment stating outright that it is a developer convenience, never a gate, and that it needs a local daemon. It consults no exit code that any gate reads. Deleting it and make lint-fix is acceptable if you prefer — lint-fix is not in the canonical scripts-to-rule-them-all set — but then say so in the PR body.
Deletions
Everything on the host lint path goes: the native escape hatch and its version detection in script/lint, VAULTIK_LINT_IN_CONTAINER in both files, the per-worktree cache directory machinery (cache_home, cache_dir, path_digest, prepare_cache, prune_dead_caches), the lock-retry loop, and script/lint-audit with its run_capture/audit_output plumbing — a container per run has its own cache and its own lock, so neither contamination nor contention exists to audit for. Update script/bootstrap's docker-requirement text, which names "the Dockerfile's lint stage" throughout.
Definition of done
Everything in the issue body, plus:
Both traps above demonstrated, not asserted. Two consecutive script/lint runs on an unchanged tree, --progress=plain, showing the lint layer executing (not CACHED) on both.
Negative control: introduce a deliberate lint violation, confirm the build fails citing that specific finding, revert, confirm clean.
Withheld---build-arg counterfactual: a bare docker build -f Dockerfile.lint . fails on the guard rather than exiting 0.
make check and script/cibuild both green, evidence recorded on the PR.
TODO.md updated in the same commit; markdown formatted with make fmt.
Do not run docker builder prune — the build cache is shared with other work on this host. Scope any invalidation with --no-cache-filter=<stage>.
Commit title ends (closes #113). Issue #88 and issue #103 describe defects in code this removes; leave them out of the commit message and they will be closed against this work separately.
Implementation requirements. Work lands on `next`; a `next` -> `main` PR is opened from the first commit of this cycle.
## Shape
New root `Dockerfile.lint`, `FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240` (the Debian-based tag, exactly as the owner's ruling names it — not the `-alpine` variant currently in `Dockerfile`). It COPYs the repo in and runs `golangci-lint run --config .golangci.yml ./...` as a build step. `script/lint` reduces to building it.
## Four things this repo has to get right that the reference does not cover
**1. A cached build lints nothing.** The homoicon reference has no cache-busting; on an unchanged tree its `docker build` exits 0 in under a second having run no linter. Do not copy that. Reuse the mechanism this repo already established for exactly this in `Dockerfile` and `script/cibuild`: `ARG CHECK_EPOCH` immediately above the lint `RUN`s, a `RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard, the value expanded into the check command itself, no default value, and `script/lint` computing `epoch="$(date +%s%N)$$"` as a bare assignment on its own line (inline in the argument, a failing substitution does not abort under `set -eu` and yields a constant empty epoch — the exact false green being prevented). The `ARG` goes below `COPY go.mod go.sum` / `go mod download` so dependency layers still cache.
**2. The `Dockerfile` lint stage cannot survive as-is.** It runs `make lint`, which will now shell out to `docker build` — docker-in-docker, inside a BuildKit step, with no daemon. Recommended resolution: delete the lint stage from `Dockerfile` entirely (along with `ENV VAULTIK_LINT_IN_CONTAINER=1` and the builder's `COPY --from=lint /src/go.sum /dev/null`), move `make fmt-check` next to `make test` in the builder stage, and have `script/cibuild` build `Dockerfile.lint` first with a fresh epoch and then `Dockerfile` with a fresh epoch, failing on either. Rationale: `Dockerfile.lint` then becomes the single source of truth for the linter version. The alternative — keeping a lint stage that calls `golangci-lint` directly — reintroduces two independently-bumpable digest pins, which is the drift [issue #78](https://git.eeqj.de/sneak/vaultik/issues/78) was filed over. If you take the alternative, say why in the PR body. Note the consequence either way: `script/docker` builds only the product image and so no longer lints; `script/cibuild` is the gate and `script/check` runs `script/lint`. Say that plainly in a comment rather than leaving it to be discovered.
**3. `golangci-lint config verify` fetches its JSON schema over an unpinned live HTTPS call.** Recommendation: omit it, and say in `Dockerfile.lint` why. It makes the lint gate network-dependent on an unpinned remote resource, against the hash-pinning policy, and an upstream outage or an egress-less runner then produces a red that is not a lint verdict — the false-red class this repo has spent several issues eliminating. Include it only on evidence that it runs fully offline at this pinned version; if you test that, test it with the network actually off (`docker run --network none`) and record the output.
**4. `script/lint-fix` cannot delegate to a build step** — a build cannot write fixes back to the host tree. Recommendation: keep it, reimplemented as a bind-mounted `docker run` against the image reference parsed out of `Dockerfile.lint`, with a comment stating outright that it is a developer convenience, never a gate, and that it needs a local daemon. It consults no exit code that any gate reads. Deleting it and `make lint-fix` is acceptable if you prefer — `lint-fix` is not in the canonical scripts-to-rule-them-all set — but then say so in the PR body.
## Deletions
Everything on the host lint path goes: the native escape hatch and its version detection in `script/lint`, `VAULTIK_LINT_IN_CONTAINER` in both files, the per-worktree cache directory machinery (`cache_home`, `cache_dir`, `path_digest`, `prepare_cache`, `prune_dead_caches`), the lock-retry loop, and `script/lint-audit` with its `run_capture`/`audit_output` plumbing — a container per run has its own cache and its own lock, so neither contamination nor contention exists to audit for. Update `script/bootstrap`'s docker-requirement text, which names "the Dockerfile's lint stage" throughout.
## Definition of done
Everything in the issue body, plus:
- Both traps above demonstrated, not asserted. Two consecutive `script/lint` runs on an unchanged tree, `--progress=plain`, showing the lint layer executing (not `CACHED`) on both.
- Negative control: introduce a deliberate lint violation, confirm the build fails citing that specific finding, revert, confirm clean.
- Withheld-`--build-arg` counterfactual: a bare `docker build -f Dockerfile.lint .` fails on the guard rather than exiting 0.
- `make check` and `script/cibuild` both green, evidence recorded on the PR.
- `TODO.md` updated in the same commit; markdown formatted with `make fmt`.
Do not run `docker builder prune` — the build cache is shared with other work on this host. Scope any invalidation with `--no-cache-filter=<stage>`.
Commit title ends ` (closes #113)`. [Issue #88](https://git.eeqj.de/sneak/vaultik/issues/88) and [issue #103](https://git.eeqj.de/sneak/vaultik/issues/103) describe defects in code this removes; leave them out of the commit message and they will be closed against this work separately.
Implementation plan, working on next in a private clone.
New Dockerfile.lint at the repo root, FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 (the Debian tag named by the ruling; digest confirmed pullable on this host). Layout: WORKDIR /src, COPY go.mod go.sum ./, RUN go mod download, COPY . ., then ARG CHECK_EPOCH, RUN [ -n "$CHECK_EPOCH" ] || exit 1, RUN echo "check epoch: ${CHECK_EPOCH}" && golangci-lint run --config .golangci.yml ./.... No default on the ARG; it sits below the module layers so dependency caching survives, and the value is expanded into the lint command itself rather than left to BuildKit's unreferenced-ARG handling.
script/lint reduces to epoch="$(date +%s%N)$$" as a bare assignment on its own line, then docker build --build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint ., plus a docker-availability check that fails with a readable message. It will reject arguments rather than silently ignore them (a build step cannot take linter flags); BUILDKIT_PROGRESS=plain is the supported way to see the layers execute, so no flag plumbing is needed.
Point 3 (config verify): omitting it, with the reason written into Dockerfile.lint — it is a live unpinned HTTPS fetch of a JSON schema, so it makes the gate network-dependent and converts an upstream outage or an egress-less runner into a red that is not a lint verdict.
Point 4 (script/lint-fix): keeping it, reimplemented as a bind-mounted docker run against the image reference parsed out of Dockerfile.lint's FROM line, with a header stating outright that it is a developer convenience, never a gate, and that it needs a local daemon.
Point 2 (Dockerfile lint stage): taking the recommended resolution. Delete the lint stage, ENV VAULTIK_LINT_IN_CONTAINER=1 and COPY --from=lint /src/go.sum /dev/null; move make fmt-check beside make test in the builder stage under the existing epoch guard; script/cibuild builds Dockerfile.lint with a fresh epoch and then Dockerfile with a second fresh epoch, failing on either. Dockerfile.lint becomes the single source of truth for the linter version. The consequence — script/docker builds the product image only and no longer lints, script/cibuild is the gate, script/check runs script/lint — gets stated in a comment in script/docker and in README.md, not left to be discovered.
Deletions: the native escape hatch and its version detection, VAULTIK_LINT_IN_CONTAINER in both files, cache_home / cache_dir / path_digest / prepare_cache / prune_dead_caches, the lock-retry loop, and script/lint-audit with its run_capture / audit_output plumbing. script/bootstrap's docker-requirement prose, the Makefiledeps comment and the README.md entrypoint text all stop naming "the Dockerfile's lint stage".
Tests: a parse-based guard beside the existing cmd/vaultik/makefile_test.go, asserting the invariants whose loss is silent — ARG CHECK_EPOCH present in Dockerfile.lint with no default and below go mod download, the non-empty guard present, the value expanded into the lint command, script/lint and script/cibuild computing the epoch as a bare assignment and passing it as --build-arg, and no VAULTIK_LINT_IN_CONTAINER or host-lint path left anywhere.
Evidence to be recorded on the PR: two consecutive script/lint runs on an unchanged tree with plain progress, both showing the lint layer executing rather than CACHED; a negative control with a deliberate violation, then reverted; a bare docker build -f Dockerfile.lint . failing on the guard instead of exiting 0; make check and script/cibuild green with output showing the layers ran. No docker builder prune — invalidation is scoped by the epoch alone.
One note for the record rather than a new issue: REPO_POLICIES.md in this repo still mandates the in-Dockerfile lint stage with COPY --from=lint /src/go.sum /dev/null. The ruling supersedes it here; the policy text is org-wide and out of scope for this change, so Dockerfile.lint will carry a comment saying so.
Implementation plan, working on `next` in a private clone.
**New `Dockerfile.lint`** at the repo root, `FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240` (the Debian tag named by the ruling; digest confirmed pullable on this host). Layout: `WORKDIR /src`, `COPY go.mod go.sum ./`, `RUN go mod download`, `COPY . .`, then `ARG CHECK_EPOCH`, `RUN [ -n "$CHECK_EPOCH" ] || exit 1`, `RUN echo "check epoch: ${CHECK_EPOCH}" && golangci-lint run --config .golangci.yml ./...`. No default on the `ARG`; it sits below the module layers so dependency caching survives, and the value is expanded into the lint command itself rather than left to BuildKit's unreferenced-`ARG` handling.
**`script/lint`** reduces to `epoch="$(date +%s%N)$$"` as a bare assignment on its own line, then `docker build --build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .`, plus a docker-availability check that fails with a readable message. It will reject arguments rather than silently ignore them (a build step cannot take linter flags); `BUILDKIT_PROGRESS=plain` is the supported way to see the layers execute, so no flag plumbing is needed.
**Point 3 (`config verify`): omitting it**, with the reason written into `Dockerfile.lint` — it is a live unpinned HTTPS fetch of a JSON schema, so it makes the gate network-dependent and converts an upstream outage or an egress-less runner into a red that is not a lint verdict.
**Point 4 (`script/lint-fix`): keeping it**, reimplemented as a bind-mounted `docker run` against the image reference parsed out of `Dockerfile.lint`'s `FROM` line, with a header stating outright that it is a developer convenience, never a gate, and that it needs a local daemon.
**Point 2 (`Dockerfile` lint stage): taking the recommended resolution.** Delete the lint stage, `ENV VAULTIK_LINT_IN_CONTAINER=1` and `COPY --from=lint /src/go.sum /dev/null`; move `make fmt-check` beside `make test` in the builder stage under the existing epoch guard; `script/cibuild` builds `Dockerfile.lint` with a fresh epoch and then `Dockerfile` with a second fresh epoch, failing on either. `Dockerfile.lint` becomes the single source of truth for the linter version. The consequence — `script/docker` builds the product image only and no longer lints, `script/cibuild` is the gate, `script/check` runs `script/lint` — gets stated in a comment in `script/docker` and in `README.md`, not left to be discovered.
**Deletions:** the native escape hatch and its version detection, `VAULTIK_LINT_IN_CONTAINER` in both files, `cache_home` / `cache_dir` / `path_digest` / `prepare_cache` / `prune_dead_caches`, the lock-retry loop, and `script/lint-audit` with its `run_capture` / `audit_output` plumbing. `script/bootstrap`'s docker-requirement prose, the `Makefile` `deps` comment and the `README.md` entrypoint text all stop naming "the Dockerfile's lint stage".
**Tests:** a parse-based guard beside the existing `cmd/vaultik/makefile_test.go`, asserting the invariants whose loss is silent — `ARG CHECK_EPOCH` present in `Dockerfile.lint` with no default and below `go mod download`, the non-empty guard present, the value expanded into the lint command, `script/lint` and `script/cibuild` computing the epoch as a bare assignment and passing it as `--build-arg`, and no `VAULTIK_LINT_IN_CONTAINER` or host-lint path left anywhere.
**Evidence to be recorded on the PR:** two consecutive `script/lint` runs on an unchanged tree with plain progress, both showing the lint layer executing rather than `CACHED`; a negative control with a deliberate violation, then reverted; a bare `docker build -f Dockerfile.lint .` failing on the guard instead of exiting 0; `make check` and `script/cibuild` green with output showing the layers ran. No `docker builder prune` — invalidation is scoped by the epoch alone.
One note for the record rather than a new issue: `REPO_POLICIES.md` in this repo still mandates the in-`Dockerfile` lint stage with `COPY --from=lint /src/go.sum /dev/null`. The ruling supersedes it here; the policy text is org-wide and out of scope for this change, so `Dockerfile.lint` will carry a comment saying so.
Landed on next as commit 739de1e, in pull request #114 (the next -> main PR for this cycle, opened from this commit as it is
the first since the last merge to main). Full verification output is
in the PR body.
What was built
New root Dockerfile.lint, FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240,
COPYing the repo in and running golangci-lint run --config .golangci.yml ./... as a build step. script/lint reduces to building it. That FROM line is now the only
pin of the linter version anywhere in the repo.
Trap 1 (a cached build lints nothing): ARG CHECK_EPOCH with no
default, below COPY go.mod go.sum / go mod download so dependency
layers still cache, RUN [ -n "$CHECK_EPOCH" ] || exit 1, and the value
expanded into the lint command. script/lint computes epoch="$(date +%s%N)$$" as a bare assignment on its own line.
Trap 2 (the Dockerfile lint stage): took the recommended resolution.
The lint stage, ENV VAULTIK_LINT_IN_CONTAINER=1 and COPY --from=lint /src/go.sum /dev/null are gone; make fmt-check
moved beside make test in the builder stage; script/cibuild builds Dockerfile.lint then Dockerfile, each with its own fresh epoch,
failing on either. The consequence is stated in comments in script/docker, in the Dockerfile header and in README.md: script/docker builds the product image only and does not lint, so script/check and script/cibuild are the gates.
Trap 3 (golangci-lint config verify): omitted, reason written into Dockerfile.lint. It fetches its JSON schema over an unpinned live
HTTPS call, which puts the gate's verdict at the mercy of a remote
resource outside this repo's hash-pinning discipline and turns an
upstream outage or an egress-less runner into a red that is not a lint
verdict. I did not test it offline, so I am not claiming it fails
offline — I am declining to add a network dependency to a gate. Note
that the config is not unvalidated in practice: golangci-lint run
rejects an unparseable or unknown-key config itself, at the gating
version.
Trap 4 (script/lint-fix): kept, reimplemented as a bind-mounted docker run against the image reference parsed out of Dockerfile.lint's FROM line. Its header states outright that it is a
developer convenience, never a gate, that no gate reads its exit status,
and that it needs a local daemon because a remote one cannot see these
files.
Deletions: the native escape hatch and installed_version / pinned_version / in_lint_container, VAULTIK_LINT_IN_CONTAINER in
both files, cache_home / cache_dir / path_digest / prepare_cache / prune_dead_caches, the lock-retry loop, and script/lint-audit with its run_capture / audit_output plumbing. script/bootstrap's docker-requirement prose, the Makefiledeps
comment and README.md no longer name "the Dockerfile's lint stage". script/lint now takes no arguments and says so, rather than dropping
flags a build step cannot accept.
cmd/vaultik/lintdocker_test.go parses both Dockerfiles and both
scripts and fails if any part of the mechanism is dropped — the digest
pin, the defaultless ARG below go mod download, the guard, the
expansion into each check command, the bare epoch assignment in both
scripts, cibuild building both files, and the absence of any host-lint
escape hatch. Every one of those losses is silent.
How it was verified
Two consecutive script/lint runs on a byte-identical tree
(git status --short captured before and after and compared), plain
progress: dependency layers CACHED in both, lint layer executing in
both with different epochs and ~55s of analysis each.
Negative control: reintroduced inline error handling in repoRoot;
the build failed citing cmd/vaultik/lintdocker_test.go:328:9: ... (noinlineerr) and script/lint exited 1. Reverted, re-ran, 0 issues., exit 0. That
finding was a real one this gate caught while the test file was being
written, not a staged one.
Withheld --build-arg: bare docker build -f Dockerfile.lint . fails
at RUN [ -n "$CHECK_EPOCH" ] || exit 1, and does so on both of two
consecutive attempts (failed steps are never cached), rather than
exiting 0.
The guard test itself demonstrated effective: changing ARG CHECK_EPOCH to ARG CHECK_EPOCH=constant makes TestLintDockerfileCannotBeCachedGreen fail.
make check green in 1m20s, script/cibuild green in 3m0s, both with
the check layers visibly executing and no (cached) test lines.
script/lint-fix smoke-tested: 0 issues., exit 0, tree unchanged.
No docker builder prune at any point; every invalidation was scoped by CHECK_EPOCH.
Two things flagged rather than fixed: REPO_POLICIES.md still mandates
the in-Dockerfile lint stage, which this supersedes for this repo (the
policy text is org-wide, so it was left alone, with a comment in Dockerfile.lint pointing at the divergence), and golangci-lint
prints a gomodguard deprecation warning on every run, which is a .golangci.yml matter and out of scope here.
Landed on `next` as commit `739de1e`, in
[pull request #114](https://git.eeqj.de/sneak/vaultik/pulls/114) (the
`next` -> `main` PR for this cycle, opened from this commit as it is
the first since the last merge to `main`). Full verification output is
in the PR body.
## What was built
New root `Dockerfile.lint`,
`FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`,
COPYing the repo in and running
`golangci-lint run --config .golangci.yml ./...` as a build step.
`script/lint` reduces to building it. That `FROM` line is now the only
pin of the linter version anywhere in the repo.
Trap 1 (a cached build lints nothing): `ARG CHECK_EPOCH` with no
default, below `COPY go.mod go.sum` / `go mod download` so dependency
layers still cache, `RUN [ -n "$CHECK_EPOCH" ] || exit 1`, and the value
expanded into the lint command. `script/lint` computes
`epoch="$(date +%s%N)$$"` as a bare assignment on its own line.
Trap 2 (the `Dockerfile` lint stage): took the recommended resolution.
The lint stage, `ENV VAULTIK_LINT_IN_CONTAINER=1` and
`COPY --from=lint /src/go.sum /dev/null` are gone; `make fmt-check`
moved beside `make test` in the builder stage; `script/cibuild` builds
`Dockerfile.lint` then `Dockerfile`, each with its own fresh epoch,
failing on either. The consequence is stated in comments in
`script/docker`, in the `Dockerfile` header and in `README.md`:
`script/docker` builds the product image only and does not lint, so
`script/check` and `script/cibuild` are the gates.
Trap 3 (`golangci-lint config verify`): **omitted**, reason written into
`Dockerfile.lint`. It fetches its JSON schema over an unpinned live
HTTPS call, which puts the gate's verdict at the mercy of a remote
resource outside this repo's hash-pinning discipline and turns an
upstream outage or an egress-less runner into a red that is not a lint
verdict. I did not test it offline, so I am not claiming it fails
offline — I am declining to add a network dependency to a gate. Note
that the config is not unvalidated in practice: `golangci-lint run`
rejects an unparseable or unknown-key config itself, at the gating
version.
Trap 4 (`script/lint-fix`): **kept**, reimplemented as a bind-mounted
`docker run` against the image reference parsed out of
`Dockerfile.lint`'s `FROM` line. Its header states outright that it is a
developer convenience, never a gate, that no gate reads its exit status,
and that it needs a local daemon because a remote one cannot see these
files.
Deletions: the native escape hatch and `installed_version` /
`pinned_version` / `in_lint_container`, `VAULTIK_LINT_IN_CONTAINER` in
both files, `cache_home` / `cache_dir` / `path_digest` /
`prepare_cache` / `prune_dead_caches`, the lock-retry loop, and
`script/lint-audit` with its `run_capture` / `audit_output` plumbing.
`script/bootstrap`'s docker-requirement prose, the `Makefile` `deps`
comment and `README.md` no longer name "the Dockerfile's lint stage".
`script/lint` now takes no arguments and says so, rather than dropping
flags a build step cannot accept.
`cmd/vaultik/lintdocker_test.go` parses both Dockerfiles and both
scripts and fails if any part of the mechanism is dropped — the digest
pin, the defaultless `ARG` below `go mod download`, the guard, the
expansion into each check command, the bare epoch assignment in both
scripts, `cibuild` building both files, and the absence of any host-lint
escape hatch. Every one of those losses is silent.
## How it was verified
- Two consecutive `script/lint` runs on a byte-identical tree
(`git status --short` captured before and after and compared), plain
progress: dependency layers `CACHED` in both, lint layer executing in
both with different epochs and ~55s of analysis each.
- Negative control: reintroduced inline error handling in `repoRoot`;
the build failed citing
`cmd/vaultik/lintdocker_test.go:328:9: ... (noinlineerr)` and
`script/lint` exited 1. Reverted, re-ran, `0 issues.`, exit 0. That
finding was a real one this gate caught while the test file was being
written, not a staged one.
- Withheld `--build-arg`: bare `docker build -f Dockerfile.lint .` fails
at `RUN [ -n "$CHECK_EPOCH" ] || exit 1`, and does so on both of two
consecutive attempts (failed steps are never cached), rather than
exiting 0.
- The guard test itself demonstrated effective: changing
`ARG CHECK_EPOCH` to `ARG CHECK_EPOCH=constant` makes
`TestLintDockerfileCannotBeCachedGreen` fail.
- `make check` green in 1m20s, `script/cibuild` green in 3m0s, both with
the check layers visibly executing and no `(cached)` test lines.
- `script/lint-fix` smoke-tested: `0 issues.`, exit 0, tree unchanged.
No `docker builder prune` at any point; every invalidation was scoped by
`CHECK_EPOCH`.
Two things flagged rather than fixed: `REPO_POLICIES.md` still mandates
the in-`Dockerfile` lint stage, which this supersedes for this repo (the
policy text is org-wide, so it was left alone, with a comment in
`Dockerfile.lint` pointing at the divergence), and `golangci-lint`
prints a `gomodguard` deprecation warning on every run, which is a
`.golangci.yml` matter and out of scope here.
Correction to my earlier comment on this issue, after review failed pull request #114. next
is now at d257f8f (the same commit, amended).
I wrote that golangci-lint config verify was omitted because it
fetches its JSON schema over an unpinned live HTTPS call, and that golangci-lint run rejects an unknown-key config itself. Both claims
were false at the pinned version, and I did not test either before
writing them. The point-3 note in this issue asked for the offline
test as the condition for including it; I did not run it, and declining
on an untested premise was the error.
Reproduced at golangci/golangci-lint:v2.12.2@sha256:5cceeef0..., network genuinely
off:
$ docker run --rm --network none ... golangci-lint config verify --config .golangci.yml
exit=0
$ docker run --rm --network none ... golangci-lint config verify --config <same file, `linters:` renamed `linterz:`>
jsonschema: "" does not validate with "/additionalProperties": additional properties 'linterz' not allowed
The command is terminated due to an error: the configuration contains invalid elements
exit=3
The schema is embedded; it validates offline and rejects the typo
offline. And golangci-lint run catches YAML it cannot parse but
silently ignores an unknown top-level key, so the omission left a live
false green in the gate's own config: with one planted over-length
comment line and nothing else changed, script/lint exited 1 naming a revive finding under linters: and exited 0 with 0 issues. under linterz:, in a run whose lint layer executed for 49s. default: all,
the disable list and every threshold were silently discarded.
config verify now runs as its own CHECK_EPOCH-keyed layer above the
lint, and the comment block in Dockerfile.lint records what was
measured rather than the retracted rationale. The other rework item was
the host-lint guard test, which asserted the absence of one retired
variable name and so could never fire again; it now asserts the property
structurally — no script invokes golangci-lint except through docker — and was mutation-proved against a renamed escape hatch.
Full verbatim evidence for both is on pull request #114. make check and script/cibuild are green with the check layers
demonstrably executing.
Correction to my earlier comment on this issue, after review failed
[pull request #114](https://git.eeqj.de/sneak/vaultik/pulls/114). `next`
is now at `d257f8f` (the same commit, amended).
I wrote that `golangci-lint config verify` was **omitted** because it
fetches its JSON schema over an unpinned live HTTPS call, and that
`golangci-lint run` rejects an unknown-key config itself. **Both claims
were false at the pinned version, and I did not test either before
writing them.** The point-3 note in this issue asked for the offline
test as the condition for including it; I did not run it, and declining
on an untested premise was the error.
Reproduced at
`golangci/golangci-lint:v2.12.2@sha256:5cceeef0...`, network genuinely
off:
```
$ docker run --rm --network none ... golangci-lint config verify --config .golangci.yml
exit=0
$ docker run --rm --network none ... golangci-lint config verify --config <same file, `linters:` renamed `linterz:`>
jsonschema: "" does not validate with "/additionalProperties": additional properties 'linterz' not allowed
The command is terminated due to an error: the configuration contains invalid elements
exit=3
```
The schema is embedded; it validates offline and rejects the typo
offline. And `golangci-lint run` catches YAML it cannot *parse* but
silently ignores an unknown top-level *key*, so the omission left a live
false green in the gate's own config: with one planted over-length
comment line and nothing else changed, `script/lint` exited 1 naming a
`revive` finding under `linters:` and exited 0 with `0 issues.` under
`linterz:`, in a run whose lint layer executed for 49s. `default: all`,
the disable list and every threshold were silently discarded.
`config verify` now runs as its own `CHECK_EPOCH`-keyed layer above the
lint, and the comment block in `Dockerfile.lint` records what was
measured rather than the retracted rationale. The other rework item was
the host-lint guard test, which asserted the absence of one retired
variable name and so could never fire again; it now asserts the property
structurally — no script invokes `golangci-lint` except through
`docker` — and was mutation-proved against a renamed escape hatch.
Full verbatim evidence for both is on
[pull request #114](https://git.eeqj.de/sneak/vaultik/pulls/114).
`make check` and `script/cibuild` are green with the check layers
demonstrably executing.
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.
Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the
script/entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.Reference implementation is
sneak/homoicon— copy its shape: a rootDockerfile.lintbuiltFROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which COPYs the repo in and runsgolangci-lint run --config .golangci.yml ./...as a build step, withscript/lintreduced to building it. Linting as a build step means a successful build IS a clean lint, and it works even where the docker daemon is remote and bind mounts are impossible.This supersedes the per-worktree cache-isolation approach previously scoped here. The controlled experiment run in this repo showed isolation fixes contamination but NOT lock contention — two concurrent runs with entirely separate cache directories still collided. A container per run has its own cache and its own lock, so the whole class goes away. Caching is explicitly waived by the owner.
Two things to get right, both of which would otherwise ship a false green:
docker build -f Dockerfile.lint .on an unchanged tree returns success in well under a second having run no linter. Force the lint layers to execute.golangci-lint config verifyfetches its JSON schema over an unpinned live HTTPS call, making lint network-dependent and breaking hash-pinning. Decide deliberately whether to include it.Also remove the golangci-lint install path from
script/bootstrapand the native escape hatch inscript/lint— nothing runs on the host any more, which also closes the context-gating problem raised earlier.Definition of done
script/lintruns the linter only in Docker; no host golangci-lint path remains.script/lintruns on an unchanged tree both demonstrably execute the linter.make checkstill green.Canonical tracking issue: sneak/prompts#40
Implementation requirements. Work lands on
next; anext->mainPR is opened from the first commit of this cycle.Shape
New root
Dockerfile.lint,FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240(the Debian-based tag, exactly as the owner's ruling names it — not the-alpinevariant currently inDockerfile). It COPYs the repo in and runsgolangci-lint run --config .golangci.yml ./...as a build step.script/lintreduces to building it.Four things this repo has to get right that the reference does not cover
1. A cached build lints nothing. The homoicon reference has no cache-busting; on an unchanged tree its
docker buildexits 0 in under a second having run no linter. Do not copy that. Reuse the mechanism this repo already established for exactly this inDockerfileandscript/cibuild:ARG CHECK_EPOCHimmediately above the lintRUNs, aRUN [ -n "$CHECK_EPOCH" ] || exit 1guard, the value expanded into the check command itself, no default value, andscript/lintcomputingepoch="$(date +%s%N)$$"as a bare assignment on its own line (inline in the argument, a failing substitution does not abort underset -euand yields a constant empty epoch — the exact false green being prevented). TheARGgoes belowCOPY go.mod go.sum/go mod downloadso dependency layers still cache.2. The
Dockerfilelint stage cannot survive as-is. It runsmake lint, which will now shell out todocker build— docker-in-docker, inside a BuildKit step, with no daemon. Recommended resolution: delete the lint stage fromDockerfileentirely (along withENV VAULTIK_LINT_IN_CONTAINER=1and the builder'sCOPY --from=lint /src/go.sum /dev/null), movemake fmt-checknext tomake testin the builder stage, and havescript/cibuildbuildDockerfile.lintfirst with a fresh epoch and thenDockerfilewith a fresh epoch, failing on either. Rationale:Dockerfile.lintthen becomes the single source of truth for the linter version. The alternative — keeping a lint stage that callsgolangci-lintdirectly — reintroduces two independently-bumpable digest pins, which is the drift issue #78 was filed over. If you take the alternative, say why in the PR body. Note the consequence either way:script/dockerbuilds only the product image and so no longer lints;script/cibuildis the gate andscript/checkrunsscript/lint. Say that plainly in a comment rather than leaving it to be discovered.3.
golangci-lint config verifyfetches its JSON schema over an unpinned live HTTPS call. Recommendation: omit it, and say inDockerfile.lintwhy. It makes the lint gate network-dependent on an unpinned remote resource, against the hash-pinning policy, and an upstream outage or an egress-less runner then produces a red that is not a lint verdict — the false-red class this repo has spent several issues eliminating. Include it only on evidence that it runs fully offline at this pinned version; if you test that, test it with the network actually off (docker run --network none) and record the output.4.
script/lint-fixcannot delegate to a build step — a build cannot write fixes back to the host tree. Recommendation: keep it, reimplemented as a bind-mounteddocker runagainst the image reference parsed out ofDockerfile.lint, with a comment stating outright that it is a developer convenience, never a gate, and that it needs a local daemon. It consults no exit code that any gate reads. Deleting it andmake lint-fixis acceptable if you prefer —lint-fixis not in the canonical scripts-to-rule-them-all set — but then say so in the PR body.Deletions
Everything on the host lint path goes: the native escape hatch and its version detection in
script/lint,VAULTIK_LINT_IN_CONTAINERin both files, the per-worktree cache directory machinery (cache_home,cache_dir,path_digest,prepare_cache,prune_dead_caches), the lock-retry loop, andscript/lint-auditwith itsrun_capture/audit_outputplumbing — a container per run has its own cache and its own lock, so neither contamination nor contention exists to audit for. Updatescript/bootstrap's docker-requirement text, which names "the Dockerfile's lint stage" throughout.Definition of done
Everything in the issue body, plus:
script/lintruns on an unchanged tree,--progress=plain, showing the lint layer executing (notCACHED) on both.--build-argcounterfactual: a baredocker build -f Dockerfile.lint .fails on the guard rather than exiting 0.make checkandscript/cibuildboth green, evidence recorded on the PR.TODO.mdupdated in the same commit; markdown formatted withmake fmt.Do not run
docker builder prune— the build cache is shared with other work on this host. Scope any invalidation with--no-cache-filter=<stage>.Commit title ends
(closes #113). Issue #88 and issue #103 describe defects in code this removes; leave them out of the commit message and they will be closed against this work separately.Implementation plan, working on
nextin a private clone.New
Dockerfile.lintat the repo root,FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240(the Debian tag named by the ruling; digest confirmed pullable on this host). Layout:WORKDIR /src,COPY go.mod go.sum ./,RUN go mod download,COPY . ., thenARG CHECK_EPOCH,RUN [ -n "$CHECK_EPOCH" ] || exit 1,RUN echo "check epoch: ${CHECK_EPOCH}" && golangci-lint run --config .golangci.yml ./.... No default on theARG; it sits below the module layers so dependency caching survives, and the value is expanded into the lint command itself rather than left to BuildKit's unreferenced-ARGhandling.script/lintreduces toepoch="$(date +%s%N)$$"as a bare assignment on its own line, thendocker build --build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint ., plus a docker-availability check that fails with a readable message. It will reject arguments rather than silently ignore them (a build step cannot take linter flags);BUILDKIT_PROGRESS=plainis the supported way to see the layers execute, so no flag plumbing is needed.Point 3 (
config verify): omitting it, with the reason written intoDockerfile.lint— it is a live unpinned HTTPS fetch of a JSON schema, so it makes the gate network-dependent and converts an upstream outage or an egress-less runner into a red that is not a lint verdict.Point 4 (
script/lint-fix): keeping it, reimplemented as a bind-mounteddocker runagainst the image reference parsed out ofDockerfile.lint'sFROMline, with a header stating outright that it is a developer convenience, never a gate, and that it needs a local daemon.Point 2 (
Dockerfilelint stage): taking the recommended resolution. Delete the lint stage,ENV VAULTIK_LINT_IN_CONTAINER=1andCOPY --from=lint /src/go.sum /dev/null; movemake fmt-checkbesidemake testin the builder stage under the existing epoch guard;script/cibuildbuildsDockerfile.lintwith a fresh epoch and thenDockerfilewith a second fresh epoch, failing on either.Dockerfile.lintbecomes the single source of truth for the linter version. The consequence —script/dockerbuilds the product image only and no longer lints,script/cibuildis the gate,script/checkrunsscript/lint— gets stated in a comment inscript/dockerand inREADME.md, not left to be discovered.Deletions: the native escape hatch and its version detection,
VAULTIK_LINT_IN_CONTAINERin both files,cache_home/cache_dir/path_digest/prepare_cache/prune_dead_caches, the lock-retry loop, andscript/lint-auditwith itsrun_capture/audit_outputplumbing.script/bootstrap's docker-requirement prose, theMakefiledepscomment and theREADME.mdentrypoint text all stop naming "the Dockerfile's lint stage".Tests: a parse-based guard beside the existing
cmd/vaultik/makefile_test.go, asserting the invariants whose loss is silent —ARG CHECK_EPOCHpresent inDockerfile.lintwith no default and belowgo mod download, the non-empty guard present, the value expanded into the lint command,script/lintandscript/cibuildcomputing the epoch as a bare assignment and passing it as--build-arg, and noVAULTIK_LINT_IN_CONTAINERor host-lint path left anywhere.Evidence to be recorded on the PR: two consecutive
script/lintruns on an unchanged tree with plain progress, both showing the lint layer executing rather thanCACHED; a negative control with a deliberate violation, then reverted; a baredocker build -f Dockerfile.lint .failing on the guard instead of exiting 0;make checkandscript/cibuildgreen with output showing the layers ran. Nodocker builder prune— invalidation is scoped by the epoch alone.One note for the record rather than a new issue:
REPO_POLICIES.mdin this repo still mandates the in-Dockerfilelint stage withCOPY --from=lint /src/go.sum /dev/null. The ruling supersedes it here; the policy text is org-wide and out of scope for this change, soDockerfile.lintwill carry a comment saying so.Landed on
nextas commit739de1e, inpull request #114 (the
next->mainPR for this cycle, opened from this commit as it isthe first since the last merge to
main). Full verification output isin the PR body.
What was built
New root
Dockerfile.lint,FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240,COPYing the repo in and running
golangci-lint run --config .golangci.yml ./...as a build step.script/lintreduces to building it. ThatFROMline is now the onlypin of the linter version anywhere in the repo.
Trap 1 (a cached build lints nothing):
ARG CHECK_EPOCHwith nodefault, below
COPY go.mod go.sum/go mod downloadso dependencylayers still cache,
RUN [ -n "$CHECK_EPOCH" ] || exit 1, and the valueexpanded into the lint command.
script/lintcomputesepoch="$(date +%s%N)$$"as a bare assignment on its own line.Trap 2 (the
Dockerfilelint stage): took the recommended resolution.The lint stage,
ENV VAULTIK_LINT_IN_CONTAINER=1andCOPY --from=lint /src/go.sum /dev/nullare gone;make fmt-checkmoved beside
make testin the builder stage;script/cibuildbuildsDockerfile.lintthenDockerfile, each with its own fresh epoch,failing on either. The consequence is stated in comments in
script/docker, in theDockerfileheader and inREADME.md:script/dockerbuilds the product image only and does not lint, soscript/checkandscript/cibuildare the gates.Trap 3 (
golangci-lint config verify): omitted, reason written intoDockerfile.lint. It fetches its JSON schema over an unpinned liveHTTPS call, which puts the gate's verdict at the mercy of a remote
resource outside this repo's hash-pinning discipline and turns an
upstream outage or an egress-less runner into a red that is not a lint
verdict. I did not test it offline, so I am not claiming it fails
offline — I am declining to add a network dependency to a gate. Note
that the config is not unvalidated in practice:
golangci-lint runrejects an unparseable or unknown-key config itself, at the gating
version.
Trap 4 (
script/lint-fix): kept, reimplemented as a bind-mounteddocker runagainst the image reference parsed out ofDockerfile.lint'sFROMline. Its header states outright that it is adeveloper convenience, never a gate, that no gate reads its exit status,
and that it needs a local daemon because a remote one cannot see these
files.
Deletions: the native escape hatch and
installed_version/pinned_version/in_lint_container,VAULTIK_LINT_IN_CONTAINERinboth files,
cache_home/cache_dir/path_digest/prepare_cache/prune_dead_caches, the lock-retry loop, andscript/lint-auditwith itsrun_capture/audit_outputplumbing.script/bootstrap's docker-requirement prose, theMakefiledepscomment and
README.mdno longer name "the Dockerfile's lint stage".script/lintnow takes no arguments and says so, rather than droppingflags a build step cannot accept.
cmd/vaultik/lintdocker_test.goparses both Dockerfiles and bothscripts and fails if any part of the mechanism is dropped — the digest
pin, the defaultless
ARGbelowgo mod download, the guard, theexpansion into each check command, the bare epoch assignment in both
scripts,
cibuildbuilding both files, and the absence of any host-lintescape hatch. Every one of those losses is silent.
How it was verified
script/lintruns on a byte-identical tree(
git status --shortcaptured before and after and compared), plainprogress: dependency layers
CACHEDin both, lint layer executing inboth with different epochs and ~55s of analysis each.
repoRoot;the build failed citing
cmd/vaultik/lintdocker_test.go:328:9: ... (noinlineerr)andscript/lintexited 1. Reverted, re-ran,0 issues., exit 0. Thatfinding was a real one this gate caught while the test file was being
written, not a staged one.
--build-arg: baredocker build -f Dockerfile.lint .failsat
RUN [ -n "$CHECK_EPOCH" ] || exit 1, and does so on both of twoconsecutive attempts (failed steps are never cached), rather than
exiting 0.
ARG CHECK_EPOCHtoARG CHECK_EPOCH=constantmakesTestLintDockerfileCannotBeCachedGreenfail.make checkgreen in 1m20s,script/cibuildgreen in 3m0s, both withthe check layers visibly executing and no
(cached)test lines.script/lint-fixsmoke-tested:0 issues., exit 0, tree unchanged.No
docker builder pruneat any point; every invalidation was scoped byCHECK_EPOCH.Two things flagged rather than fixed:
REPO_POLICIES.mdstill mandatesthe in-
Dockerfilelint stage, which this supersedes for this repo (thepolicy text is org-wide, so it was left alone, with a comment in
Dockerfile.lintpointing at the divergence), andgolangci-lintprints a
gomodguarddeprecation warning on every run, which is a.golangci.ymlmatter and out of scope here.Correction to my earlier comment on this issue, after review failed
pull request #114.
nextis now at
d257f8f(the same commit, amended).I wrote that
golangci-lint config verifywas omitted because itfetches its JSON schema over an unpinned live HTTPS call, and that
golangci-lint runrejects an unknown-key config itself. Both claimswere false at the pinned version, and I did not test either before
writing them. The point-3 note in this issue asked for the offline
test as the condition for including it; I did not run it, and declining
on an untested premise was the error.
Reproduced at
golangci/golangci-lint:v2.12.2@sha256:5cceeef0..., network genuinelyoff:
The schema is embedded; it validates offline and rejects the typo
offline. And
golangci-lint runcatches YAML it cannot parse butsilently ignores an unknown top-level key, so the omission left a live
false green in the gate's own config: with one planted over-length
comment line and nothing else changed,
script/lintexited 1 naming arevivefinding underlinters:and exited 0 with0 issues.underlinterz:, in a run whose lint layer executed for 49s.default: all,the disable list and every threshold were silently discarded.
config verifynow runs as its ownCHECK_EPOCH-keyed layer above thelint, and the comment block in
Dockerfile.lintrecords what wasmeasured rather than the retracted rationale. The other rework item was
the host-lint guard test, which asserted the absence of one retired
variable name and so could never fire again; it now asserts the property
structurally — no script invokes
golangci-lintexcept throughdocker— and was mutation-proved against a renamed escape hatch.Full verbatim evidence for both is on
pull request #114.
make checkandscript/cibuildare green with the check layersdemonstrably executing.