All checks were successful
check / check (pull_request) Successful in 2m58s
Every lint run now happens inside its own container, invoked through script/lint, and linting is a build step rather than a container command: a successful build of the new root Dockerfile.lint IS a clean lint. That shape also works where the docker daemon is remote and bind mounts are impossible. Its FROM line -- golangci/golangci-lint:v2.12.2, pinned by digest -- is now the only pin of the linter version in this repo. A container per run has its own lint cache and its own golangci-lint lock, both discarded with it, so neither cross-worktree contamination nor lock contention exists any more. The machinery that defended against them is therefore gone: the per-worktree cache directories, the lock-retry loop, and script/lint-audit, which existed to catch findings replayed from a cache that no longer exists. So is the host lint path in its entirety -- the native escape hatch, its version detection, and VAULTIK_LINT_IN_CONTAINER in both script/lint and the Dockerfile. Nothing lints on the host, at any version. A cached build lints nothing, so the CHECK_EPOCH mechanism the product Dockerfile already used is what makes a green mean something: ARG CHECK_EPOCH with no default, placed below the module layers so dependency caching survives, a `RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard so a build that withholds the arg fails instead of replaying, and the value expanded into the lint command itself. script/lint computes `epoch="$(date +%s%N)$$"` as a bare assignment on its own line, because inline in the argument a failing substitution does not abort under `set -eu` and yields a constant empty epoch -- which is exactly the false green being prevented. The product Dockerfile loses its lint stage rather than gaining a second linter pin. That stage ran `make lint`, which is now `docker build`: docker-in-docker inside a BuildKit step with no daemon. Calling golangci-lint directly there instead would have meant two independently bumpable digests for one tool. `make fmt-check` moves beside `make test` in the builder stage, and script/cibuild now builds Dockerfile.lint and then Dockerfile, each with its own fresh epoch, failing on either. Consequence, stated in comments rather than left to be discovered: script/docker builds the product image only and no longer lints; script/check and script/cibuild are the gates. `golangci-lint config verify` runs as its own epoch-keyed layer, above the lint. It is not belt-and-braces: `golangci-lint run` rejects a config it cannot PARSE but silently IGNORES an unknown top-level KEY. Renaming .golangci.yml's `linters:` to `linterz:` -- one character -- discards `default: all`, the disable list and every threshold, leaves only the small default linter set running, and exits 0 reporting `0 issues.` on a tree the real config fails with an lll finding, in a run whose lint layer demonstrably executed. That is a set-but- ineffective config falling back to defaults instead of failing loudly, sitting in the gate's own configuration. `config verify` catches it and does so with the network genuinely off at this pin: under `docker run --network none` against the pinned digest it exits 0 on this repo's config and exits 3 on the `linterz:` variant. It is keyed on CHECK_EPOCH like the lint itself, because a cached validation validates nothing. script/lint-fix is kept, reimplemented as a bind-mounted docker run against the image parsed out of Dockerfile.lint -- a build step cannot write fixes back to the worktree -- and its header states outright that it is a developer convenience, never a gate, and needs a local daemon. 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 emptiness guard, the expansion of the epoch into each check command, the bare per-invocation epoch assignment in both scripts, cibuild building both files, the config verification running before the lint, and -- structurally, not by searching for one retired variable name -- that no script invokes golangci-lint except through docker. Every one of those losses is silent: the build still exits 0 and nothing is checked, which is why they are asserted rather than trusted. The scanner behind the last of those has its own test, because a structural check that goes blind passes on every tree, including a broken one. script/lint takes no arguments now, and says so instead of dropping them: a build step has no command line to pass linter flags to.
64 lines
2.8 KiB
Bash
Executable File
64 lines
2.8 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/cibuild: run the CI build. This is the full gate, and it is two
|
|
# builds, in this order:
|
|
#
|
|
# Dockerfile.lint the linter, as a build step (a clean build IS a
|
|
# clean lint)
|
|
# Dockerfile `make fmt-check` and `make test` in the builder
|
|
# stage, then the product image
|
|
#
|
|
# Either one failing fails this script. Note what follows from the
|
|
# split: script/docker builds only the product image and so no longer
|
|
# lints -- this script and script/check (which runs script/lint) are the
|
|
# things that decide whether the tree is clean.
|
|
#
|
|
# Generic apart from the two Dockerfiles: the Gitea workflow runs this
|
|
# on push.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
# Both Dockerfiles key their check layers on CHECK_EPOCH, so a fresh
|
|
# value is what forces those layers to re-run: without it an
|
|
# unchanged tree replays them from cache, the checks never execute,
|
|
# and the build still exits 0. Each ARG sits immediately above the
|
|
# check RUNs, so dependency and module layers still cache. Both
|
|
# Dockerfiles also refuse to build at all when CHECK_EPOCH is empty,
|
|
# so a missing value fails loudly here rather than passing quietly.
|
|
#
|
|
# The value must be unique per invocation, not per second. `date +%s`
|
|
# is second-granular, so two concurrent invocations in the same
|
|
# second get identical epochs and the later one can be served from
|
|
# cache -- the original defect in miniature. `%N` alone does not fix
|
|
# it: busybox silently drops %N, exits 0, and hands back second
|
|
# granularity with no warning. `$$` is what makes this correct
|
|
# regardless, since concurrent invocations have different pids.
|
|
#
|
|
# Assign the epoch on its own line rather than inline in the
|
|
# argument. Under `set -eu` a command substitution that fails
|
|
# inside an argument does NOT abort the script: CHECK_EPOCH would
|
|
# become an empty string, an empty string is a constant, and a
|
|
# constant CHECK_EPOCH is exactly the cached-check false green this
|
|
# script exists to prevent -- so the guard would disarm itself and
|
|
# still exit 0. As a bare assignment, `set -e` catches a failing
|
|
# `date` and no build starts.
|
|
#
|
|
# A separate value per build, because they are separate builds: one
|
|
# `date` shared between them would still be fresh, but reusing it
|
|
# invites the two to be collapsed into a single value that is
|
|
# computed somewhere else and passed in.
|
|
epoch="$(date +%s%N)$$"
|
|
# cacheonly for the lint build: its verdict is the exit status and
|
|
# the image is never run, so exporting it is pure cost. See
|
|
# script/lint.
|
|
docker build --output=type=cacheonly \
|
|
--build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .
|
|
|
|
epoch="$(date +%s%N)$$"
|
|
docker build --build-arg CHECK_EPOCH="$epoch" .
|
|
}
|
|
|
|
main "$@"
|