1 Commits
Author SHA1 Message Date
sneak c2c58ea349 Install the pinned node when the installed one is another major version (closes #118)
check / check (push) Canceled after 0s
script/bootstrap used whatever node was installed, so on the CI runner
image, which ships node 24, the pinned yarn 1.22.22 ran under node 24
and printed the deprecation warning DEP0169 on every bootstrap. It now
uses the installed node only when its major version is the pinned one,
and otherwise installs the pinned node with nvm, as it already did when
node was missing, with yarn and the packages under it. Only the major is
compared because the Dockerfile stages use a node 22 alpine image that
nvm cannot replace. script/fmt and script/fmt-check carry the same test
to pick their yarn: the yarn on PATH, or yarn under the pinned node
loaded through nvm.

Model: opus-5-5
2026-10-08 01:06:25 +00:00
7 changed files with 91 additions and 55 deletions
+8 -4
View File
@@ -28,10 +28,14 @@ fmt-check, and commit.
yarn 1.22.22 printed the deprecation warning DEP0169 on every bootstrap. Only yarn 1.22.22 printed the deprecation warning DEP0169 on every bootstrap. Only
the major is compared because the `Dockerfile` stages start from a node 22 the major is compared because the `Dockerfile` stages start from a node 22
alpine image whose exact version is not the pin, and nvm cannot install a alpine image whose exact version is not the pin, and nvm cannot install a
prebuilt node on alpine. `script/fmt` and `script/fmt-check` now run yarn prebuilt node on alpine; the version-comparison rule in `REPO_POLICIES.md`
under nvm's pinned node when nvm has it installed, and otherwise the `yarn` on names node as its exception. `script/fmt` and `script/fmt-check` pick their
`PATH`. `REPO_POLICIES.md` and both checklists say so. Not yet tried on the yarn with the same test: the `yarn` on `PATH` when the installed node is the
shared runner. Repositories pick this up on their next re-vendor. pinned major, and otherwise yarn under the pinned node through nvm.
`REPO_POLICIES.md` and both checklists say so, and the checklists' final
`script/cibuild` check names what the CI runner image has: node 24 and no
yarn. Not yet tried on the shared runner. Repositories pick this up on their
next re-vendor.
- 2026-10-07: The `script/cibuild` item in `NEW_REPO_CHECKLIST.md` now matches - 2026-10-07: The `script/cibuild` item in `NEW_REPO_CHECKLIST.md` now matches
the canonical `script/cibuild` (issue 125). Its image build carries the tag, the canonical `script/cibuild` (issue 125). Its image build carries the tag,
`-t "$tag"`, and the item says that `$version` comes from `-t "$tag"`, and the item says that `$version` comes from
+12 -9
View File
@@ -149,11 +149,13 @@ 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` run `yarn` under nvm's pinned node - [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test
when nvm has that version installed, and otherwise the `yarn` on `PATH`. as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
`script/bootstrap` leaves the node and yarn it installs under nvm off the the pinned major version, and otherwise yarn under the pinned node, with
`PATH` of the shell that called it, so a bare `yarn` exits 127 on a runner nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
carrying nothing but docker and git, or runs under another node. and yarn it installs under nvm off the `PATH` of the shell that called it,
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
git, or runs under another node.
- [ ] `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
@@ -206,10 +208,11 @@ with your task.
# Final # Final
- [ ] `make check` passes - [ ] `make check` passes
- [ ] `script/cibuild` succeeds in a fresh clone on a host carrying nothing but - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
docker and git, with no node or yarn on `PATH`, which is what CI has, and (the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
demonstrably executed the checks — a sub-second build, or `CACHED` on a git and node 24, with no yarn on `PATH`), and demonstrably executed the
gate layer, means nothing ran 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
+12 -9
View File
@@ -171,11 +171,13 @@ are thin shims calling them. Model scripts:
on its own line before the build. The bootstrap is required: CI checks out on its own line before the build. The bootstrap is required: CI checks out
and runs this alone, and `script/fmt-check` runs the formatter on the and runs this alone, and `script/fmt-check` runs the formatter on the
host. host.
- [ ] `script/fmt` and `script/fmt-check` run `yarn` under nvm's pinned node - [ ] `script/fmt` and `script/fmt-check` pick their `yarn` with the same test
when nvm has that version installed, and otherwise the `yarn` on `PATH`. as `script/bootstrap`: the `yarn` on `PATH` when the node on `PATH` has
`script/bootstrap` leaves the node and yarn it installs under nvm off the the pinned major version, and otherwise yarn under the pinned node, with
`PATH` of the shell that called it, so a bare `yarn` exits 127 on a runner nvm loaded through `$HOME/.nvm/nvm.sh`. `script/bootstrap` leaves the node
carrying nothing but docker and git, or runs under another node. and yarn it installs under nvm off the `PATH` of the shell that called it,
so a bare `yarn` exits 127 on a runner carrying nothing but docker and
git, or runs under another node.
- [ ] No `docker build` in `script/` leaves a dangling image behind: - [ ] No `docker build` in `script/` leaves a dangling image behind:
`script/lint` and `script/test` write no image, and `script/docker` and `script/lint` and `script/test` write no image, and `script/docker` and
`script/cibuild` tag theirs `script/cibuild` tag theirs
@@ -190,10 +192,11 @@ 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 on a host carrying nothing but - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has
docker and git, with no node or yarn on `PATH`, which is what CI has, and (the runner image `docker.gitea.com/runner-images:ubuntu-latest`: docker,
demonstrably executed the checks — a sub-second build, or `CACHED` on a git and node 24, with no yarn on `PATH`), and demonstrably executed the
gate layer, means nothing ran 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.
+17 -11
View File
@@ -73,17 +73,18 @@ style conventions are in separate documents:
`script/bootstrap` installs node and yarn under nvm it leaves neither on the `script/bootstrap` installs node and yarn under nvm it leaves neither on the
`PATH` of the shell that called it, so a bare `yarn` either exits 127 or runs `PATH` of the shell that called it, so a bare `yarn` either exits 127 or runs
under another node. The host entrypoints that need yarn — `script/fmt` and under another node. The host entrypoints that need yarn — `script/fmt` and
`script/fmt-check` — therefore run it under nvm's pinned node when nvm has `script/fmt-check` — therefore use the same test as `script/bootstrap`: when
that version installed, and otherwise run the `yarn` on `PATH`. A runner the node on `PATH` has the pinned major version they run the `yarn` on `PATH`,
carrying nothing but docker and git then gets through `script/check`. Four and otherwise they load nvm through `$HOME/.nvm/nvm.sh` and run yarn under the
further scripts are our own extensions to the standard: `script/check` runs pinned node. A runner carrying nothing but docker and git then gets through
`script/test`, `script/lint` and `script/fmt-check`; `script/precommit` is `script/check`. Four further scripts are our own extensions to the standard:
what the git pre-commit hook runs, and it calls `script/check`; `script/check` runs `script/test`, `script/lint` and `script/fmt-check`;
`script/install-precommit` installs the git pre-commit hook (the `make hooks` `script/precommit` is what the git pre-commit hook runs, and it calls
target shims to it); and `script/projectname` (literally that filename) simply `script/check`; `script/install-precommit` installs the git pre-commit hook
outputs the project's name. Scripts that need the name call (the `make hooks` target shims to it); and `script/projectname` (literally
`script/projectname` — e.g. `script/docker` assembles its image tag from it — that filename) simply outputs the project's name. Scripts that need the name
so those scripts stay byte-identical across all repos. Repo-type-specific call `script/projectname` — e.g. `script/docker` assembles its image tag from
it — so those scripts stay byte-identical across all repos. Repo-type-specific
pre-commit extras (e.g. `go mod tidy` verification in Go repos) belong in pre-commit extras (e.g. `go mod tidy` verification in Go repos) belong in
`script/precommit`, not in the hook itself. Model scripts are at `script/precommit`, not in the hook itself. Model scripts are at
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README
@@ -553,6 +554,11 @@ style conventions are in separate documents:
function defined and never invoked has the same exit status and the same function defined and never invoked has the same exit status and the same
empty output as one that worked. empty output as one that worked.
Node is the exception to the whole-token comparison: the canonical
`script/bootstrap` compares only its major version, because the node 22
alpine image the `Dockerfile` stages start from is not the exact pin, and
nvm cannot install a prebuilt node on alpine.
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`. Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
A Go tool a repo needs on the host is installed with `go install` pinned to A Go tool a repo needs on the host is installed with `go install` pinned to
+4 -2
View File
@@ -106,9 +106,11 @@ ensure_nvm() {
# major is compared: the Dockerfile stages start from a node 22 alpine # major is compared: the Dockerfile stages start from a node 22 alpine
# image whose exact version is not the pin, and nvm cannot install a # image whose exact version is not the pin, and nvm cannot install a
# prebuilt node on alpine. Another major is not used: the pinned yarn 1 # prebuilt node on alpine. Another major is not used: the pinned yarn 1
# prints a deprecation warning under node 24. # prints a deprecation warning under node 24. script/fmt and
# script/fmt-check carry this function unchanged, to run the yarn
# installed here.
node_is_pinned_major() { node_is_pinned_major() {
if missing node; then return 1; fi if ! command -v node >/dev/null 2>&1; then return 1; fi
installed="$(node --version)" installed="$(node --version)"
installed="${installed#v}" installed="${installed#v}"
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ] [ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
+19 -10
View File
@@ -7,21 +7,30 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap. # Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0" NODE_VERSION="22.17.0"
# When the installed node is not the pinned major version, # True when the node on PATH has the pinned major version: the same
# script/bootstrap installs the pinned node and yarn under nvm and # function as in script/bootstrap, which uses it to decide where it
# leaves neither on the PATH of the shell that called it. So yarn runs # installs yarn.
# under nvm's pinned node when nvm has it installed, and otherwise is node_is_pinned_major() {
# the yarn on PATH. nvm is a bash script, hence the subshell. if ! command -v node >/dev/null 2>&1; then return 1; fi
installed="$(node --version)"
installed="${installed#v}"
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
}
# Run the yarn script/bootstrap installed: the yarn on PATH when the
# node on PATH has the pinned major version, and otherwise yarn under
# the pinned node in nvm, which bootstrap leaves off the PATH of the
# shell that called it. nvm is a bash script, hence the subshell.
run_yarn() { run_yarn() {
if [ -d "$HOME/.nvm/versions/node/v$NODE_VERSION" ]; then if node_is_pinned_major; then
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && exec yarn "$@"
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
fi fi
if ! command -v yarn >/dev/null 2>&1; then if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt: no yarn; run script/bootstrap first" >&2 echo "fmt: no yarn; run script/bootstrap first" >&2
exit 1 exit 1
fi fi
exec yarn "$@" exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
} }
main() { main() {
+19 -10
View File
@@ -7,21 +7,30 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap. # Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0" NODE_VERSION="22.17.0"
# When the installed node is not the pinned major version, # True when the node on PATH has the pinned major version: the same
# script/bootstrap installs the pinned node and yarn under nvm and # function as in script/bootstrap, which uses it to decide where it
# leaves neither on the PATH of the shell that called it. So yarn runs # installs yarn.
# under nvm's pinned node when nvm has it installed, and otherwise is node_is_pinned_major() {
# the yarn on PATH. nvm is a bash script, hence the subshell. if ! command -v node >/dev/null 2>&1; then return 1; fi
installed="$(node --version)"
installed="${installed#v}"
[ "${installed%%.*}" = "${NODE_VERSION%%.*}" ]
}
# Run the yarn script/bootstrap installed: the yarn on PATH when the
# node on PATH has the pinned major version, and otherwise yarn under
# the pinned node in nvm, which bootstrap leaves off the PATH of the
# shell that called it. nvm is a bash script, hence the subshell.
run_yarn() { run_yarn() {
if [ -d "$HOME/.nvm/versions/node/v$NODE_VERSION" ]; then if node_is_pinned_major; then
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && exec yarn "$@"
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
fi fi
if ! command -v yarn >/dev/null 2>&1; then if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt-check: no yarn; run script/bootstrap first" >&2 echo "fmt-check: no yarn; run script/bootstrap first" >&2
exit 1 exit 1
fi fi
exec yarn "$@" exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
} }
main() { main() {