1 Commits
Author SHA1 Message Date
sneak 51ee510ed8 Gate the build on Docker lint and test phases (closes #40, closes #30)
check / check (push) Successful in 23s
Per the owner ruling on issue 40, linting and testing are phases of the
main Dockerfile rather than a separate lint file. script/lint and
script/test build one phase each by name with caching disabled, and the
final stage copies a harmless file from each so the image cannot be built
unless both passed. A stage that is not the last is built only when
something depends on it or --target names it, so the gates are invoked by
name and the edges kept. script/check runs the gates and builds no image
of its own; script/cibuild bootstraps first, because CI runs it alone and
fmt-check is native. fmt and fmt-check source nvm for the pinned node
before calling yarn, which bootstrap installs but leaves off its caller's
PATH. Every build in script/ is tagged and uncached. Issue 30 closes too:
a container has its own lint cache and lock.

Model: opus-5
2026-09-08 05:55:15 +00:00
6 changed files with 70 additions and 14 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ alpine. We provide:
a container) a container)
- `script/fmt-check` — check formatting (read-only; native) - `script/fmt-check` — check formatting (read-only; native)
- `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own - `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
extension); builds no image extension); builds no image of its own
- `script/docker` - `script/docker`
`docker build --no-cache --build-arg VERSION="$version" -t prompts .`, the tag `docker build --no-cache --build-arg VERSION="$version" -t prompts .`, the tag
coming from `script/projectname` (byte-identical across repos) coming from `script/projectname` (byte-identical across repos)
+9 -3
View File
@@ -103,6 +103,11 @@ with your task.
the image with `--no-cache`. Without the bootstrap the CI run dies in the image with `--no-cache`. Without the bootstrap the CI run dies in
`script/fmt-check`, which runs the formatter on the host and finds nothing `script/fmt-check`, which runs the formatter on the host and finds nothing
installed. installed.
- [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version
before invoking `yarn`, as `script/bootstrap`'s own install step does.
`script/bootstrap` leaves the node and yarn it installs off the `PATH` of
the shell that called it, so a bare `yarn` exits 127 on a runner carrying
nothing but docker and git.
- [ ] `script/bootstrap` installs no linter of its own — delete the block, its - [ ] `script/bootstrap` installs no linter of its own — delete the block, its
version variables and its call site. A JS repo's `yarn install` stays; it version variables and its call site. A JS repo's `yarn install` stays; it
brings a linter along with every other dependency, and no verdict is taken brings a linter along with every other dependency, and no verdict is taken
@@ -155,9 +160,10 @@ with your task.
# Final # Final
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `script/cibuild` succeeds in a fresh clone with nothing installed, which - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying nothing but
is what CI has, and demonstrably executed the checks — a sub-second build, docker and git, with no node or yarn on `PATH`, which is what CI has, and
or `CACHED` on a gate layer, means nothing ran demonstrably executed the checks — a sub-second build, or `CACHED` on a
gate layer, means nothing ran
- [ ] A planted lint violation fails both `make lint` and a plain - [ ] A planted lint violation fails both `make lint` and a plain
`docker build .`; revert it afterwards `docker build .`; revert it afterwards
- [ ] Commit and merge fixes before starting your actual task - [ ] Commit and merge fixes before starting your actual task
+9 -3
View File
@@ -125,6 +125,11 @@ are thin shims calling them. Model scripts:
`docker build --no-cache --build-arg VERSION="$version" .` (what CI runs). `docker build --no-cache --build-arg VERSION="$version" .` (what CI runs).
The bootstrap is required: CI checks out and runs this alone, and The bootstrap is required: CI checks out and runs this alone, and
`script/fmt-check` runs the formatter on the host. `script/fmt-check` runs the formatter on the host.
- [ ] `script/fmt` and `script/fmt-check` source nvm for the pinned node version
before invoking `yarn`, as `script/bootstrap`'s own install step does.
`script/bootstrap` leaves the node and yarn it installs off the `PATH` of
the shell that called it, so a bare `yarn` exits 127 on a runner carrying
nothing but docker and git.
- [ ] Every `docker build` in `script/` is tagged, so no invocation leaves a - [ ] Every `docker build` in `script/` is tagged, so no invocation leaves a
dangling image behind dangling image behind
- [ ] `script/precommit` — called by the pre-commit hook; runs `script/check` - [ ] `script/precommit` — called by the pre-commit hook; runs `script/check`
@@ -138,9 +143,10 @@ are thin shims calling them. Model scripts:
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `make docker` succeeds - [ ] `make docker` succeeds
- [ ] `script/cibuild` succeeds in a fresh clone with nothing installed, which - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying nothing but
is what CI has, and demonstrably executed the checks — a sub-second build, docker and git, with no node or yarn on `PATH`, which is what CI has, and
or `CACHED` on a gate layer, means nothing ran demonstrably executed the checks — a sub-second build, or `CACHED` on a
gate layer, means nothing ran
- [ ] Plant a lint violation and confirm both `make lint` and a plain - [ ] Plant a lint violation and confirm both `make lint` and a plain
`docker build .` fail on it; revert. A plain build that passes proves the `docker build .` fail on it; revert. A plain build that passes proves the
final stage is missing its `COPY --from=` edge to the gate phases. final stage is missing its `COPY --from=` edge to the gate phases.
+11 -5
View File
@@ -65,11 +65,17 @@ style conventions are in separate documents:
`script/bootstrap` first**, because the workflow checks out the repo and runs `script/bootstrap` first**, because the workflow checks out the repo and runs
nothing else, while `script/fmt-check` runs the formatter on the host: on a nothing else, while `script/fmt-check` runs the formatter on the host: on a
pristine checkout with nothing installed the run dies there, after the pristine checkout with nothing installed the run dies there, after the
containerised gates have passed. Four further scripts are our own extensions containerised gates have passed. **The bootstrap alone is not enough**:
to the standard: `script/check` runs `script/test`, `script/lint` and `script/bootstrap` installs node and yarn under nvm and leaves neither on the
`script/fmt-check`; `script/precommit` is what the git pre-commit hook runs, `PATH` of the shell that called it, so a bare `yarn` still exits 127. The host
and it calls `script/check`; `script/install-precommit` installs the git entrypoints that need yarn — `script/fmt` and `script/fmt-check` — therefore
pre-commit hook (the `make hooks` target shims to it); and source nvm for the pinned node version before invoking it, exactly as
`script/bootstrap`'s own install step does. A runner carrying nothing but
docker and git then gets through `script/check`. Four further scripts are our
own extensions to the standard: `script/check` runs `script/test`,
`script/lint` and `script/fmt-check`; `script/precommit` is what the git
pre-commit hook runs, and it calls `script/check`; `script/install-precommit`
installs the git pre-commit hook (the `make hooks` target shims to it); and
`script/projectname` (literally that filename) simply outputs the project's `script/projectname` (literally that filename) simply outputs the project's
name. Scripts that need the name call `script/projectname` — e.g. name. Scripts that need the name call `script/projectname` — e.g.
`script/docker` assembles its image tag from it — so those scripts stay `script/docker` assembles its image tag from it — so those scripts stay
+20 -1
View File
@@ -4,9 +4,28 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# script/bootstrap installs node and yarn under nvm and leaves neither
# on the PATH of the shell that called it, so resolve the pinned
# toolchain here the way bootstrap's own install step does. nvm is a
# bash script, hence the subshell.
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
exec yarn "$@"
fi
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt: no yarn; run script/bootstrap first" >&2
exit 1
fi
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
}
main() { main() {
cd "$ROOT" cd "$ROOT"
yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"
+20 -1
View File
@@ -4,9 +4,28 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# script/bootstrap installs node and yarn under nvm and leaves neither
# on the PATH of the shell that called it, so resolve the pinned
# toolchain here the way bootstrap's own install step does. nvm is a
# bash script, hence the subshell.
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
exec yarn "$@"
fi
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt-check: no yarn; run script/bootstrap first" >&2
exit 1
fi
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
}
main() { main() {
cd "$ROOT" cd "$ROOT"
yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"