Compare commits
3 Commits
f5761b6227
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3cd4c18c | ||
| 910f343263 | |||
|
|
25b6c0a9de |
33
Dockerfile
33
Dockerfile
@@ -1,18 +1,16 @@
|
||||
# Hugo static-site build image. The build runs `make test` -- a clean
|
||||
# `hugo --minify` production build -- so the image build fails on any
|
||||
# template, content or config error.
|
||||
# Hugo static-site build image. The build runs the individual non-lint
|
||||
# checks -- script/test, a clean `hugo --minify` production build, and
|
||||
# script/fmt-check, the read-only prettier check -- so the image build
|
||||
# fails on any template, content, config or formatting error.
|
||||
#
|
||||
# It deliberately does NOT run `make check`. `make check` runs
|
||||
# script/lint, and script/lint is a `docker build` of Dockerfile.lint,
|
||||
# so `RUN make check` here would be docker-in-docker inside a bare
|
||||
# alpine with no docker client and no daemon socket. The checks are
|
||||
# therefore split by where they run: the production build here, lint and
|
||||
# the prettier format check in Dockerfile.lint. Do not reintroduce a
|
||||
# `make check` (or a `make lint` / `make fmt-check`) line in this file.
|
||||
#
|
||||
# CI coverage is unaffected: script/cibuild builds this image and then
|
||||
# calls script/lint and script/fmt-check, so every check in `make check`
|
||||
# still runs on every push -- see script/cibuild.
|
||||
# It deliberately does NOT run `make check`, and only the lint is
|
||||
# missing from what it does run. `make check` calls script/lint, and
|
||||
# script/lint is a `docker build` of Dockerfile.lint, so `RUN make
|
||||
# check` here would attempt a docker build inside a build step, in a
|
||||
# bare alpine with no docker client and no daemon socket. Putting
|
||||
# `make check` (or a `make lint`) back reintroduces exactly that
|
||||
# recursion. The lint is not skipped: script/cibuild runs script/lint
|
||||
# first, in its own container, before this build starts.
|
||||
#
|
||||
# Build this only via script/cibuild or script/docker: both pass the
|
||||
# CHECK_EPOCH build argument that this file requires, and a bare
|
||||
@@ -51,5 +49,8 @@ COPY . .
|
||||
ARG CHECK_EPOCH
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
|
||||
# Run the production build - build fails if the site does not build.
|
||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
||||
# The individual non-lint checks - build fails if either fails. Invoked
|
||||
# as script/ entrypoints rather than `make check` for the reason in the
|
||||
# header comment above.
|
||||
RUN echo "check epoch: ${CHECK_EPOCH}" && script/test
|
||||
RUN script/fmt-check
|
||||
|
||||
@@ -1,85 +1,61 @@
|
||||
# Lint image. Every lint-class check for this repo runs here and
|
||||
# nowhere else: the checks are build steps, so a successful build IS a
|
||||
# clean lint. There is no host lint path and no "am I already in a
|
||||
# container?" bypass -- script/lint and script/fmt-check are reduced to
|
||||
# building the stage below that carries their check.
|
||||
# Lint-only image. `script/lint` builds this file and nothing else: the
|
||||
# lint runs as a build step, so a successful build IS a clean lint.
|
||||
#
|
||||
# Build this only via script/lint or script/fmt-check: both pass the
|
||||
# CHECK_EPOCH build argument that the stages here require, and a bare
|
||||
# `docker build -f Dockerfile.lint .` fails by design. See the guards.
|
||||
# One stage, deliberately. A whole-file `docker build -f Dockerfile.lint .`
|
||||
# builds only the file's LAST stage, and sibling stages off a shared base
|
||||
# have no ordering edge between them, so a second stage sitting beside
|
||||
# this one would be silently skipped by exactly the invocation the
|
||||
# canonical org-wide `script/lint` uses -- a green that linted nothing,
|
||||
# which is the failure mode this file exists to prevent. With a single
|
||||
# stage there is nothing to skip and `script/lint` needs no `--target`.
|
||||
# If a second check is ever added here it must be chained (`FROM lint AS
|
||||
# ...`) or carry an explicit ordering edge, never left as a sibling.
|
||||
#
|
||||
# Why this is a separate file from the main Dockerfile, and why that
|
||||
# one no longer runs `make check`: `make check` runs script/lint, and
|
||||
# script/lint is now a `docker build`. A `RUN make check` in an image
|
||||
# would therefore be docker-in-docker inside a bare alpine with no
|
||||
# docker client and no daemon socket. The checks are split by where
|
||||
# they run instead -- the main Dockerfile runs the production build,
|
||||
# this file runs lint and the format check -- so no image ever shells
|
||||
# back into `make check`. script/cibuild drives all of them, so CI
|
||||
# coverage is unchanged.
|
||||
# Only linting is containerised (owner ruling, 2026-08-10: "fmt and fmt
|
||||
# check arent docker, just linting"). script/fmt and script/fmt-check run
|
||||
# on the host, and the main Dockerfile runs the production build and the
|
||||
# format check directly -- see the comment there.
|
||||
#
|
||||
# The lint is invoked directly below rather than through `make lint` or
|
||||
# `script/lint`. That is not a style choice: `script/lint` IS this build,
|
||||
# so calling it from inside would recurse into a docker build with no
|
||||
# daemon.
|
||||
#
|
||||
# This repo's lint is a clean Hugo build that surfaces broken internal
|
||||
# links and template path problems: `hugo` fails on build errors and
|
||||
# --printPathWarnings reports render-target collisions.
|
||||
|
||||
# alpine 3.21, 2026-02-28
|
||||
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 AS base
|
||||
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Keep these four instructions byte-identical to the main Dockerfile's,
|
||||
# in the same order: Docker keys layers on the instruction chain, not on
|
||||
# the file they live in, so an identical prefix means this stage is a
|
||||
# the file they live in, so an identical prefix means this build is a
|
||||
# cache hit against the main image's layers. script/bootstrap compiles
|
||||
# the pinned Hugo from source, which is by far the most expensive step
|
||||
# here, and it must not be paid twice.
|
||||
# here, and it must not be paid twice. The dependency layer is also
|
||||
# deliberately above the ARG below, so it stays cached and only the lint
|
||||
# step re-runs on every invocation.
|
||||
COPY script/ script/
|
||||
RUN script/bootstrap
|
||||
|
||||
COPY . .
|
||||
|
||||
# --- lint -------------------------------------------------------------
|
||||
#
|
||||
# The site's lint is a clean build that surfaces broken internal links
|
||||
# and template path problems: `hugo` fails on build errors and
|
||||
# --printPathWarnings reports render-target collisions.
|
||||
#
|
||||
# The flags are inlined rather than reached through `RUN script/lint`,
|
||||
# and that is forced, not lazy: script/lint is the docker build that
|
||||
# produces this stage, so calling it here is the recursion described
|
||||
# above. Keep these flags in sync with script/lint's documentation.
|
||||
FROM base AS lint
|
||||
|
||||
# CHECK_EPOCH is a per-invocation nonce supplied by script/lint. Without
|
||||
# it an unchanged tree serves the check layer from cache: the lint never
|
||||
# it an unchanged tree serves the lint layer from cache: the lint never
|
||||
# executes and the build still exits 0, which is precisely the false
|
||||
# green this repo already fixed once in the main Dockerfile. Caching is
|
||||
# explicitly waived for lint, so the value is expanded into the checked
|
||||
# explicitly waived for lint, so the value is expanded into the linted
|
||||
# command as well as the guard -- two independent value-keyed
|
||||
# invalidation points, so a cache miss never depends on BuildKit's
|
||||
# treatment of an unreferenced ARG. Declared with no default: a default
|
||||
# is a constant, and a constant is a stable cache key. ARG is
|
||||
# stage-scoped, so the fmt-check stage below declares its own.
|
||||
#
|
||||
# Everything above this line still caches, so script/bootstrap is not
|
||||
# rebuilt.
|
||||
# treatment of an unreferenced ARG, and the epoch is visible in the build
|
||||
# log. Declared with no default: a default is a constant, and a constant
|
||||
# is a stable cache key. The guard makes a bare
|
||||
# `docker build -f Dockerfile.lint .` fail loudly instead of silently
|
||||
# reusing the empty (and therefore stable) cache key. Keep both
|
||||
# references.
|
||||
ARG CHECK_EPOCH
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
RUN echo "lint epoch: ${CHECK_EPOCH}" && hugo --minify --printPathWarnings
|
||||
|
||||
# --- fmt-check --------------------------------------------------------
|
||||
#
|
||||
# The read-only prettier check over this repo's markdown and CSS. Same
|
||||
# version, same scope and same flags as script/fmt, which is the
|
||||
# authoritative copy and stays on the host because it writes to the
|
||||
# working tree; keep the two in sync. The exclusions live in
|
||||
# .prettierignore with the reason for each.
|
||||
#
|
||||
# Built as a sibling of `lint` rather than stacked on top of it so that
|
||||
# a --target build runs exactly one check, and so a lint failure and a
|
||||
# formatting failure are reported independently.
|
||||
FROM base AS fmt-check
|
||||
|
||||
# Same nonce, same reasoning as the lint stage above. Declared again
|
||||
# because ARG does not cross a FROM.
|
||||
ARG CHECK_EPOCH
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
RUN echo "fmt-check epoch: ${CHECK_EPOCH}" && \
|
||||
npx --yes "prettier@3.4.2" --check \
|
||||
'**/*.md' '**/*.css' --tab-width 4 --prose-wrap always
|
||||
|
||||
8
Makefile
8
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks serve
|
||||
.PHONY: bootstrap setup test lint fmt fmt-check check docker cibuild precommit hooks serve
|
||||
|
||||
bootstrap:
|
||||
@script/bootstrap
|
||||
@@ -24,6 +24,12 @@ check:
|
||||
docker:
|
||||
@script/docker
|
||||
|
||||
cibuild:
|
||||
@script/cibuild
|
||||
|
||||
precommit:
|
||||
@script/precommit
|
||||
|
||||
hooks:
|
||||
@script/install-precommit
|
||||
|
||||
|
||||
71
README.md
71
README.md
@@ -39,12 +39,13 @@ build, and the formatting check:
|
||||
make check
|
||||
```
|
||||
|
||||
The lint build and the formatting check run inside Docker, so `make check` needs
|
||||
a working Docker daemon; there is no host fallback.
|
||||
The lint runs inside Docker, so `make check` needs a working Docker daemon;
|
||||
there is no host fallback. On a machine that has never built the image, the
|
||||
first run compiles the pinned Hugo from source, which takes minutes; later runs
|
||||
reuse that cached layer.
|
||||
|
||||
`make fmt` rewrites the repo's markdown and CSS to the project's prettier
|
||||
settings; run it if `make check` fails on formatting. It runs on the host,
|
||||
because it writes to your working tree.
|
||||
settings; run it if `make check` fails on formatting.
|
||||
|
||||
To contribute to this site, contact **sneak@sneak.berlin** for git repository
|
||||
access.
|
||||
@@ -66,40 +67,50 @@ provide:
|
||||
- `script/test` — the correctness check: a clean `hugo --minify` production
|
||||
build
|
||||
- `script/lint` — a clean build that surfaces broken links and path collisions,
|
||||
run inside Docker: it builds the `lint` stage of `Dockerfile.lint`, where the
|
||||
check is a build step, so a successful build is a clean lint
|
||||
run inside Docker: it builds `Dockerfile.lint`, where the lint is a build
|
||||
step, so a successful build is a clean lint
|
||||
- `script/fmt` — format every markdown and CSS file in the repo with prettier;
|
||||
the exclusions live in `.prettierignore` with the reason for each. The one
|
||||
prettier entrypoint that runs on the host, because it writes to your working
|
||||
tree
|
||||
- `script/fmt-check` — check that formatting (read-only), also inside Docker:
|
||||
the `fmt-check` stage of `Dockerfile.lint`
|
||||
the exclusions live in `.prettierignore` with the reason for each
|
||||
- `script/fmt-check` — check that formatting (read-only)
|
||||
- `script/check` — run `script/test`, `script/lint`, then `script/fmt-check`;
|
||||
modifies no tracked files
|
||||
- `script/docker` — build the Docker image tagged with the project name
|
||||
- `script/cibuild` — the CI build: the main image (the production build), then
|
||||
`script/lint` and `script/fmt-check`
|
||||
- `script/cibuild` — the CI build: `script/lint` first, for fail-fast feedback,
|
||||
then the main image, which runs the non-lint checks
|
||||
- `script/install-precommit` — install the git pre-commit hook that runs
|
||||
`script/check`
|
||||
`script/precommit`
|
||||
|
||||
Every lint run for this repo happens inside a container. `script/lint` and
|
||||
`script/fmt-check` have no host path and no "already inside a container?"
|
||||
branch, so what a developer runs and what CI runs are the same build.
|
||||
Each of those has a Makefile shim of the same name — `make bootstrap`,
|
||||
`make setup`, `make test`, `make lint`, `make fmt`, `make fmt-check`,
|
||||
`make check`, `make docker`, `make cibuild` — with one exception:
|
||||
`script/install-precommit` is `make hooks`. `script/precommit`, which is what
|
||||
the installed hook runs, is `make precommit`. Prefer the make targets; the
|
||||
`Makefile` lists the operations you are expected to run.
|
||||
|
||||
That is also why the main `Dockerfile` runs `make test` rather than
|
||||
`make check`: `make check` calls `script/lint`, which is itself a
|
||||
`docker build`, so a `make check` inside an image would be docker-in-docker in a
|
||||
bare Alpine with no docker client and no daemon socket. The checks are split by
|
||||
where they run — the production build in `Dockerfile`, lint and the format check
|
||||
in `Dockerfile.lint` — and `script/cibuild` drives all three, so CI coverage is
|
||||
unchanged.
|
||||
`make cibuild` is the slowest target: it is the only one that runs two container
|
||||
builds, the lint image first and then the main image. The two share the
|
||||
`script/bootstrap` layer byte-for-byte, so the pinned-Hugo compile described
|
||||
above is paid once per machine rather than twice, and once that layer is cached
|
||||
a full `make cibuild` takes seconds. That is the cost of the CI build, not a
|
||||
sign of a problem.
|
||||
|
||||
Build any image through `script/cibuild`, `script/docker`, `script/lint` or
|
||||
`script/fmt-check` only. All four pass a per-invocation `CHECK_EPOCH` build
|
||||
argument that the Dockerfiles require, so a check layer can never be served from
|
||||
cache — without it Docker returns a green it did not earn. A bare `docker build`
|
||||
fails closed on the `CHECK_EPOCH` guard rather than caching its way to a false
|
||||
success.
|
||||
Every lint run for this repo happens inside a container, and only the lint does.
|
||||
`script/lint` has no host path and no "already inside a container?" branch, so
|
||||
what a developer runs and what CI runs are the same build. `script/fmt` and
|
||||
`script/fmt-check` run on the host: a formatting check is not a lint.
|
||||
|
||||
That is also why the main `Dockerfile` runs `script/test` and `script/fmt-check`
|
||||
rather than `make check`. `make check` calls `script/lint`, which is itself a
|
||||
`docker build`, so a `make check` inside an image would attempt a docker build
|
||||
in a bare Alpine with no docker client and no daemon socket. The lint is not
|
||||
skipped — `script/cibuild` runs it first, in its own container, before the main
|
||||
image build starts.
|
||||
|
||||
Build any image through `script/cibuild`, `script/docker` or `script/lint` only.
|
||||
All three pass a per-invocation `CHECK_EPOCH` build argument that the
|
||||
Dockerfiles require, so a check layer can never be served from cache — without
|
||||
it Docker returns a green it did not earn. A bare `docker build` fails closed on
|
||||
the `CHECK_EPOCH` guard rather than caching its way to a false success.
|
||||
|
||||
A convenience `make serve` target runs `hugo server` for local preview.
|
||||
|
||||
|
||||
146
TODO.md
146
TODO.md
@@ -20,62 +20,99 @@ wrangler CLI install, an exact version), and the Hugo that builds the published
|
||||
site is a deliberate pinned version rather than whatever the base image's
|
||||
package repo serves. The site now ships a Cloudflare Pages `_headers` file, so
|
||||
its response security headers are declared in the repo instead of being whatever
|
||||
the edge defaults to — unverified in production until the next deploy. Every
|
||||
lint-class check now runs inside a container and nowhere else: `script/lint` and
|
||||
`script/fmt-check` build stages of `Dockerfile.lint`, with no host path to fall
|
||||
back to.
|
||||
the edge defaults to, and confirmed live in production on both hostnames. The
|
||||
lint now runs inside a container and nowhere else: `script/lint` is a build of
|
||||
`Dockerfile.lint`, with no host path to fall back to. Formatting is not a lint
|
||||
and stays on the host. Every entrypoint the README documents now has a
|
||||
`Makefile` target, so the org-wide "use make targets, never the underlying tool"
|
||||
rule is satisfiable for the CI build as well as for the everyday checks.
|
||||
|
||||
# Next Step
|
||||
|
||||
Add the missing `cibuild` and `precommit` shims to the `Makefile`, so that every
|
||||
documented entrypoint has a make target and the documented "always use make
|
||||
targets" rule is actually satisfiable
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/34). Done when `make cibuild` and
|
||||
`make precommit` exist, are declared `.PHONY`, and the README Entrypoints
|
||||
section matches.
|
||||
Make the prettier scope's exclusion of dot-directories explicit instead of
|
||||
leaning on `.gitignore` (https://git.eeqj.de/sneak/lora.vegas/issues/33).
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-10: moved every lint-class check into Docker
|
||||
- 2026-08-10: added the `cibuild` and `precommit` targets to the `Makefile`
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/34). Both scripts already
|
||||
existed, were already documented, and are the two entrypoints a contributor is
|
||||
most likely to be told to run — the CI build and what the pre-commit hook runs
|
||||
— yet neither had a make target, so the standing rule to use make targets
|
||||
rather than the underlying tool could not be followed for either. It bit
|
||||
reviewers twice, most recently on
|
||||
https://git.eeqj.de/sneak/lora.vegas/pulls/32, and it bit hardest for the
|
||||
build: since https://git.eeqj.de/sneak/lora.vegas/issues/30 a bare
|
||||
`docker build .` fails closed on the `CHECK_EPOCH` guard, so `script/cibuild`
|
||||
is one of only three supported ways to build an image and was the only one
|
||||
without a target. The two targets are thin shims in the existing style and
|
||||
change nothing about what the scripts do. `.PHONY` was already complete and
|
||||
now lists both. `README.md`'s Entrypoints section gained the script-to-target
|
||||
mapping, including the two names that do not match —
|
||||
`script/install-precommit` is `make hooks`, and `script/precommit` is
|
||||
`make precommit` — plus a note that `make cibuild` is the slowest target
|
||||
because it is the only one that runs two container builds, while still taking
|
||||
seconds once the shared `script/bootstrap` layer is cached. The section's
|
||||
pre-existing `script/install-precommit` bullet, which claimed the installed
|
||||
hook runs `script/check`, now says `script/precommit`, which is what the
|
||||
script actually writes into `.git/hooks/pre-commit`. The stale Future Step
|
||||
asking for post-deploy confirmation of `static/_headers` is dropped here: it
|
||||
was confirmed live on both hostnames
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/14). Verified that `make cibuild`
|
||||
earns its green rather than replaying a warm cache: one invocation ran both
|
||||
builds for real in sequence, each with its own distinct `CHECK_EPOCH`, with
|
||||
`RUN script/bootstrap` `CACHED` above and no check layer cached below it — the
|
||||
`Dockerfile.lint` build echoed its epoch and printed hugo's own build table,
|
||||
then the main image build echoed a different epoch, ran `script/test` for the
|
||||
production build and `script/fmt-check` for the formatting check.
|
||||
`make precommit` passes on a clean tree, and `make check` passes
|
||||
- 2026-08-10: moved the lint into Docker
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/38). A new root `Dockerfile.lint`
|
||||
carries the checks as build steps — a `lint` stage running
|
||||
`hugo --minify --printPathWarnings` and a `fmt-check` stage running the
|
||||
prettier check — on a shared `base` stage whose first four instructions are
|
||||
byte-identical to the main `Dockerfile`'s, so the expensive
|
||||
`RUN script/bootstrap` layer that compiles Hugo from source is a cache hit
|
||||
against the main image rather than a second build of the same thing.
|
||||
`script/lint` and `script/fmt-check` are now nothing but a `docker build` of
|
||||
their stage; there is no host path and deliberately no "already inside a
|
||||
container?" branch, which would be a host lint path in disguise. The recursion
|
||||
this creates was resolved by splitting the checks by where they run rather
|
||||
than by adding 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`, so CI still covers all
|
||||
three and cannot drift from what a developer runs. `script/fmt` stays on the
|
||||
host because it rewrites the working tree, which makes it the authoritative
|
||||
copy of the prettier version and flags that the `fmt-check` stage duplicates;
|
||||
both sides carry a keep-in-sync note, and that duplication is forced, since
|
||||
any `RUN script/fmt-check` inside the image is the recursion again. Caching is
|
||||
waived for the checks exactly as the main `Dockerfile` already does it:
|
||||
`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. All four image-building entrypoints now
|
||||
generate and pass it — `script/cibuild`, `script/docker`, `script/lint`,
|
||||
`script/fmt-check` — which is the failure mode this repo already hit once, a
|
||||
Dockerfile guard asserting a property one entrypoint did not supply. Verified
|
||||
rather than assumed: two consecutive `script/lint` runs on an unchanged tree
|
||||
both executed hugo for real (second run 2.9s wall, `RUN script/bootstrap`
|
||||
`CACHED`, distinct epoch echoed, `Total in 37 ms` printed), a constant-epoch
|
||||
counterfactual restored the false green (exit 0 in 0.25s, lint layer `CACHED`,
|
||||
no hugo output at all), an empty epoch failed closed on the guard, a broken
|
||||
template failed the lint stage with hugo's own render error, and an over-long
|
||||
line appended to `README.md` failed the fmt-check stage with
|
||||
runs `hugo --minify --printPathWarnings` as a build step, so a successful
|
||||
build is a clean lint, and `script/lint` is nothing but a build of that file —
|
||||
no host path and deliberately no "already inside a container?" branch, which
|
||||
would be a host lint path in disguise. The containerisation boundary is lint
|
||||
only, per the owner ruling of the same day: formatting is not a lint, so
|
||||
`script/fmt` and `script/fmt-check` stay on the host. `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 have
|
||||
no ordering edge, so any second stage beside the lint would be silently
|
||||
skipped by the invocation the canonical org-wide `script/lint` uses — a green
|
||||
that linted nothing. With one stage there is nothing to skip and `script/lint`
|
||||
needs no `--target`. Its first four instructions are byte-identical to the
|
||||
main `Dockerfile`'s, so the expensive `RUN script/bootstrap` layer that
|
||||
compiles Hugo from source is shared between the two images rather than paid
|
||||
twice. The recursion this creates was resolved by direction, not detection:
|
||||
`make check` calls `script/lint`, so the main `Dockerfile` can no longer
|
||||
`RUN make check` — that would attempt a docker build inside a build step, in a
|
||||
bare Alpine with no docker client and no daemon socket. It runs the individual
|
||||
non-lint checks instead, `script/test` and `script/fmt-check`, matching the
|
||||
canonical shape upstream, and `script/cibuild` runs `script/lint` first for
|
||||
fail-fast feedback before the main image build starts. So CI still covers all
|
||||
three checks and cannot drift from what a developer runs. Caching is waived
|
||||
for the lint exactly as the main `Dockerfile` already does it:
|
||||
`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
|
||||
handling of an unreferenced `ARG`. Every image-building entrypoint generates
|
||||
and passes it — `script/cibuild`, `script/docker`, `script/lint` — which is
|
||||
the failure mode this repo already hit once, a Dockerfile guard asserting a
|
||||
property one entrypoint did not supply. `script/lint` builds with
|
||||
`--output type=cacheonly`: the build is run for its exit status, not for an
|
||||
image, and since the lint layer is cache-busted every run an exporting build
|
||||
would leave one dangling image per lint on a host shared with other work.
|
||||
Verified rather than assumed: two consecutive `script/lint` runs on an
|
||||
unchanged tree both executed hugo for real (second run 0.85s wall,
|
||||
`RUN script/bootstrap` `CACHED`, distinct epochs echoed, real build tables
|
||||
printed); a whole-file `docker build -f Dockerfile.lint .` with the argument
|
||||
and no `--target` ran the lint for real, which is the regression test for the
|
||||
skipped-sibling hazard; 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 in 0.6s without the main image build starting
|
||||
at all; a planted over-long line failed the host `script/fmt-check` with
|
||||
`[warn] README.md`; both violations were reverted and re-run clean. Not
|
||||
changed here, and still true: the lint stage fails on hugo build errors but
|
||||
not on render-target collisions, which `--printPathWarnings` only prints
|
||||
changed here, and still true: the lint fails on hugo build errors but not on
|
||||
render-target collisions, which `--printPathWarnings` only prints
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/25) — containerising the run
|
||||
neither fixes nor worsens that
|
||||
- 2026-08-10: added the `LICENSE` file and made the README say what it says
|
||||
@@ -276,8 +313,6 @@ section matches.
|
||||
Startable work first. Everything under "Blocked" waits on somebody or something
|
||||
outside this repo, so nothing there may be picked up as the Next Step.
|
||||
|
||||
- Make the prettier scope's exclusion of dot-directories explicit instead of
|
||||
leaning on `.gitignore` (https://git.eeqj.de/sneak/lora.vegas/issues/33)
|
||||
- Fix the README's SSH-only clone URL, and add the two entrypoints the
|
||||
Entrypoints section omits, `script/precommit` and `script/projectname`
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/36)
|
||||
@@ -307,17 +342,6 @@ outside this repo, so nothing there may be picked up as the Next Step.
|
||||
- Delete the stale remote branches `feat/initial-site` and `security-audit`;
|
||||
only the owner can remove them
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/15)
|
||||
- After the next deploy, confirm the `_headers` file actually took effect, on
|
||||
both `https://lora.vegas/` and `https://www.lora.vegas/`: `curl -sSI` against
|
||||
each must show `strict-transport-security` or `content-security-policy`.
|
||||
Cloudflare Pages silently ignores a malformed `_headers`, and checking
|
||||
`x-content-type-options` would pass either way because the edge sends it
|
||||
regardless. `www` has to be checked too and not just the apex: dropping
|
||||
`includeSubDomains` rests on `www.lora.vegas` being served by this same Pages
|
||||
project, which was established behaviourally from identical response bodies
|
||||
rather than from the Cloudflare dashboard. If `www` turns out not to be
|
||||
covered, the `includeSubDomains` decision has to be revisited
|
||||
(https://git.eeqj.de/sneak/lora.vegas/issues/14)
|
||||
- Decide the HSTS `includeSubDomains` and `preload` posture for `lora.vegas`.
|
||||
Both are owner calls: neither can be walked back inside the max-age window,
|
||||
and `includeSubDomains` binds hostnames this repo does not control
|
||||
|
||||
11
script/check
11
script/check
@@ -4,10 +4,13 @@
|
||||
# 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.
|
||||
# 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,28 +1,30 @@
|
||||
#!/bin/sh
|
||||
# script/cibuild: run the CI build. The Gitea workflow runs this on
|
||||
# push, and it is the single entrypoint that covers everything:
|
||||
# push, and it is the single entrypoint that covers everything. Two
|
||||
# container builds, in order:
|
||||
#
|
||||
# 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
|
||||
# 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)
|
||||
#
|
||||
# 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.
|
||||
# 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 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.
|
||||
# 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
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
@@ -30,6 +32,8 @@ 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
|
||||
@@ -40,9 +44,6 @@ 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 "$@"
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
# 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.
|
||||
# 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)"
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
#!/bin/sh
|
||||
# script/fmt-check: check the formatting of this repo's markdown and
|
||||
# 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.
|
||||
# CSS (read-only). Same scope and same settings as script/fmt - keep
|
||||
# the two in sync - but fails instead of writing.
|
||||
#
|
||||
# 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.
|
||||
# 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)"
|
||||
|
||||
PRETTIER_VERSION="3.4.2"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
epoch="$(date +%s%N)$$"
|
||||
docker build -f Dockerfile.lint --target fmt-check \
|
||||
--build-arg CHECK_EPOCH="$epoch" .
|
||||
npx --yes "prettier@${PRETTIER_VERSION}" --check \
|
||||
'**/*.md' '**/*.css' --tab-width 4 --prose-wrap always
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
28
script/lint
28
script/lint
@@ -3,14 +3,18 @@
|
||||
# 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.
|
||||
# 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 check layer must not be allowed to cache, and why the value is
|
||||
# the lint layer must not be allowed to cache, and why the value is
|
||||
# built in an assignment rather than inline.
|
||||
set -eu
|
||||
|
||||
@@ -19,8 +23,18 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
epoch="$(date +%s%N)$$"
|
||||
docker build -f Dockerfile.lint --target lint \
|
||||
--build-arg CHECK_EPOCH="$epoch" .
|
||||
# --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