Re-vendor the canonical files from sneak/prompts at dd4027b (closes #213)
check / check (push) Failing after 4s

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. The
image takes its version from the VERSION build arg or git describe, and
still stamps the commit and its date from .git. 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", the version script/docker
stamps outside a git checkout.

Model: opus-5-5
This commit is contained in:
2026-10-06 00:29:40 +00:00
parent 070090124a
commit 99f4318ae6
34 changed files with 787 additions and 1180 deletions
+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 "$@"