Run every lint-class check inside Docker (closes #38)
All checks were successful
check / check (push) Successful in 1m9s
All checks were successful
check / check (push) Successful in 1m9s
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.
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
# scripts-to-rule-them-all. Must not modify any tracked files. Runs the
|
||||
# canonical order: the clean production build, then the lint build that
|
||||
# reports path warnings, then the read-only formatting check.
|
||||
#
|
||||
# The last two run inside Docker (they build stages of Dockerfile.lint),
|
||||
# so this script needs a working docker daemon. That is deliberate:
|
||||
# every lint run for this repo happens in a container, and there is no
|
||||
# host fallback to drop back to.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
#!/bin/sh
|
||||
# script/cibuild: run the CI build. The Dockerfile runs `make check`,
|
||||
# so a successful build implies all checks pass. The Gitea workflow
|
||||
# runs this on push.
|
||||
# script/cibuild: run the CI build. The Gitea workflow runs this on
|
||||
# push, and it is the single entrypoint that covers everything:
|
||||
#
|
||||
# That implication only holds because of CHECK_EPOCH. Docker keys the
|
||||
# `RUN make check` layer on content, so on an unchanged tree it is
|
||||
# served from cache: the checks never execute and the 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.
|
||||
# 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
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
@@ -25,6 +40,9 @@ main() {
|
||||
# 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 "$@"
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
# the reason next to each entry: the Hugo layout templates, which are
|
||||
# Go templates and not HTML, and content/, whose reformatting was
|
||||
# measured to change the rendered page.
|
||||
#
|
||||
# This runs on the host, unlike the read-only check: it rewrites the
|
||||
# working tree, which a container build cannot do. It is therefore the
|
||||
# authoritative copy of the prettier version, scope and flags -- the
|
||||
# fmt-check stage of Dockerfile.lint duplicates them and must be kept in
|
||||
# sync with this file.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
#!/bin/sh
|
||||
# script/fmt-check: check the formatting of this repo's markdown and
|
||||
# CSS (read-only). Same scope and same settings as script/fmt - keep
|
||||
# the two in sync - but fails instead of writing.
|
||||
# CSS (read-only). Like script/lint, it runs only in Docker: it builds
|
||||
# the `fmt-check` stage of Dockerfile.lint, where prettier runs as a
|
||||
# build step. Leaving prettier to run on the host here would have left
|
||||
# `make check` with a host lint path, which is the thing being removed.
|
||||
#
|
||||
# The version, scope and flags live in Dockerfile.lint and must stay in
|
||||
# sync with script/fmt, which is the authoritative copy and stays on the
|
||||
# host because it writes to the working tree.
|
||||
#
|
||||
# Dockerfile.lint requires the CHECK_EPOCH build argument, generated
|
||||
# here exactly as script/cibuild generates it -- see that script for why
|
||||
# the check layer must not be allowed to cache, and why the value is
|
||||
# built in an assignment rather than inline.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
PRETTIER_VERSION="3.4.2"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
npx --yes "prettier@${PRETTIER_VERSION}" --check \
|
||||
'**/*.md' '**/*.css' --tab-width 4 --prose-wrap always
|
||||
epoch="$(date +%s%N)$$"
|
||||
docker build -f Dockerfile.lint --target fmt-check \
|
||||
--build-arg CHECK_EPOCH="$epoch" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
21
script/lint
21
script/lint
@@ -1,15 +1,26 @@
|
||||
#!/bin/sh
|
||||
# script/lint: this Hugo site has no dedicated linter, so the lint gate
|
||||
# is a clean build that surfaces broken internal links and template
|
||||
# path problems. It is a real check: `hugo` fails on build errors, and
|
||||
# --printPathWarnings reports render-target collisions.
|
||||
# script/lint: run the lint. This Hugo site has no dedicated linter, so
|
||||
# the lint gate is a clean build that surfaces broken internal links and
|
||||
# template path problems -- but where it runs is not negotiable: every
|
||||
# lint run happens inside a Docker container, so this script does
|
||||
# nothing except build the `lint` stage of Dockerfile.lint. The check is
|
||||
# a build step there, so a successful build is a clean lint. There is
|
||||
# deliberately no host fallback and no "already inside a container?"
|
||||
# branch: either would be a host lint path wearing a disguise.
|
||||
#
|
||||
# Dockerfile.lint requires the CHECK_EPOCH build argument, generated
|
||||
# here exactly as script/cibuild generates it -- see that script for why
|
||||
# the check layer must not be allowed to cache, and why the value is
|
||||
# built in an assignment rather than inline.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
hugo --minify --printPathWarnings
|
||||
epoch="$(date +%s%N)$$"
|
||||
docker build -f Dockerfile.lint --target lint \
|
||||
--build-arg CHECK_EPOCH="$epoch" .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user