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
+25 -19
View File
@@ -38,7 +38,12 @@ pkg_install() {
detect_pkgmgr
case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;;
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
# A fresh image, such as the CI runner's, has no package lists,
# so apt-get install finds nothing until they are fetched.
apt)
$SUDO apt-get update
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
;;
brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;;
esac
@@ -48,12 +53,12 @@ missing() {
! command -v "$1" >/dev/null 2>&1
}
# Docker is a hard requirement, not a nice-to-have: script/lint lints by
# building Dockerfile.lint, whose digest-pinned golangci-lint image is
# the only place the linter runs, and script/check and script/precommit
# both run script/lint. A bootstrap that prints "bootstrap complete" on a
# machine where `make check` cannot run is a false success, so this fails
# instead.
# Docker is a hard requirement, not a nice-to-have: script/lint and
# script/test build the lint and test phases of the Dockerfile, the only
# place the linter and the tests run, and script/check and
# script/precommit both run them. A bootstrap that prints "bootstrap
# complete" on a machine where `make check` cannot run is a false
# success, so this fails instead.
#
# Installing docker from here was considered and rejected: it needs root,
# a running daemon, and on macOS a GUI cask, so an attempt would itself
@@ -80,15 +85,15 @@ bootstrap: FAILED - $reason.
Docker is required to develop this repo. Without it these do not work:
script/lint builds Dockerfile.lint, which runs the linter as a
build step in a digest-pinned golangci-lint image.
That FROM line is the single source of truth for the
linter version
script/check runs script/lint
script/lint builds the lint phase of the Dockerfile, which runs
the linter as a build step in a digest-pinned
golangci-lint image
script/test builds the test phase of the Dockerfile
script/check runs script/test and script/lint
script/precommit runs script/check, so commits are blocked by the
pre-commit hook installed by script/setup
script/cibuild builds Dockerfile.lint and Dockerfile, which is what
CI runs
script/cibuild runs script/check and builds the image, which is
what CI runs
Install docker (and start the daemon, checking DOCKER_HOST and your
group membership), then re-run script/bootstrap. golangci-lint on PATH
@@ -108,11 +113,12 @@ main() {
if missing go; then pkg_install go golang go go; fi
# golangci-lint is deliberately NOT installed: script/lint lints by
# building Dockerfile.lint, whose digest-pinned image is the only
# place the linter runs, so whatever a package manager happens to
# ship would only be a shadow of the pinned version that could drift
# from CI. Nothing on the host is ever used as a linter, at any
# version, so installing one here would buy nothing.
# building the lint phase of the Dockerfile, whose digest-pinned
# image is the only place the linter runs, so whatever a package
# manager happens to ship would only be a shadow of the pinned
# version that could drift from CI. Nothing on the host is ever used
# as a linter, at any version, so installing one here would buy
# nothing.
# goreleaser, at the version pinned by script/install-goreleaser and
# verified against a hardcoded sha256. Package managers are not used
+3 -2
View File
@@ -1,7 +1,8 @@
#!/bin/sh
# script/check: run all checks (test, lint, fmt-check). Our own
# extension to scripts-to-rule-them-all. Must not modify any files.
# Generic: usually needs no adaptation.
# extension to scripts-to-rule-them-all. test and lint are Docker
# phases; fmt-check is native, because a formatter writes the working
# tree. Must not modify any files.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
+18 -68
View File
@@ -1,78 +1,28 @@
#!/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.
# script/cibuild: run the CI build. It bootstraps first: a CI runner
# checks out and runs this and nothing else, and script/fmt-check runs
# the formatter on the host, which a pristine checkout cannot do.
# --no-cache for the same reason as script/docker: the gate phases the
# final stage depends on are RUN steps, and a cached one is a check that
# did not run.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && 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.
# Dockerfile.lint also refuses to build at all when CHECK_EPOCH is
# empty, so a missing value fails its build loudly rather than passing
# quietly; the product Dockerfile does not, because a plain `docker
# build .` must succeed.
#
# 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 .
# Version, commit and build date are computed here on the host, the
# same way script/docker does, and passed into the product build,
# where they take precedence over what the build would derive from
# the .git in its context. VERSION comes from script/version, as in
# the Makefile.
version="$("$ROOT/script/version")"
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
commit_date="$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)"
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" \
"$SCRIPT_DIR/bootstrap"
"$SCRIPT_DIR/check"
# Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. The VERSION build argument takes precedence over
# the version a build stage derives from the .git in the context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \
--build-arg VERSION="$version" \
--build-arg COMMIT="$commit" \
--build-arg COMMIT_DATE="$commit_date" \
.
-t "$("$SCRIPT_DIR/projectname")" .
}
main "$@"
+10 -31
View File
@@ -1,15 +1,8 @@
#!/bin/sh
# script/docker: build the Docker image tagged with the project name.
# The tag comes from script/projectname. Unlike the canonical copy in
# sneak/prompts, it passes a fresh CHECK_EPOCH instead of --no-cache, and
# COMMIT and COMMIT_DATE as well as VERSION.
#
# This builds the PRODUCT image only, and the product Dockerfile has no
# lint stage: linting lives in Dockerfile.lint and is run by
# script/lint. So a green here means `make fmt-check` and `make test`
# passed and the image built -- it says nothing about lint. The gates
# are script/check (which runs script/lint) and script/cibuild (which
# builds both files).
# Identical in all repos; the tag comes from script/projectname.
# --no-cache because the gate phases the final stage depends on are RUN
# steps, and a cached one is a check that did not run.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -17,28 +10,14 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
# Same CHECK_EPOCH contract as script/cibuild, for the same reason
# and with the same bare-assignment and `$$` requirements -- see the
# comments there. This script is not the CI gate, but a local build
# is almost always warm, so without this it would report a green the
# tree had not earned and the two entrypoints would disagree about
# whether the tree is clean.
epoch="$(date +%s%N)$$"
# Version, commit and build date are computed here on the host and
# passed into the build, where they take precedence over what the
# build would derive from the .git in its context. VERSION comes
# from script/version, as in the Makefile, so the image reports the
# same string, -dirty included, that a local build of the same tree
# would.
version="$("$SCRIPT_DIR/version")"
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
commit_date="$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)"
docker build --build-arg CHECK_EPOCH="$epoch" \
# Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. The VERSION build argument takes precedence over
# the version a build stage derives from the .git in the context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \
--build-arg VERSION="$version" \
--build-arg COMMIT="$commit" \
--build-arg COMMIT_DATE="$commit_date" \
-t "$("$SCRIPT_DIR/projectname")" .
}
+7 -4
View File
@@ -7,9 +7,11 @@
# Only .gitea/workflows/release.yml calls this. goreleaser is not a
# compiler: it shells out to `go` for the `before:` hook and for every
# one of the four cross-compiles, so the release runner needs a Go
# toolchain on PATH. check.yml never does -- it builds inside the
# digest-pinned Dockerfile images -- so this is the release path's only
# host Go, and per REPO_POLICIES.md it must be pinned by hash.
# toolchain on PATH. check.yml compiles nothing on the host: it uses
# whatever Go script/bootstrap finds or installs only for that script's
# `go mod download` and for gofmt in script/fmt-check. So this is the
# only host Go that builds anything, and per REPO_POLICIES.md it must be
# pinned by hash.
# actions/setup-go exposes no checksum input, so Go is installed the way
# script/install-goreleaser installs goreleaser: download the exact
# archive from go.dev and refuse it unless its sha256 matches the value
@@ -18,7 +20,8 @@
# The version is go.mod's `go` directive, the single source of truth for
# the toolchain. GO_VERSION below MUST equal it, and this script fails
# when they disagree -- so bumping Go is one reviewed change touching
# go.mod, the checksum here, and the Dockerfile golang digest together.
# go.mod, the checksum here, and the Dockerfile's two golang digests
# together.
#
# Linux only, because that is what the release runner is. A darwin dev
# building a snapshot uses their own Go; supporting an OS means adding
+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 "$@"
+9 -8
View File
@@ -5,27 +5,28 @@
#
# THIS IS A DEVELOPER CONVENIENCE AND NEVER A GATE. Nothing in
# script/check, script/precommit or script/cibuild calls it, and no gate
# reads its exit status. The gate is script/lint, which builds
# Dockerfile.lint; run that afterwards to find out whether the tree is
# actually clean.
# reads its exit status. The gate is script/lint, which builds the lint
# phase of the Dockerfile; run that afterwards to find out whether the
# tree is actually clean.
#
# Unlike script/lint this cannot be a build step: a build step writes
# into an image, and fixes have to land in the worktree. So it runs the
# same pinned image as a container with the tree bind-mounted, which
# means it needs a LOCAL docker daemon -- a remote daemon has no access
# to these files, and this script will appear to do nothing there. The
# image reference is parsed out of Dockerfile.lint's FROM line, so the
# image reference is parsed out of the lint phase's FROM line, so the
# autofixer is always the same version as the linter that gates; fixes
# written by a different version are not necessarily fixes for the
# version that decides.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
DOCKERFILE="$ROOT/Dockerfile.lint"
DOCKERFILE="$ROOT/Dockerfile"
# The image reference from Dockerfile.lint, tag and digest included.
# The image reference from the FROM line ending `AS lint`, tag and
# digest included.
lint_image() {
awk '$1 == "FROM" { print $2; exit }' "$DOCKERFILE"
awk '$1 == "FROM" && $NF == "lint" { print $2; exit }' "$DOCKERFILE"
}
main() {
@@ -33,7 +34,7 @@ main() {
image="$(lint_image)"
if [ -z "$image" ]; then
echo "lint-fix: no FROM line found in $DOCKERFILE" >&2
echo "lint-fix: no FROM ... AS lint line found in $DOCKERFILE" >&2
exit 1
fi
+10 -60
View File
@@ -1,69 +1,19 @@
#!/bin/sh
# script/test: run the test suite. Quiet on success; on failure, rerun
# verbosely for full diagnostic output (the exit 1 ensures the rerun
# never turns a failure into a pass).
# script/test: run the test suite. Testing is a phase of the Dockerfile
# and this builds that phase alone, on the same terms as script/lint:
# --target because a phase that is not the last stage is built only when
# named, --no-cache because a cached test layer is a test that did not
# run, and a tag so each build replaces the previous image.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# The flags live in one function so the quiet run and the verbose rerun
# below cannot drift apart. A rerun that used different flags would
# diagnose a different program than the one that failed.
#
# -count=1 is the documented way to bypass Go's test result cache, and
# it is not optional here. Without it, a package whose inputs are
# unchanged prints `ok <pkg> (cached)`, and that line is
# indistinguishable -- to every check this repo performs -- from a
# package that actually ran. The whole suite reports its full set of
# `ok` lines in under half a second having executed nothing. That
# matters beyond the local inner loop: the Dockerfile's `RUN make test`
# is forced to re-execute by CHECK_EPOCH, but a GOCACHE baked into an
# earlier image layer survives into the re-executed step, so the step
# can re-run and still do no work. It is applied unconditionally rather
# than only in the containerised path because the pre-commit hook runs
# this same script; a gate that is honest only in CI is dishonest
# exactly where people lean on it most.
#
# -timeout is a hang backstop, not a performance budget: its job is to
# turn a deadlocked test into a stack dump instead of a wedged CI job,
# so it wants to sit far above the slowest legitimate runtime, not just
# above it. It is per test binary and covers test execution only -- the
# clock starts inside testing.M.Run, after compilation and linking, so
# build time is not charged against it. (Measured: a containerised run
# with an empty GOCACHE reports per-package durations within noise of a
# warm host run. A shell `timeout 30 go test ./...` would include
# compilation, but that is a different mechanism from this flag.)
#
# The 120s value DELIBERATELY DIVERGES from REPO_POLICIES.md:192, which
# mandates "Add a 30-second timeout", and from that file's canonical Go
# recipe at :212-214, which uses -timeout 30s. REPO_POLICIES.md is
# org-canonical and cannot be amended from this repo, so the divergence
# is recorded here instead, and issue #101 proposes amending the policy
# text upstream. Do not revert this to 30s without reading #101 first.
#
# Why it diverges: the slowest packages are internal/database and
# internal/vaultik, observed under -race at about 6.4s warm, 8.1s in a
# cold containerised run on a contended host, and 10.2s in an
# independent cold run on this same host. The worst case is not tightly
# characterised -- each fresh measurement has come in above the last --
# which is itself an argument for generous headroom. Against the 10.2s
# observation, 30s is only 2.9x: not a safety margin but a flake
# waiting for a slow day, whose failure mode is a timeout that looks
# like a real defect. 120s leaves about 12x while still bounding a hung
# package -- including the verbose rerun below -- to a few minutes. The
# cost of that choice, also recorded on #101: because of the rerun, a
# hung package pays the timeout twice.
run_tests() {
go test -race -timeout 120s -count=1 "$@" ./...
}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
run_tests || {
echo "--- Rerunning with -v for details ---"
run_tests -v
exit 1
}
docker build --no-cache \
--target test \
-t "$("$SCRIPT_DIR/projectname")-test" .
}
main "$@"
+2 -3
View File
@@ -3,9 +3,8 @@
# Our own extension to scripts-to-rule-them-all. The Makefile's LDFLAGS
# call this rather than carrying a hardcoded constant, which is what used
# to make every local build claim to be 1.0.0-rc.1 regardless of git
# state. script/docker and script/cibuild pass its output to the image
# build; given no version, the image build runs `git describe --tags
# --always` itself, without `--dirty`.
# state. script/docker and script/cibuild run the same `git describe`
# themselves, and fall back to "unknown" rather than "dev".
#
# The version is `git describe --tags --always --dirty`: the tag on a
# tagged commit, tag-N-gHASH on a commit after one, the short commit