Files
lora.vegas/script/cibuild
clawbot f5761b6227
All checks were successful
check / check (push) Successful in 1m9s
Run every lint-class check inside Docker (closes #38)
Add a root Dockerfile.lint that carries the checks as build steps -- a
`lint` stage running `hugo --minify --printPathWarnings` and a
`fmt-check` stage running the prettier check -- and reduce script/lint
and script/fmt-check to building their stage. A successful build is a
clean check. There is no host path and deliberately no "am I already
inside a container?" branch, which would be a host lint path in
disguise.

The two stages share a `base` whose first four instructions are
byte-identical to the main Dockerfile's, so the expensive
`RUN script/bootstrap` layer that compiles the pinned Hugo from source
is a cache hit against the main image instead of a second build of the
same thing.

Resolve the resulting recursion by splitting the checks by where they
run, not with an escape hatch. `make check` runs script/lint, so the
main Dockerfile can no longer `RUN make check`: that would be
docker-in-docker inside a bare Alpine with no docker client and no
daemon socket, and script/cibuild is what CI runs on every push. The
main Dockerfile therefore runs `make test`, the production build, and
script/cibuild builds it and then calls script/lint and
script/fmt-check. CI still covers the production build, lint and the
format check, and it runs exactly what a developer runs.

script/fmt stays on the host because it rewrites the working tree,
which a container build cannot do. That makes it the authoritative
copy of the prettier version, scope and flags that the fmt-check stage
duplicates; both sides carry a keep-in-sync note. The duplication is
forced: any `RUN script/fmt-check` inside the image is the recursion
again.

Caching is waived for the checks in the shape this repo already
settled: `ARG CHECK_EPOCH` with no default, declared and guarded
separately in each stage because ARG does not cross a FROM, with the
value expanded into the checked command as well as the guard so
invalidation does not rest on BuildKit's treatment of an unreferenced
ARG. All four image-building entrypoints now generate and pass it --
script/cibuild, script/docker, script/lint, script/fmt-check.

Verified: two consecutive script/lint runs on an unchanged tree both
executed hugo for real, with script/bootstrap CACHED; a constant-epoch
counterfactual restored the false green (exit 0, lint layer CACHED, no
hugo output); an empty epoch failed closed on the guard; a broken
template failed the lint stage and an unformatted README failed the
fmt-check stage, both reverted and re-run clean; script/cibuild and
`make check` are green with all three checks demonstrably executing.
2026-08-10 12:52:56 +00:00

49 lines
2.1 KiB
Bash
Executable File

#!/bin/sh
# script/cibuild: run the CI build. The Gitea workflow runs this on
# push, and it is the single entrypoint that covers everything:
#
# 1. the main Dockerfile, which runs the clean `hugo --minify`
# production build (`make test`)
# 2. script/lint, which builds Dockerfile.lint's `lint` stage
# 3. script/fmt-check, which builds Dockerfile.lint's `fmt-check`
# stage
#
# Steps 2 and 3 are delegated to the same scripts a developer runs, so
# CI cannot drift from `make check`. They are separate builds rather
# than a `RUN make check` inside the main image because script/lint is
# itself a `docker build`: shelling back into `make check` from an image
# would be docker-in-docker inside a bare alpine with no docker client
# and no daemon socket. See Dockerfile.lint for the full reasoning.
#
# The main image build below only implies a passing production build
# because of CHECK_EPOCH. Docker keys the `RUN make test` layer on
# content, so on an unchanged tree it is served from cache: the build
# never executes and the image build still exits 0. Passing a value that
# differs on every invocation invalidates that layer and everything
# below it, while the script/bootstrap toolchain layer above it keeps
# caching. script/lint and script/fmt-check each do the same for their
# own stage.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
# Assigned to a variable rather than substituted inline in the
# argument list: a command substitution that fails inside an
# argument does not trip `set -e`, so the inline form would quietly
# pass an empty string and restore the cached false green. As the
# whole of an assignment its exit status is the command's, so
# `set -e` catches it. `%N` keeps two invocations within the same
# second distinct; busybox date silently drops `%N` and still exits
# 0, so `$$` is appended to cover that degradation.
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" .
"$SCRIPT_DIR/lint"
"$SCRIPT_DIR/fmt-check"
}
main "$@"