Bootstrap keeps yarn and node only at their pinned versions (closes #154)
check / check (push) Canceled after 0s

script/bootstrap kept any yarn found on PATH, so YARN_VERSION applied
only where none was installed. It now keeps the yarn on PATH only when
yarn --version is exactly YARN_VERSION, and otherwise prepares that
version through corepack. The node on PATH is used only when it has the
pinned major version and is at least NODE_MIN_VERSION, as
REPO_POLICIES.md asks. After installing either, bootstrap looks the tool
up through PATH again, stops naming the path if it is not the pinned
one, and prints the version it uses, as it does for Go. script/fmt and
script/fmt-check use the same node test, and otherwise run prettier
under the pinned node in nvm.

Model: opus-5-5
This commit is contained in:
2026-10-19 03:57:54 +00:00
parent f5aa81f024
commit c8e65f2468
5 changed files with 149 additions and 22 deletions
+10 -6
View File
@@ -41,11 +41,13 @@ The scripts cover both the frontend and the Go backend, so `make check` fails if
either one is broken. We provide: either one is broken. We provide:
- `script/bootstrap` — install all dependencies (the pinned node via nvm unless - `script/bootstrap` — install all dependencies (the pinned node via nvm unless
one new enough for the frontend's dependencies is installed, yarn via the `node` on `PATH` has the pinned major version and is new enough for the
corepack, `yarn install --frozen-lockfile`, Go `GO_VERSION` unless the `go` on frontend's dependencies, yarn `YARN_VERSION` via corepack unless the `yarn` on
`PATH` is exactly that version, and the Go modules), linking what it installs `PATH` is exactly that version, `yarn install --frozen-lockfile`, Go
itself into `~/.local/bin`, which has to be on `PATH`. It installs no Go `GO_VERSION` unless the `go` on `PATH` is exactly that version, and the Go
linter and not Docker: `make test` and `make lint` run in Docker modules), linking what it installs itself into `~/.local/bin`, which has to be
on `PATH`. It installs no Go linter and not Docker: `make test` and
`make lint` run in Docker
- `script/setup` — make a fresh clone ready for development: bootstrap plus the - `script/setup` — make a fresh clone ready for development: bootstrap plus the
git pre-commit hook git pre-commit hook
- `script/dev` — run the Vite dev server, which proxies `/api` to a locally - `script/dev` — run the Vite dev server, which proxies `/api` to a locally
@@ -66,7 +68,9 @@ either one is broken. We provide:
eslint in its `frontend-lint` stage, and golangci-lint over the Go packages eslint in its `frontend-lint` stage, and golangci-lint over the Go packages
- `script/fmt` — format all files (writes): prettier over the JavaScript, CSS, - `script/fmt` — format all files (writes): prettier over the JavaScript, CSS,
HTML and Markdown, then gofmt over `cmd/` and `internal/`. It runs on the HTML and Markdown, then gofmt over `cmd/` and `internal/`. It runs on the
host, as `script/fmt-check` does, with `~/.local/bin` put on `PATH` host, as `script/fmt-check` does, with `~/.local/bin` put on `PATH`, and runs
prettier under the pinned node in nvm when the `node` on `PATH` is not one
`script/bootstrap` would use
- `script/fmt-check` — check formatting (read-only): prettier, then gofmt - `script/fmt-check` — check formatting (read-only): prettier, then gofmt
- `script/check` — run test, lint, and fmt-check - `script/check` — run test, lint, and fmt-check
- `script/add-dependency` — add a frontend package, or move one to another - `script/add-dependency` — add a frontend package, or move one to another
+11
View File
@@ -21,6 +21,17 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
# Completed Steps # Completed Steps
- 2026-10-19: `script/bootstrap` keeps the `yarn` on `PATH` only when
`yarn --version` is exactly `YARN_VERSION`, and otherwise prepares that
version through corepack, as it did when no yarn was installed
([#154](https://git.eeqj.de/sneak/netwatch/issues/154)); any yarn used to be
kept. It uses the `node` on `PATH` only when that node has the major version
of `NODE_VERSION` as well as being at least `NODE_MIN_VERSION`, as
`REPO_POLICIES.md` asks; a node 24 was kept before. After installing node or
yarn it looks the tool up through `PATH` again and stops if that is not the
pinned one, as it does for Go, and it prints the node and yarn versions it
uses. `script/fmt` and `script/fmt-check` use the same node test, and
otherwise run prettier under the pinned node in nvm
- 2026-10-19: `DEBUG` unset or empty leaves debug logging off, and any other - 2026-10-19: `DEBUG` unset or empty leaves debug logging off, and any other
value, `false` and `0` included, turns it on, as the code style guide says value, `false` and `0` included, turns it on, as the code style guide says
([#159](https://git.eeqj.de/sneak/netwatch/issues/159)). No value of it stops ([#159](https://git.eeqj.de/sneak/netwatch/issues/159)). No value of it stops
+56 -14
View File
@@ -3,11 +3,13 @@
# this repo. Idempotent: every install is guarded by a check so already # this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tooling comes from nix, apt, brew, # installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present. Node is # or apk (detected in that order); assumes nothing is present. Node is
# used directly if it is at least NODE_MIN_VERSION; otherwise it is # used directly only if it has the major version of NODE_VERSION and is
# installed at a pinned version via nvm (installing nvm itself first, # at least NODE_MIN_VERSION; otherwise NODE_VERSION is installed via nvm
# from a hash-verified release archive, never curl | sh). Go, with its # (installing nvm itself first, from a hash-verified release archive,
# gofmt, is used directly only if it is exactly GO_VERSION; otherwise # never curl | sh). Yarn is used directly only if it is exactly
# that release is installed from its hash-verified archive. # YARN_VERSION; otherwise that version is prepared through corepack.
# Go, with its gofmt, is used directly only if it is exactly GO_VERSION;
# otherwise that release is installed from its hash-verified archive.
# #
# What this script installs outside the system package manager lives # What this script installs outside the system package manager lives
# under $HOME and is linked into ~/.local/bin, where make and the git # under $HOME and is linked into ~/.local/bin, where make and the git
@@ -142,32 +144,65 @@ ensure_nvm() {
rm -rf "$tmp" rm -rf "$tmp"
} }
# node_ok: the node on PATH is at least NODE_MIN_VERSION. node itself # node_ok: the node on PATH has the major version of NODE_VERSION and is
# compares the two: major, then minor, then patch. # at least NODE_MIN_VERSION. Only the major is compared with the pin:
# the Dockerfile's node stages start from a node 22 alpine image whose
# exact version is not the pin, and nvm cannot install a prebuilt node
# on alpine. node itself compares the versions: major, then minor, then
# patch. A node that fails does not pass. script/bootstrap, script/fmt
# and script/fmt-check carry this function unchanged.
node_ok() { node_ok() {
if missing node; then return 1; fi if ! command -v node >/dev/null 2>&1; then return 1; fi
node -e ' node -e '
const have = process.versions.node.split(".").map(Number); const have = process.versions.node.split(".").map(Number);
const want = process.argv[1].split(".").map(Number); const want = process.argv[1].split(".").map(Number);
const pinned = process.argv[2].split(".").map(Number);
if (have[0] !== pinned[0]) process.exit(1);
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1); if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1);
} }
' "$NODE_MIN_VERSION" ' "$NODE_MIN_VERSION" "$NODE_VERSION"
} }
# ensure_node: unless node_ok, install NODE_VERSION and link its node. # ensure_node: unless node_ok, install NODE_VERSION and link its node.
# Afterwards node is looked up through PATH again and must pass node_ok.
ensure_node() { ensure_node() {
if node_ok; then return 0; fi if node_ok; then
echo "bootstrap: using node $(node --version)"
return 0
fi
ensure_nvm ensure_nvm
nvm_sh "nvm install $NODE_VERSION" nvm_sh "nvm install $NODE_VERSION"
link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node
hash -r
if ! node_ok; then
echo "bootstrap: after installing node $NODE_VERSION through nvm," >&2
echo " the node on PATH, $(command -v node), is not it" >&2
exit 1
fi
echo "bootstrap: installed node $(node --version)"
} }
# ensure_yarn: corepack writes its shims (pnpm and yarnpkg as well as # yarn_ok: yarn --version on PATH prints exactly YARN_VERSION, compared
# yarn) into $TOOLCHAIN rather than next to itself, and the npm fallback # over the whole version. A yarn that fails or prints anything else does
# installs there too; only yarn is linked. # not pass.
yarn_ok() {
if missing yarn; then return 1; fi
version="$(yarn --version 2>/dev/null)" || return 1
[ "$version" = "$YARN_VERSION" ]
}
# ensure_yarn: unless yarn_ok, prepare YARN_VERSION and link its yarn.
# corepack writes its shims (pnpm and yarnpkg as well as yarn) into
# $TOOLCHAIN rather than next to itself, and the npm fallback installs
# there too; only yarn is linked. Afterwards yarn is looked up through
# PATH again and must pass yarn_ok, so another yarn that hides the link
# stops bootstrap.
ensure_yarn() { ensure_yarn() {
if ! missing yarn; then return 0; fi if yarn_ok; then
echo "bootstrap: using yarn $(yarn --version)"
return 0
fi
shims="$TOOLCHAIN/corepack-shims" shims="$TOOLCHAIN/corepack-shims"
mkdir -p "$shims" mkdir -p "$shims"
if ! missing corepack; then if ! missing corepack; then
@@ -182,6 +217,13 @@ ensure_yarn() {
shims="$TOOLCHAIN/npm-global/bin" shims="$TOOLCHAIN/npm-global/bin"
fi fi
link_bin "$shims/yarn" yarn link_bin "$shims/yarn" yarn
hash -r
if ! yarn_ok; then
echo "bootstrap: after installing yarn $YARN_VERSION in $shims," >&2
echo " the yarn on PATH, $(command -v yarn), does not report it" >&2
exit 1
fi
echo "bootstrap: installed yarn $(yarn --version)"
} }
# go_ok: the go on PATH has its gofmt beside it (a Go release ships the # go_ok: the go on PATH has its gofmt beside it (a Go release ships the
+36 -1
View File
@@ -8,12 +8,47 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pins in script/bootstrap.
NODE_VERSION="22.17.0"
NODE_MIN_VERSION="22.13.0"
# node_ok: the node on PATH has the major version of NODE_VERSION and is
# at least NODE_MIN_VERSION. Only the major is compared with the pin:
# the Dockerfile's node stages start from a node 22 alpine image whose
# exact version is not the pin, and nvm cannot install a prebuilt node
# on alpine. node itself compares the versions: major, then minor, then
# patch. A node that fails does not pass. script/bootstrap, script/fmt
# and script/fmt-check carry this function unchanged.
node_ok() {
if ! command -v node >/dev/null 2>&1; then return 1; fi
node -e '
const have = process.versions.node.split(".").map(Number);
const want = process.argv[1].split(".").map(Number);
const pinned = process.argv[2].split(".").map(Number);
if (have[0] !== pinned[0]) process.exit(1);
for (let i = 0; i < 3; i++) {
if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1);
}
' "$NODE_MIN_VERSION" "$NODE_VERSION"
}
main() { main() {
cd "$ROOT" cd "$ROOT"
# script/bootstrap links the node, yarn and gofmt it installs into # script/bootstrap links the node, yarn and gofmt it installs into
# ~/.local/bin, which the shell that called it may not have on PATH. # ~/.local/bin, which the shell that called it may not have on PATH.
PATH="$HOME/.local/bin:$PATH" PATH="$HOME/.local/bin:$PATH"
"$ROOT/script/frontend-fmt" # prettier runs under a node script/bootstrap would use: the node on
# PATH when it passes node_ok, and otherwise the pinned node in nvm.
# nvm is a bash script, hence the bash.
if node_ok; then
"$ROOT/script/frontend-fmt"
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && "$2"' \
bash "$NODE_VERSION" "$ROOT/script/frontend-fmt"
else
echo "fmt: no node script/bootstrap would use; run it first" >&2
exit 1
fi
gofmt -l -w cmd internal gofmt -l -w cmd internal
} }
+36 -1
View File
@@ -6,12 +6,47 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pins in script/bootstrap.
NODE_VERSION="22.17.0"
NODE_MIN_VERSION="22.13.0"
# node_ok: the node on PATH has the major version of NODE_VERSION and is
# at least NODE_MIN_VERSION. Only the major is compared with the pin:
# the Dockerfile's node stages start from a node 22 alpine image whose
# exact version is not the pin, and nvm cannot install a prebuilt node
# on alpine. node itself compares the versions: major, then minor, then
# patch. A node that fails does not pass. script/bootstrap, script/fmt
# and script/fmt-check carry this function unchanged.
node_ok() {
if ! command -v node >/dev/null 2>&1; then return 1; fi
node -e '
const have = process.versions.node.split(".").map(Number);
const want = process.argv[1].split(".").map(Number);
const pinned = process.argv[2].split(".").map(Number);
if (have[0] !== pinned[0]) process.exit(1);
for (let i = 0; i < 3; i++) {
if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1);
}
' "$NODE_MIN_VERSION" "$NODE_VERSION"
}
main() { main() {
cd "$ROOT" cd "$ROOT"
# script/bootstrap links the node, yarn and gofmt it installs into # script/bootstrap links the node, yarn and gofmt it installs into
# ~/.local/bin, which the shell that called it may not have on PATH. # ~/.local/bin, which the shell that called it may not have on PATH.
PATH="$HOME/.local/bin:$PATH" PATH="$HOME/.local/bin:$PATH"
"$ROOT/script/frontend-fmt-check" # prettier runs under a node script/bootstrap would use: the node on
# PATH when it passes node_ok, and otherwise the pinned node in nvm.
# nvm is a bash script, hence the bash.
if node_ok; then
"$ROOT/script/frontend-fmt-check"
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && "$2"' \
bash "$NODE_VERSION" "$ROOT/script/frontend-fmt-check"
else
echo "fmt-check: no node script/bootstrap would use; run it first" >&2
exit 1
fi
unformatted="$(gofmt -l cmd internal)" unformatted="$(gofmt -l cmd internal)"
if [ -n "$unformatted" ]; then if [ -n "$unformatted" ]; then
echo "Go files not formatted:" >&2 echo "Go files not formatted:" >&2