1 Commits
Author SHA1 Message Date
sneak 703e3e7c6e main() is one call into internal/cli (closes #160)
check / check (push) Canceled after 0s
cmd/netwatch-server/main.go parsed the command line, ran the check-cidr
and prepare-data-dir subcommands and built the fx application itself;
the Go style guide puts all of that in internal/ or pkg/. The new
package internal/cli does it, with the same arguments, messages and
exit codes. main() is now os.Exit(cli.Main(os.Stderr, Appname, Version,
os.Args[1:], cli.RunServer)): Main takes the writer for errors and the
function that runs the server, so its tests give it a buffer and a
function that fails them. Appname and Version stay in main, where the
build sets Version. prepare-data-dir also takes the volume and the
user's name, so its own tests give it a temporary directory and the
user they run as.

Model: opus-5-5
2026-10-19 05:08:44 +00:00
14 changed files with 112 additions and 300 deletions

No files matched your search

-11
View File
@@ -1,20 +1,9 @@
name: check name: check
on: [push] on: [push]
# Free the shared runner: a new push cancels only the same branch's older run.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Free the shared runner from a hung build.
timeout-minutes: 20
steps: steps:
# actions/checkout v4.2.2, 2026-02-22 # actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# script/cibuild needs no token, so none is left in .git/config.
with:
persist-credentials: false
# All history and tags, so git describe finds the version tag.
fetch-depth: 0
- run: script/cibuild - run: script/cibuild
+5 -13
View File
@@ -41,13 +41,11 @@ 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
the `node` on `PATH` has the pinned major version and is new enough for the one new enough for the frontend's dependencies is installed, yarn via
frontend's dependencies, yarn `YARN_VERSION` via corepack, or npm where there corepack, `yarn install --frozen-lockfile`, Go `GO_VERSION` unless the `go` on
is no corepack, unless the `yarn` on `PATH` is exactly that version, `PATH` is exactly that version, and the Go modules), linking what it installs
`yarn install --frozen-lockfile`, Go `GO_VERSION` unless the `go` on `PATH` is itself into `~/.local/bin`, which has to be on `PATH`. It installs no Go
exactly that version, and the Go modules), linking what it installs itself linter and not Docker: `make test` and `make lint` run in Docker
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
@@ -60,12 +58,6 @@ either one is broken. We provide:
defaults (see [Settings](#settings)) defaults (see [Settings](#settings))
- `script/clean` — remove what `script/build` writes - `script/clean` — remove what `script/build` writes
- `script/projectname` — print the project name (used for the Docker image tag) - `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 - `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 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 with the race detector and coverage. The frontend's production build is left
+6 -37
View File
@@ -12,8 +12,7 @@
pre-1.0. No git tags. `main` is the stable branch and `next` the development pre-1.0. No git tags. `main` is the stable branch and `next` the development
branch, which every PR targets. The frontend and the Go backend ship as one branch, which every PR targets. The frontend and the Go backend ship as one
Docker image, and the Gitea workflow `.gitea/workflows/check.yml` runs Docker image, and the Gitea workflow `.gitea/workflows/check.yml` runs
`script/cibuild` on every push, and cancels the older run on the same branch. `script/cibuild` on every push. Working toward 1.0.0.
Working toward 1.0.0.
# Next Step # Next Step
@@ -30,41 +29,11 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
`Appname` and `Version` stay in `main`, where the build sets `Version`, and `Appname` and `Version` stay in `main`, where the build sets `Version`, and
`main` gives `cli.Main` stderr and the function that runs the server, which `main` gives `cli.Main` stderr and the function that runs the server, which
the tests replace with a buffer and a function that fails them. Unit tests the tests replace with a buffer and a function that fails them. Unit tests
call `cli.Main` with `check-cidr` and a CIDR the server takes and one it call `cli.Main` with `check-cidr` and a CIDR it takes and one it refuses, and
refuses, and check the exit code and what it writes to stderr, and with check the exit code, and with `prepare-data-dir` and a path outside `/data`,
`prepare-data-dir` and a path outside `/data`, and check that it exits 1 with and check that it exits 1 with the error from looking up the `netwatch` user,
the error from looking up the `netwatch` user, which the test image does not which the test image does not have. Others give each subcommand input it takes
have. Others give the functions behind the subcommands input they take and and input it refuses, and check the exit code and what it writes to stderr
input they refuse, and check the exit code and what they write to stderr
- 2026-10-19: `.gitea/workflows/check.yml` is the sneak/prompts copy at
`0653579` ([#153](https://git.eeqj.de/sneak/netwatch/issues/153)). Its
checkout step sets `fetch-depth: 0`, so CI clones all history and tags, and
`git describe` in `script/cibuild` gives the image built from a tagged commit
the tag as its version; it gave the short commit. The checkout step also sets
`persist-credentials: false`, so the job's token is not left in `.git/config`.
A new push cancels the older run on the same branch, and a run stops after 20
minutes
- 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
still POSIX sh with no `set -e`, and does what it did, with the same messages
and exit codes. Checked by running the image with each script: a normal start,
bad `PORT`, `TRUSTED_PROXIES` and `DATA_DIR` values, `docker stop`, INT, and
nginx exiting on its own
- 2026-10-19: while report files cannot be written, the health check answers 503 - 2026-10-19: while report files cannot be written, the health check answers 503
with `"status":"error"` and the same fields, so the image's `HEALTHCHECK` with `"status":"error"` and the same fields, so the image's `HEALTHCHECK`
reports the container `unhealthy` reports the container `unhealthy`
+63 -91
View File
@@ -13,27 +13,24 @@ set -u
# address and start anyway, and reports a bad port without naming # address and start anyway, and reports a bad port without naming
# PORT, so a value that is not a usable port stops the container here, # PORT, so a value that is not a usable port stops the container here,
# before either process starts. # before either process starts.
check_port() { export PORT="${PORT:-8080}"
export PORT="${PORT:-8080}" case "$PORT" in
case "$PORT" in *[!0-9]*)
*[!0-9]*) echo "entrypoint: PORT must be a port number, not '$PORT'" >&2
echo "entrypoint: PORT must be a port number, not '$PORT'" >&2
exit 1
;;
esac
# The length is checked first because, for a number too big for it,
# the shell's test prints an error and is false, so the range checks
# alone would let it through.
if [ "${#PORT}" -gt 5 ] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
echo "entrypoint: PORT must be from 1 to 65535, not '$PORT'" >&2
exit 1 exit 1
fi ;;
if [ "$PORT" -eq 8081 ]; then esac
echo "entrypoint: PORT cannot be 8081, netwatch-server" \ # The length is checked first because, for a number too big for it,
"listens there" >&2 # the shell's test prints an error and is false, so the range checks
exit 1 # alone would let it through.
fi if [ "${#PORT}" -gt 5 ] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
} echo "entrypoint: PORT must be from 1 to 65535, not '$PORT'" >&2
exit 1
fi
if [ "$PORT" -eq 8081 ]; then
echo "entrypoint: PORT cannot be 8081, netwatch-server listens there" >&2
exit 1
fi
# TRUSTED_PROXIES names the reverse proxies in front of the container, # TRUSTED_PROXIES names the reverse proxies in front of the container,
# as IP addresses or CIDRs separated by commas. nginx takes the client # as IP addresses or CIDRs separated by commas. nginx takes the client
@@ -50,23 +47,21 @@ check_port() {
# TRUSTED_PROXIES. Its error, naming the CIDR, is dropped for the one # TRUSTED_PROXIES. Its error, naming the CIDR, is dropped for the one
# below, naming the entry as written. set -f keeps a * in an entry from # below, naming the entry as written. set -f keeps a * in an entry from
# becoming a list of file names. # becoming a list of file names.
write_trusted_proxies() { TRUSTED_PROXIES="${TRUSTED_PROXIES-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}"
TRUSTED_PROXIES="${TRUSTED_PROXIES-10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}" set -f
set -f for proxy in $(printf '%s' "$TRUSTED_PROXIES" | tr ',' ' '); do
for proxy in $(printf '%s' "$TRUSTED_PROXIES" | tr ',' ' '); do case "$proxy" in
case "$proxy" in */*) cidr="$proxy" ;;
*/*) cidr="$proxy" ;; *:*) cidr="$proxy/128" ;;
*:*) cidr="$proxy/128" ;; *) cidr="$proxy/32" ;;
*) cidr="$proxy/32" ;; esac
esac if ! netwatch-server check-cidr "$cidr" 2> /dev/null; then
if ! netwatch-server check-cidr "$cidr" 2> /dev/null; then echo "entrypoint: TRUSTED_PROXIES must be IP addresses or CIDRs" \
echo "entrypoint: TRUSTED_PROXIES must be IP addresses or CIDRs" \ "separated by commas; '$proxy' is neither" >&2
"separated by commas; '$proxy' is neither" >&2 exit 1
exit 1 fi
fi echo "set_real_ip_from $cidr;"
echo "set_real_ip_from $cidr;" done > /etc/nginx/trusted-proxies.conf
done > /etc/nginx/trusted-proxies.conf
}
# netwatch-server keeps its report files in DATA_DIR, on the /data # netwatch-server keeps its report files in DATA_DIR, on the /data
# volume, which may be a host directory owned by root or by another # volume, which may be a host directory owned by root or by another
@@ -78,10 +73,12 @@ write_trusted_proxies() {
# naming DATA_DIR, unless DATA_DIR is /data or a path below it, and it # naming DATA_DIR, unless DATA_DIR is /data or a path below it, and it
# acts on nothing outside /data, whatever symbolic or hard links it # acts on nothing outside /data, whatever symbolic or hard links it
# meets there. # meets there.
prepare_data_dir() { export DATA_DIR="${DATA_DIR:-/data/reports}"
export DATA_DIR="${DATA_DIR:-/data/reports}" netwatch-server prepare-data-dir "$DATA_DIR" || exit 1
netwatch-server prepare-data-dir "$DATA_DIR" || exit 1
} # A stop signal is only noted here; the loop below acts on it.
stop_requested=""
trap 'stop_requested=yes' TERM INT
# netwatch-server runs as the netwatch user and listens on loopback # netwatch-server runs as the netwatch user and listens on loopback
# only, on a port other than the public one; configs/nginx.conf proxies # only, on a port other than the public one; configs/nginx.conf proxies
@@ -90,11 +87,9 @@ prepare_data_dir() {
# the container has. The netwatch user has no login shell, hence -s # the container has. The netwatch user has no login shell, hence -s
# /bin/sh. busybox su replaces itself with the command instead of # /bin/sh. busybox su replaces itself with the command instead of
# staying on as its parent, so $! is the server's own PID. # staying on as its parent, so $! is the server's own PID.
start_backend() { BIND_ADDRESS=127.0.0.1 PORT=8081 TRUSTED_PROXIES=127.0.0.1/32 \
BIND_ADDRESS=127.0.0.1 PORT=8081 TRUSTED_PROXIES=127.0.0.1/32 \ su -s /bin/sh netwatch -c 'exec netwatch-server' &
su -s /bin/sh netwatch -c 'exec netwatch-server' & backend=$!
backend=$!
}
# nginx starts through the nginx image's own entrypoint, which applies # nginx starts through the nginx image's own entrypoint, which applies
# the image's start-up configuration and then replaces itself with # the image's start-up configuration and then replaces itself with
@@ -103,11 +98,9 @@ start_backend() {
# that rendering to PORT: a variable nginx itself uses, such as $uri, # that rendering to PORT: a variable nginx itself uses, such as $uri,
# would otherwise be replaced by an environment variable of the same # would otherwise be replaced by an environment variable of the same
# name. # name.
start_nginx() { NGINX_ENVSUBST_FILTER='^PORT$' \
NGINX_ENVSUBST_FILTER='^PORT$' \ /docker-entrypoint.sh nginx -g 'daemon off;' &
/docker-entrypoint.sh nginx -g 'daemon off;' & nginx=$!
nginx=$!
}
running() { running() {
kill -0 "$1" 2>/dev/null kill -0 "$1" 2>/dev/null
@@ -116,47 +109,26 @@ running() {
# POSIX sh cannot wait for whichever of two children exits first, so # POSIX sh cannot wait for whichever of two children exits first, so
# look once a second. The shell collects a child that has exited while # look once a second. The shell collects a child that has exited while
# it runs sleep, and running() is false for that child from then on. # it runs sleep, and running() is false for that child from then on.
wait_for_stop_or_exit() { while [ -z "$stop_requested" ] && running "$backend" && running "$nginx"; do
while [ -z "$stop_requested" ] && running "$backend" && running "$nginx"; do sleep 1
sleep 1 done
done
}
# Stop both, then wait until neither is left. # Stop both, then wait until neither is left.
stop_both() { kill -TERM "$backend" "$nginx" 2>/dev/null
kill -TERM "$backend" "$nginx" 2>/dev/null while running "$backend" || running "$nginx"; do
while running "$backend" || running "$nginx"; do sleep 1
sleep 1 done
done
} wait "$backend"
backend_status=$?
wait "$nginx"
nginx_status=$?
echo "entrypoint: netwatch-server exited $backend_status," \
"nginx exited $nginx_status"
# Success is a requested stop that both processes exited cleanly from. # Success is a requested stop that both processes exited cleanly from.
exit_with_status() { if [ -n "$stop_requested" ] && [ "$backend_status" -eq 0 ] &&
wait "$backend" [ "$nginx_status" -eq 0 ]; then
backend_status=$? exit 0
wait "$nginx" fi
nginx_status=$? exit 1
echo "entrypoint: netwatch-server exited $backend_status," \
"nginx exited $nginx_status"
if [ -n "$stop_requested" ] && [ "$backend_status" -eq 0 ] &&
[ "$nginx_status" -eq 0 ]; then
exit 0
fi
exit 1
}
main() {
check_port
write_trusted_proxies
prepare_data_dir
# A stop signal is only noted here; wait_for_stop_or_exit acts on it.
stop_requested=""
trap 'stop_requested=yes' TERM INT
start_backend
start_nginx
wait_for_stop_or_exit
stop_both
exit_with_status
}
main "$@"
+5 -5
View File
@@ -29,9 +29,9 @@ import (
// with appname and version, the name and version the server logs and // with appname and version, the name and version the server logs and
// reports. main passes os.Stderr and RunServer; the tests pass a // reports. main passes os.Stderr and RunServer; the tests pass a
// buffer and a function that fails them, so that no test starts the // buffer and a function that fails them, so that no test starts the
// server. When serve is RunServer and the server fails to start, to // server. When serve is RunServer and the server fails to start or to
// listen or to stop, fx's Run exits the process with status 1 and Main // stop, fx's Run exits the process with status 1 and Main never
// never returns. // returns.
func Main( func Main(
stderr io.Writer, stderr io.Writer,
appname, version string, appname, version string,
@@ -96,8 +96,8 @@ func prepareDataDir(stderr io.Writer, volume, dataDir, username string) int {
// RunServer runs the server until it is told to stop, with appname and // RunServer runs the server until it is told to stop, with appname and
// version as the name and version it logs and reports. If the server // version as the name and version it logs and reports. If the server
// fails to start, to listen or to stop, fx's Run exits the process // fails to start or to stop, fx's Run exits the process with status 1
// with status 1 itself. // itself.
func RunServer(appname, version string) { func RunServer(appname, version string) {
globals.Appname = appname globals.Appname = appname
globals.Version = version globals.Version = version
+10 -22
View File
@@ -2,6 +2,7 @@ package cli_test
import ( import (
"bytes" "bytes"
"io"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
@@ -13,31 +14,18 @@ import (
"sneak.berlin/go/netwatch/internal/reportbuf" "sneak.berlin/go/netwatch/internal/reportbuf"
) )
// TestMainRunsCheckCIDR: Main given check-cidr returns 0, writing // TestMainRunsCheckCIDR: Main given check-cidr returns 0 for a CIDR the
// nothing, for a CIDR the server takes in its TRUSTED_PROXIES, and 1, // server takes in its TRUSTED_PROXIES and 1 for one it refuses, and
// with an error naming TRUSTED_PROXIES and the CIDR, for one it // does not start the server.
// refuses, and does not start the server.
func TestMainRunsCheckCIDR(t *testing.T) { func TestMainRunsCheckCIDR(t *testing.T) {
t.Parallel() t.Parallel()
var stderr bytes.Buffer for cidr, want := range map[string]int{"10.0.0.0/8": 0, "1.2.3/32": 1} {
code := cli.Main(io.Discard, "netwatch", "test",
code := cli.Main(&stderr, "netwatch", "test", []string{"check-cidr", cidr}, failIfServerStarts(t))
[]string{"check-cidr", "10.0.0.0/8"}, failIfServerStarts(t)) if code != want {
if code != 0 || stderr.Len() != 0 { t.Errorf("check-cidr %q: exit %d, want %d", cidr, code, want)
t.Errorf("check-cidr 10.0.0.0/8: exit %d, stderr %q; want exit 0 and no output", }
code, stderr.String())
}
stderr.Reset()
code = cli.Main(&stderr, "netwatch", "test",
[]string{"check-cidr", "1.2.3/32"}, failIfServerStarts(t))
want := `TRUSTED_PROXIES "1.2.3/32": `
if code != 1 || !strings.HasPrefix(stderr.String(), want) {
t.Errorf("check-cidr 1.2.3/32: exit %d, stderr %q; want exit 1 and %q...",
code, stderr.String(), want)
} }
} }
+2 -2
View File
@@ -23,8 +23,8 @@ main() {
"" | -* | *[[:space:]]*) usage ;; "" | -* | *[[:space:]]*) usage ;;
esac esac
cd "$ROOT" cd "$ROOT"
"$ROOT/script/yarn" add --dev "$1" yarn add --dev "$1"
"$ROOT/script/yarn" install --frozen-lockfile yarn install --frozen-lockfile
} }
main "$@" main "$@"
+14 -55
View File
@@ -3,14 +3,11 @@
# 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 only if it has the major version of NODE_VERSION and is # used directly if it is at least NODE_MIN_VERSION; otherwise it is
# at least NODE_MIN_VERSION; otherwise NODE_VERSION is installed via nvm # installed at a pinned version via nvm (installing nvm itself first,
# (installing nvm itself first, from a hash-verified release archive, # from a hash-verified release archive, never curl | sh). Go, with its
# never curl | sh). Yarn is used directly only if it is exactly # gofmt, is used directly only if it is exactly GO_VERSION; otherwise
# YARN_VERSION; otherwise that version is prepared through corepack, # that release is installed from its hash-verified archive.
# 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 # 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
@@ -145,63 +142,32 @@ ensure_nvm() {
rm -rf "$tmp" rm -rf "$tmp"
} }
# node_ok: the node on PATH has the major version of NODE_VERSION and is # node_ok: the node on PATH is at least NODE_MIN_VERSION. node itself
# at least NODE_MIN_VERSION. Only the major is compared with the pin, # compares the two: major, then minor, then patch.
# 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() { node_ok() {
if ! command -v node >/dev/null 2>&1; then return 1; fi if missing node; 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_VERSION" ' "$NODE_MIN_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 if node_ok; then return 0; fi
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)"
} }
# yarn_ok: yarn --version on PATH prints exactly YARN_VERSION, compared # ensure_yarn: corepack writes its shims (pnpm and yarnpkg as well as
# over the whole version. A yarn that fails or prints anything else does # yarn) into $TOOLCHAIN rather than next to itself, and the npm fallback
# not pass. # installs there too; only yarn is linked.
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 yarn_ok; then if ! missing yarn; then return 0; fi
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
@@ -216,13 +182,6 @@ 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
+1 -1
View File
@@ -8,7 +8,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$ROOT/script/yarn" build yarn build
# VERSION comes from the environment. Unset or empty, it is git # VERSION comes from the environment. Unset or empty, it is git
# describe, or "dev" where there is no git or no repository history. # describe, or "dev" where there is no git or no repository history.
+1 -1
View File
@@ -7,7 +7,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$ROOT/script/yarn" dev yarn dev
} }
main "$@" main "$@"
+1 -1
View File
@@ -10,7 +10,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$ROOT/script/yarn" prettier --write '**/*.{js,css,html,md}' yarn prettier --write '**/*.{js,css,html,md}'
} }
main "$@" main "$@"
+1 -1
View File
@@ -7,7 +7,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$ROOT/script/yarn" prettier --check '**/*.{js,css,html,md}' yarn prettier --check '**/*.{js,css,html,md}'
} }
main "$@" main "$@"
+3 -5
View File
@@ -14,14 +14,12 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
NODE_OPTIONS=--test-reporter=dot \ NODE_OPTIONS=--test-reporter=dot timeout 90 yarn --silent run test || {
timeout 90 "$ROOT/script/yarn" --silent run test || {
echo "--- Rerunning with every test listed for details ---" echo "--- Rerunning with every test listed for details ---"
NODE_OPTIONS=--test-reporter=spec \ NODE_OPTIONS=--test-reporter=spec timeout 90 yarn --silent run test
timeout 90 "$ROOT/script/yarn" --silent run test
exit 1 exit 1
} }
timeout 30 "$ROOT/script/yarn" build timeout 30 yarn build
} }
main "$@" main "$@"
-55
View File
@@ -1,55 +0,0 @@
#!/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 "$@"