Run the lint inside Docker via Dockerfile.lint (closes #38)
All checks were successful
check / check (push) Successful in 1m23s
All checks were successful
check / check (push) Successful in 1m23s
Add a root Dockerfile.lint that runs `hugo --minify --printPathWarnings`
as a build step, so a successful build IS a clean lint, and reduce
script/lint to building that file. There is no host lint path and
deliberately no "am I already inside a container?" branch, which would
be a host lint path in disguise.
The containerisation boundary is lint only, per the owner ruling on the
issue: formatting is not a lint, so script/fmt and script/fmt-check stay
on the host, unchanged in version, scope and flags. That also removes
the forced duplication of prettier's settings between a script and a
Dockerfile, and with it the keep-in-sync notes that duplication needed.
Dockerfile.lint has exactly one stage on purpose. A whole-file
`docker build -f Dockerfile.lint .` builds only the file's last stage,
and sibling stages off a shared base carry no ordering edge, so a second
stage beside the lint would be silently skipped by exactly the
invocation the canonical org-wide script/lint uses -- a green that
linted nothing, which the per-stage CHECK_EPOCH guard cannot catch
because the stage that did run satisfies it. With one stage there is
nothing to skip and script/lint needs no --target. A comment in the file
says that any second check added here must be chained or carry an
explicit ordering edge, never left as a sibling.
Its first four instructions are byte-identical to the main Dockerfile's
and in the same order, so the expensive `RUN script/bootstrap` layer
that compiles the pinned Hugo from source is shared between the two
images rather than paid twice.
Resolve the recursion by direction, not detection. `make check` calls
script/lint, and script/lint is now a `docker build`, so `RUN make
check` in an image would attempt a docker build inside a build step
where there is no daemon. The main Dockerfile therefore runs the
individual non-lint checks -- script/test and script/fmt-check, as
separate RUN lines under the CHECK_EPOCH guard -- matching the canonical
shape, and only the lint is absent from it. script/cibuild runs
script/lint first, for fail-fast feedback: on a runner with no cached
bootstrap layer a lint failure should not wait behind a Hugo build from
source. CI coverage is therefore unchanged, and it runs the same scripts
a developer runs.
Caching is waived for the lint in the shape this repo already settled:
ARG CHECK_EPOCH with no default, guarded with
`[ -n "$CHECK_EPOCH" ] || exit 1`, and the value expanded into the
linted command as well as the guard, so invalidation never rests on
BuildKit's treatment of an unreferenced ARG. Every image-building
entrypoint generates and passes it -- script/cibuild, script/docker,
script/lint -- each as a whole assignment rather than inline, for the
`set -e` reason script/cibuild documents.
script/lint builds with `--output type=cacheonly`: the build is run for
its exit status, not for an image, and because the lint layer is
cache-busted on every invocation an exporting build leaves one dangling
image per lint run. On a host shared with other work that accumulates.
The build cache is unaffected, so script/bootstrap still hits, and
failures still propagate.
Two divergences from REPO_POLICIES.md, stated rather than buried:
- REPO_POLICIES.md:92, "all Dockerfiles must run `make check`". That
rule and "every lint run happens in Docker" cannot both hold once
`make check` contains the lint.
- REPO_POLICIES.md:102-168, which requires a separate lint stage whose
result the build stage depends on through
`COPY --from=lint /src/go.sum /dev/null`, on the stated grounds that
without the edge "the build stage would not wait for lint to finish
and a lint failure might not fail the overall build". No such edge
exists here: the lint is its own file and its own build, sequenced
by script/cibuild rather than by BuildKit. Both sections are
superseded upstream by 12e8db8 in sneak/prompts, which deletes the
Go multistage lint stage and its ordering trick for the same reason
-- that stage ran `make lint`, which is now a docker build.
Verified: two consecutive script/lint runs on an unchanged tree both
executed hugo for real, distinct epochs echoed, script/bootstrap CACHED,
second run 0.85s; a whole-file `docker build -f Dockerfile.lint .` with
the argument and no --target ran the lint for real; a bare build with no
argument failed closed on the guard; a planted template error failed the
lint with hugo's own render error and made script/cibuild exit non-zero
in 0.6s with the main image build never starting; a planted over-long
line failed the host script/fmt-check; both reverted and re-run clean;
`make check`, script/docker and script/cibuild all green with every
check layer observed executing rather than served from cache, and the
bootstrap layer CACHED in both images. The deploy path is byte-identical
to main: .gitea/, script/bootstrap, script/test and .dockerignore are
untouched.
This commit is contained in:
@@ -3,6 +3,14 @@
|
||||
# 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 lint - and only the lint - runs inside Docker: it is a build of
|
||||
# Dockerfile.lint, so this script needs a working docker daemon and has
|
||||
# no host fallback to drop back to. Budget for the cold case: the first
|
||||
# lint on a machine with no cached script/bootstrap layer compiles the
|
||||
# pinned Hugo from source, which takes minutes. That cost falls on the
|
||||
# pre-commit hook too, since it runs this script. Every later run reuses
|
||||
# that layer and only the lint step re-executes.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
#!/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. Two
|
||||
# container builds, in order:
|
||||
#
|
||||
# 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. script/lint, which builds Dockerfile.lint -- the lint runs as a
|
||||
# build step there
|
||||
# 2. the main Dockerfile, which runs the non-lint checks: the clean
|
||||
# `hugo --minify` production build (script/test) and the read-only
|
||||
# prettier check (script/fmt-check)
|
||||
#
|
||||
# Lint goes first, for fail-fast feedback: on a runner with no cached
|
||||
# script/bootstrap layer the main image compiles Hugo from source, and a
|
||||
# lint failure should not wait behind that. It is a separate build
|
||||
# rather than a step inside the main image because script/lint is itself
|
||||
# a `docker build`, and a docker build cannot run a docker build. See
|
||||
# Dockerfile.lint for the full reasoning.
|
||||
#
|
||||
# The lint is delegated to the same script a developer runs, so CI
|
||||
# cannot drift from `make check`.
|
||||
#
|
||||
# Neither build implies a passing check without CHECK_EPOCH. Docker keys
|
||||
# the check layers on content, so on an unchanged tree they are served
|
||||
# from cache: nothing executes and the build still exits 0. Passing a
|
||||
# value that differs on every invocation invalidates those layers and
|
||||
# everything below them, while the script/bootstrap toolchain layer
|
||||
# above keeps caching. script/lint does the same for its own build.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
"$SCRIPT_DIR/lint"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
# 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.
|
||||
#
|
||||
# Both prettier entrypoints run on the host: only linting is
|
||||
# containerised (owner ruling, 2026-08-10), and formatting is not a
|
||||
# lint. Keep the version, scope and flags here in sync with
|
||||
# script/fmt-check.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
# 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.
|
||||
#
|
||||
# This runs on the host, not in a container: only linting is
|
||||
# containerised (owner ruling, 2026-08-10), and a formatting check is
|
||||
# not a lint. Running here also keeps it usable offline, since npx
|
||||
# reuses ~/.npm/_npx after the first run.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
35
script/lint
35
script/lint
@@ -1,15 +1,40 @@
|
||||
#!/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 Dockerfile.lint. The lint 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.
|
||||
#
|
||||
# No --target: Dockerfile.lint has exactly one stage, so the whole-file
|
||||
# build IS the lint. See that file for why a second, sibling stage would
|
||||
# be a silent skip.
|
||||
#
|
||||
# Dockerfile.lint requires the CHECK_EPOCH build argument, generated
|
||||
# here exactly as script/cibuild generates it -- see that script for why
|
||||
# the lint 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)$$"
|
||||
# --output type=cacheonly: this build is run for its exit status,
|
||||
# not for an image. Because the lint layer is cache-busted on every
|
||||
# invocation the result is a new image every time, and an untagged
|
||||
# build would leave one dangling image per lint run on a host shared
|
||||
# with other work. cacheonly keeps the build cache (so
|
||||
# script/bootstrap still hits) and exports nothing. Failures still
|
||||
# propagate: an empty CHECK_EPOCH or a failing lint exits non-zero.
|
||||
docker build \
|
||||
--build-arg CHECK_EPOCH="$epoch" \
|
||||
--output type=cacheonly \
|
||||
-f Dockerfile.lint \
|
||||
.
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user