Re-vendor the canonical files from sneak/prompts at dd4027b (closes #213)
check / check (push) Successful in 16m13s

Linting and testing become the lint and test phases of the Dockerfile,
and the build stage depends on both. Dockerfile.lint, CHECK_EPOCH and
the tests that checked them are removed. Every docker build in script/
passes --no-cache, and script/cibuild runs script/bootstrap first, which
now fetches apt package lists so a fresh CI runner can install Go. The
image takes its version from the VERSION build arg or git describe, dev
without .git, and still stamps the commit and its date. This repo's own
entries follow the canonical content in .gitignore and .editorconfig.
The golangci-lint v2.14.0 findings are fixed in the code. The rules in
CLAUDE.md move into AGENTS.md. IsDevVersion now counts "unknown".

Model: opus-5-5
This commit is contained in:
2026-10-06 03:00:12 +00:00
parent 35cf985c18
commit dcf5f7555b
36 changed files with 822 additions and 1190 deletions
+13 -98
View File
@@ -1,108 +1,23 @@
#!/bin/sh
# script/lint: run the linter.
# script/lint: run the linter. Linting is a phase of the Dockerfile and
# this builds that phase alone; the linter is never installed or run on
# a developer host, where a shared result cache and a host-global lock
# make its answer untrustworthy.
#
# The linter runs inside the image built by Dockerfile.lint, and it runs
# there as a BUILD STEP: a successful build of that file IS a clean
# lint. Nothing lints on the host, at any version, ever. That FROM line
# is the single source of truth for the linter version in this repo, so
# a local run and a CI run of the same tree cannot disagree.
#
# One container per run means one lint cache and one golangci-lint lock
# per run, both private to that run and thrown away with it. That is
# what makes concurrent runs on a shared host safe, and it is why this
# script no longer carries per-worktree cache directories, a lock-retry
# loop, or an output audit: there is no shared state left for them to
# defend (issue https://git.eeqj.de/sneak/vaultik/issues/113).
#
# To watch the linter execute, set BUILDKIT_PROGRESS=plain, which docker
# honours directly:
#
# BUILDKIT_PROGRESS=plain script/lint
#
# The check layers -- `golangci-lint config verify` and then
# `golangci-lint run` -- must appear as executing rather than CACHED on
# every run; see the CHECK_EPOCH comment in Dockerfile.lint.
# The phase is not the last stage in the file, so it is built only when
# --target names it. --no-cache because a cached lint layer is a lint
# that did not run. The tag makes each build replace the previous image
# instead of leaving a dangling one behind.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
DOCKERFILE="$ROOT/Dockerfile.lint"
require_docker() {
if ! command -v docker >/dev/null 2>&1; then
cat >&2 <<EOF
lint: docker is required to run the pinned linter.
lint image declared by: $DOCKERFILE
Install docker. Linting with any other golangci-lint is not supported:
it is what lets a local run pass while CI fails. A golangci-lint on
PATH is never used, whatever its version.
EOF
exit 1
fi
if ! docker info >/dev/null 2>&1; then
cat >&2 <<EOF
lint: the docker daemon is not reachable, so the pinned linter cannot
run.
lint image declared by: $DOCKERFILE
Start the daemon (and check DOCKER_HOST / your group membership). This
script will not fall back to a different linter version or to an
unpinned binary on PATH.
EOF
exit 1
fi
}
usage() {
cat >&2 <<EOF
usage: $(basename "$0")
script/lint takes no arguments. The linter runs as a build step, so
there is no command line to pass flags to; anything accepted here would
have to be silently dropped. To apply autofixes, use script/lint-fix,
which runs the same pinned image as a container for exactly this
reason.
EOF
exit 2
}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
[ "$#" -eq 0 ] || usage
cd "$ROOT"
require_docker
# A fresh epoch per invocation is what forces the check layers to
# execute; the layers above the ARG in Dockerfile.lint still cache,
# so a run is not cold. The value must be unique per invocation, not
# per second: `date +%s` is second-granular, so two concurrent
# invocations in the same second would get identical epochs and the
# later one could be served from cache -- the false green in
# miniature. `%N` alone does not fix it either, because 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 it 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 epoch
# is exactly the cached-lint false green this guards against. As a
# bare assignment, `set -e` catches a failing `date` and no build
# starts.
epoch="$(date +%s%N)$$"
# cacheonly: the lint verdict is the build's exit status, and the
# image it would otherwise produce is never run. Exporting it costs
# most of the wall time of a warm run and leaves a dangling image
# behind on every invocation, on a host that may be running many.
docker build \
--output=type=cacheonly \
--build-arg CHECK_EPOCH="$epoch" \
-f "$DOCKERFILE" \
"$ROOT"
docker build --no-cache \
--target lint \
-t "$("$SCRIPT_DIR/projectname")-lint" .
}
main "$@"