diff --git a/README.md b/README.md index 6f4add2..d2800df 100644 --- a/README.md +++ b/README.md @@ -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: - `script/bootstrap` — install all dependencies (the pinned node via nvm unless - one new enough for the frontend's dependencies is installed, yarn via - corepack, `yarn install --frozen-lockfile`, Go `GO_VERSION` unless the `go` on - `PATH` is exactly that version, and the Go 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 + the `node` on `PATH` has the pinned major version and is new enough for the + frontend's dependencies, yarn `YARN_VERSION` via corepack, or npm where there + is no corepack, unless the `yarn` on `PATH` is exactly that version, + `yarn install --frozen-lockfile`, Go `GO_VERSION` unless the `go` on `PATH` is + exactly that version, and the Go 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 git pre-commit hook - `script/dev` — run the Vite dev server, which proxies `/api` to a locally @@ -58,6 +60,12 @@ either one is broken. We provide: defaults (see [Settings](#settings)) - `script/clean` — remove what `script/build` writes - `script/projectname` — print the project name (used for the Docker image tag) +- `script/yarn` — run yarn with the given arguments under the node + `script/bootstrap` would use: with `~/.local/bin` first on `PATH`, the first + `node` on that `PATH` when it has the pinned major version and is new enough + for the frontend's dependencies, and otherwise the pinned node in nvm. Every + script that runs yarn on the host runs it through this one, except + `script/bootstrap`, which picks its own node and yarn - `script/test` — build the `test` phase of `Dockerfile`, writing no image: the frontend's unit tests in its `frontend-test` stage, and the backend's Go tests with the race detector and coverage. The frontend's production build is left diff --git a/TODO.md b/TODO.md index 8996811..dd8c747 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,20 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of # Completed Steps +- 2026-10-19: `script/bootstrap` keeps the `yarn` on `PATH` only when + `yarn --version` is exactly `YARN_VERSION`, and otherwise installs that + version 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 `REPO_POLICIES.md` asks, and is at least + `NODE_MIN_VERSION`; a node 24 was kept before. After installing node or yarn + it looks the tool up through `PATH` again and stops if that one fails the same + check, as it does for Go, and it prints the node and yarn versions it uses. + Every script that runs yarn on the host, except `script/bootstrap`, which + picks its own node and yarn, runs it through the new `script/yarn`. That + script puts `~/.local/bin` first on `PATH`, uses the first `node` on that + `PATH` when it passes the same test, and otherwise runs yarn under the pinned + node in nvm; `make build` ran yarn under whatever node was first on `PATH` - 2026-10-19: `bin/entrypoint.sh` has each of its steps in a function, and a `main` at the end calls them in order, as the shell section of the code style guide asks ([#163](https://git.eeqj.de/sneak/netwatch/issues/163)). It is diff --git a/script/add-dependency b/script/add-dependency index 7a4230c..5479b43 100755 --- a/script/add-dependency +++ b/script/add-dependency @@ -23,8 +23,8 @@ main() { "" | -* | *[[:space:]]*) usage ;; esac cd "$ROOT" - yarn add --dev "$1" - yarn install --frozen-lockfile + "$ROOT/script/yarn" add --dev "$1" + "$ROOT/script/yarn" install --frozen-lockfile } main "$@" diff --git a/script/bootstrap b/script/bootstrap index 1bf0e7f..63f4b2c 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -3,11 +3,14 @@ # this repo. Idempotent: every install is guarded by a check so already # installed tools are skipped. Base tooling comes from nix, apt, brew, # 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 -# installed at a pinned version via nvm (installing nvm itself first, -# from a hash-verified release archive, never curl | sh). Go, with its -# gofmt, is used directly only if it is exactly GO_VERSION; otherwise -# that release is installed from its hash-verified archive. +# used directly only if it has the major version of NODE_VERSION and is +# at least NODE_MIN_VERSION; otherwise NODE_VERSION is installed via nvm +# (installing nvm itself first, from a hash-verified release archive, +# never curl | sh). Yarn is used directly only if it is exactly +# YARN_VERSION; otherwise that version is prepared through corepack, +# or installed with npm where there is no 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 # under $HOME and is linked into ~/.local/bin, where make and the git @@ -142,32 +145,63 @@ ensure_nvm() { rm -rf "$tmp" } -# node_ok: the node on PATH is at least NODE_MIN_VERSION. node itself -# compares the two: major, then minor, then patch. +# 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, +# as REPO_POLICIES.md asks. node itself compares the versions: major, +# then minor, then patch. A node that fails does not pass. +# script/bootstrap and script/yarn carry this function unchanged. node_ok() { - if missing node; then return 1; fi + 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_MIN_VERSION" "$NODE_VERSION" } # 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() { - if node_ok; then return 0; fi + if node_ok; then + echo "bootstrap: using node $(node --version)" + return 0 + fi ensure_nvm nvm_sh "nvm install $NODE_VERSION" 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) into $TOOLCHAIN rather than next to itself, and the npm fallback -# installs there too; only yarn is linked. +# yarn_ok: yarn --version on PATH prints exactly YARN_VERSION, compared +# over the whole version. A yarn that fails or prints anything else does +# 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() { - if ! missing yarn; then return 0; fi + if yarn_ok; then + echo "bootstrap: using yarn $(yarn --version)" + return 0 + fi shims="$TOOLCHAIN/corepack-shims" mkdir -p "$shims" if ! missing corepack; then @@ -182,6 +216,13 @@ ensure_yarn() { shims="$TOOLCHAIN/npm-global/bin" fi 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 diff --git a/script/build b/script/build index 710bca6..1c5ff4f 100755 --- a/script/build +++ b/script/build @@ -8,7 +8,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - yarn build + "$ROOT/script/yarn" build # VERSION comes from the environment. Unset or empty, it is git # describe, or "dev" where there is no git or no repository history. diff --git a/script/dev b/script/dev index 7f879bd..2a05ba5 100755 --- a/script/dev +++ b/script/dev @@ -7,7 +7,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - yarn dev + "$ROOT/script/yarn" dev } main "$@" diff --git a/script/frontend-fmt b/script/frontend-fmt index ce1322d..4ea4264 100755 --- a/script/frontend-fmt +++ b/script/frontend-fmt @@ -10,7 +10,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - yarn prettier --write '**/*.{js,css,html,md}' + "$ROOT/script/yarn" prettier --write '**/*.{js,css,html,md}' } main "$@" diff --git a/script/frontend-fmt-check b/script/frontend-fmt-check index 7cc718a..36309db 100755 --- a/script/frontend-fmt-check +++ b/script/frontend-fmt-check @@ -7,7 +7,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - yarn prettier --check '**/*.{js,css,html,md}' + "$ROOT/script/yarn" prettier --check '**/*.{js,css,html,md}' } main "$@" diff --git a/script/frontend-test b/script/frontend-test index 390a0a3..da98dd4 100755 --- a/script/frontend-test +++ b/script/frontend-test @@ -14,12 +14,14 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - NODE_OPTIONS=--test-reporter=dot timeout 90 yarn --silent run test || { + NODE_OPTIONS=--test-reporter=dot \ + timeout 90 "$ROOT/script/yarn" --silent run test || { echo "--- Rerunning with every test listed for details ---" - NODE_OPTIONS=--test-reporter=spec timeout 90 yarn --silent run test + NODE_OPTIONS=--test-reporter=spec \ + timeout 90 "$ROOT/script/yarn" --silent run test exit 1 } - timeout 30 yarn build + timeout 30 "$ROOT/script/yarn" build } main "$@" diff --git a/script/yarn b/script/yarn new file mode 100755 index 0000000..51eeb8b --- /dev/null +++ b/script/yarn @@ -0,0 +1,55 @@ +#!/bin/sh +# script/yarn: run yarn with the given arguments under the node +# script/bootstrap would use. Every script that runs yarn on the host +# runs it through this one, except script/bootstrap, which picks its +# own node and yarn. +set -eu + +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, +# as REPO_POLICIES.md asks. node itself compares the versions: major, +# then minor, then patch. A node that fails does not pass. +# script/bootstrap and script/yarn 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" +} + +# Run the yarn script/bootstrap installed: the yarn on PATH when the +# node on PATH passes node_ok, and otherwise yarn under the pinned node +# in nvm. nvm is a bash script, hence the subshell. +run_yarn() { + if node_ok && command -v yarn >/dev/null 2>&1; then + exec yarn "$@" + fi + if node_ok || [ ! -s "$HOME/.nvm/nvm.sh" ]; then + echo "script/yarn: 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() { + cd "$ROOT" + # script/bootstrap links the node and yarn it installs into + # ~/.local/bin, which the shell that called it may not have on PATH. + PATH="$HOME/.local/bin:$PATH" + run_yarn "$@" +} + +main "$@"