Compare commits
1 Commits
b100814f8e
...
4baf2a1c78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4baf2a1c78 |
7
TODO.md
7
TODO.md
@@ -28,7 +28,12 @@ files, so merging it also closes most compliance gaps.
|
|||||||
pre-commit hook installer in `backend/Makefile` was removed, `script/cibuild`
|
pre-commit hook installer in `backend/Makefile` was removed, `script/cibuild`
|
||||||
now builds both images as the workflow's only build step, and
|
now builds both images as the workflow's only build step, and
|
||||||
`script/bootstrap` provisions the backend toolchain (pinned, hash-verified Go
|
`script/bootstrap` provisions the backend toolchain (pinned, hash-verified Go
|
||||||
and golangci-lint) so a fresh clone can pass the widened gate
|
and golangci-lint) so a fresh clone can pass the widened gate. Bootstrap
|
||||||
|
matches the Go pin rather than treating it as a floor, because the pinned
|
||||||
|
golangci-lint cannot analyse packages built by a newer Go; it links only into
|
||||||
|
`~/.local/bin`, never a system-wide prefix, and refuses to replace anything it
|
||||||
|
did not create; and it exits non-zero rather than reporting success when the
|
||||||
|
tools on the caller's `PATH` are not the pinned ones
|
||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
||||||
shims, README Entrypoints section
|
shims, README Entrypoints section
|
||||||
- 2026-02-27: backend with buffered zstd-compressed report storage; CI workflow
|
- 2026-02-27: backend with buffered zstd-compressed report storage; CI workflow
|
||||||
|
|||||||
207
script/bootstrap
207
script/bootstrap
@@ -16,13 +16,35 @@
|
|||||||
# pre-commit hook that script/setup installs.
|
# pre-commit hook that script/setup installs.
|
||||||
#
|
#
|
||||||
# Anything installed outside the system package manager is symlinked
|
# Anything installed outside the system package manager is symlinked
|
||||||
# into a directory that is on PATH, so a later `make check` in a plain
|
# into ~/.local/bin, so a later `make check` in a plain shell finds it.
|
||||||
# shell finds it. nvm only puts node on PATH for shells that source
|
# nvm only puts node on PATH for shells that source nvm.sh, which
|
||||||
# nvm.sh, which neither make nor the git hook does.
|
# neither make nor the git hook does.
|
||||||
|
#
|
||||||
|
# Three rules govern what this script is allowed to touch:
|
||||||
|
#
|
||||||
|
# 1. It never writes outside $HOME. A per-repo bootstrap has no
|
||||||
|
# business writing to /usr/local/bin, a Homebrew prefix, or any
|
||||||
|
# other system-wide location shared with other users and with a
|
||||||
|
# package manager.
|
||||||
|
# 2. It never replaces something it did not create. Only a symlink
|
||||||
|
# that already points into one of its own managed directories is
|
||||||
|
# overwritten; anything else is left alone and bootstrap exits
|
||||||
|
# non-zero telling you what to remove.
|
||||||
|
# 3. It never reports success while the tools a later `make check`
|
||||||
|
# would pick up are not the ones it provisioned. If it cannot
|
||||||
|
# guarantee the pinned toolchain wins on your PATH, it exits
|
||||||
|
# non-zero rather than leaving you a green bootstrap and a broken
|
||||||
|
# gate.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# The PATH as the caller had it, captured before ensure_bin_dir amends
|
||||||
|
# it. verify_toolchain checks against this rather than against the PATH
|
||||||
|
# this script builds for itself, so what it reports is what a later
|
||||||
|
# `make check` in the user's own shell will actually resolve.
|
||||||
|
ORIG_PATH="$PATH"
|
||||||
|
|
||||||
# Pinned versions, 2026-07-07
|
# Pinned versions, 2026-07-07
|
||||||
NODE_VERSION="22.17.0"
|
NODE_VERSION="22.17.0"
|
||||||
NVM_VERSION="0.40.3"
|
NVM_VERSION="0.40.3"
|
||||||
@@ -32,10 +54,27 @@ YARN_VERSION="1.22.22"
|
|||||||
|
|
||||||
# Go 1.25.7 (2026-08-09). This is the toolchain inside the pinned
|
# Go 1.25.7 (2026-08-09). This is the toolchain inside the pinned
|
||||||
# golang:1.25-alpine builder of Dockerfile.backend, so a local build
|
# golang:1.25-alpine builder of Dockerfile.backend, so a local build
|
||||||
# uses the same compiler CI does. GO_MIN_VERSION is the floor from
|
# uses the same compiler CI does.
|
||||||
# backend/go.mod; an already-installed go at or above it is used as is.
|
#
|
||||||
|
# The Go pin is a compatibility constraint to match, not a floor to
|
||||||
|
# clear. golangci-lint links go/types from the Go release it was built
|
||||||
|
# with, and go/types refuses to load packages compiled by a newer Go:
|
||||||
|
# with the pinned linter (built with go1.25.4) and a host Go 1.26,
|
||||||
|
# `make check` dies with
|
||||||
|
#
|
||||||
|
# panic: file requires newer Go version go1.26
|
||||||
|
# (application built with go1.25)
|
||||||
|
#
|
||||||
|
# So an already-installed go is reused only inside a window:
|
||||||
|
# GO_MIN_VERSION is the floor from backend/go.mod, and GO_MAX_MINOR is
|
||||||
|
# the major.minor of the Go the pinned golangci-lint was built with.
|
||||||
|
# Anything outside that window is ignored and GO_VERSION is installed
|
||||||
|
# instead. GO_MAX_MINOR is therefore coupled to GOLANGCI_LINT_VERSION
|
||||||
|
# below and must be revisited whenever that pin moves; `golangci-lint
|
||||||
|
# version` prints the "built with goX.Y.Z" it needs.
|
||||||
GO_VERSION="1.25.7"
|
GO_VERSION="1.25.7"
|
||||||
GO_MIN_VERSION="1.25.5"
|
GO_MIN_VERSION="1.25.5"
|
||||||
|
GO_MAX_MINOR="1.25"
|
||||||
|
|
||||||
# golangci-lint 2.7.2 (2026-08-09). MUST stay equal to the golangci-lint
|
# golangci-lint 2.7.2 (2026-08-09). MUST stay equal to the golangci-lint
|
||||||
# pinned in Dockerfile.backend (currently commit
|
# pinned in Dockerfile.backend (currently commit
|
||||||
@@ -47,12 +86,16 @@ GO_MIN_VERSION="1.25.5"
|
|||||||
# v2.12.2, commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5. When that
|
# v2.12.2, commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5. When that
|
||||||
# lands, GOLANGCI_LINT_VERSION and every hash in golangci_lint_sha256()
|
# lands, GOLANGCI_LINT_VERSION and every hash in golangci_lint_sha256()
|
||||||
# below must be updated to the v2.12.2 release archives in the same
|
# below must be updated to the v2.12.2 release archives in the same
|
||||||
# commit, or local and CI will disagree.
|
# commit, or local and CI will disagree. GO_MAX_MINOR must move with
|
||||||
|
# it, to the major.minor that release reports as "built with".
|
||||||
GOLANGCI_LINT_VERSION="2.7.2"
|
GOLANGCI_LINT_VERSION="2.7.2"
|
||||||
|
|
||||||
# Where hash-verified archives are unpacked. Version-scoped, so bumping
|
# Where hash-verified archives are unpacked. Version-scoped, so bumping
|
||||||
# a pin installs alongside the old copy instead of half-overwriting it.
|
# a pin installs alongside the old copy instead of half-overwriting it.
|
||||||
# Filled in by main() from script/projectname.
|
# Filled in by main() from script/projectname. TOOLCHAIN is also the
|
||||||
|
# ownership boundary used by link_bin: a symlink pointing inside it is
|
||||||
|
# one this script created and may replace.
|
||||||
|
TOOLCHAIN=""
|
||||||
GO_DIR=""
|
GO_DIR=""
|
||||||
GOLANGCI_LINT_DIR=""
|
GOLANGCI_LINT_DIR=""
|
||||||
|
|
||||||
@@ -170,33 +213,67 @@ ver_ge() {
|
|||||||
}'
|
}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ensure_bin_dir: pick the directory provisioned tools are linked into.
|
# ensure_bin_dir: the directory provisioned tools are linked into. It is
|
||||||
# /usr/local/bin when writable (root, or a Homebrew prefix), otherwise
|
# always ~/.local/bin: per-user, never a system-wide or package-manager
|
||||||
# ~/.local/bin, which is prepended to PATH for the rest of this run and
|
# prefix. It is put at the front of PATH for the rest of this run, and
|
||||||
# reported so the user can add it permanently.
|
# reported if the caller's own PATH did not already contain it.
|
||||||
ensure_bin_dir() {
|
ensure_bin_dir() {
|
||||||
[ -n "$BIN_DIR" ] && return 0
|
[ -n "$BIN_DIR" ] && return 0
|
||||||
if [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
|
|
||||||
BIN_DIR="/usr/local/bin"
|
|
||||||
else
|
|
||||||
BIN_DIR="$HOME/.local/bin"
|
BIN_DIR="$HOME/.local/bin"
|
||||||
mkdir -p "$BIN_DIR"
|
mkdir -p "$BIN_DIR"
|
||||||
fi
|
case "$PATH" in
|
||||||
case ":$PATH:" in
|
"$BIN_DIR":*) ;;
|
||||||
*":$BIN_DIR:"*) ;;
|
|
||||||
*)
|
*)
|
||||||
PATH="$BIN_DIR:$PATH"
|
PATH="$BIN_DIR:$PATH"
|
||||||
export PATH
|
export PATH
|
||||||
echo "bootstrap: add $BIN_DIR to your PATH" >&2
|
;;
|
||||||
|
esac
|
||||||
|
case ":$ORIG_PATH:" in
|
||||||
|
*":$BIN_DIR:"*) ;;
|
||||||
|
*)
|
||||||
|
echo "bootstrap: add $BIN_DIR to the front of your PATH, e.g." >&2
|
||||||
|
echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# owned_path <path>: true when <path> lies inside a directory this
|
||||||
|
# script provisions, i.e. a link to it is one this script created and
|
||||||
|
# may replace. Everything else belongs to the user or to a package
|
||||||
|
# manager and is never touched.
|
||||||
|
owned_path() {
|
||||||
|
case "$1" in
|
||||||
|
"$TOOLCHAIN"/*) return 0 ;;
|
||||||
|
"$HOME"/.nvm/*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# refuse_clobber <path> <what>: report that <path> is not ours and stop.
|
||||||
|
refuse_clobber() {
|
||||||
|
echo "bootstrap: $1 already exists and $2." >&2
|
||||||
|
echo " Refusing to replace something this script did not create." >&2
|
||||||
|
echo " Remove or rename it and re-run bootstrap." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# link_bin <target> <name>: idempotently expose one provisioned binary
|
# link_bin <target> <name>: idempotently expose one provisioned binary
|
||||||
# on PATH.
|
# on PATH. Only an existing symlink into one of our own directories is
|
||||||
|
# replaced; a regular file, a directory, or a symlink pointing anywhere
|
||||||
|
# else is left intact and bootstrap fails.
|
||||||
link_bin() {
|
link_bin() {
|
||||||
ensure_bin_dir
|
ensure_bin_dir
|
||||||
ln -sfn "$1" "$BIN_DIR/$2"
|
link="$BIN_DIR/$2"
|
||||||
|
if [ -L "$link" ]; then
|
||||||
|
existing="$(readlink "$link")"
|
||||||
|
if ! owned_path "$existing"; then
|
||||||
|
refuse_clobber "$link" \
|
||||||
|
"is a symlink to $existing, outside this repo's toolchain"
|
||||||
|
fi
|
||||||
|
elif [ -e "$link" ]; then
|
||||||
|
refuse_clobber "$link" "is not a symlink"
|
||||||
|
fi
|
||||||
|
ln -sfn "$1" "$link"
|
||||||
}
|
}
|
||||||
|
|
||||||
# nvm is a bash script; run a command in a bash with nvm loaded
|
# nvm is a bash script; run a command in a bash with nvm loaded
|
||||||
@@ -229,19 +306,31 @@ ensure_node() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ensure_yarn: yarn comes from corepack. Left to itself, `corepack
|
||||||
|
# enable` writes its shims next to the corepack binary it resolved, and
|
||||||
|
# it writes four of them (yarn, yarnpkg, pnpm, pnpx), not the one asked
|
||||||
|
# for. --install-directory keeps all four inside this repo's own
|
||||||
|
# toolchain directory, and only yarn is then linked onto PATH. The
|
||||||
|
# no-corepack fallback likewise installs into a per-user npm prefix
|
||||||
|
# under the toolchain directory instead of npm's global one. Nothing
|
||||||
|
# here writes outside $HOME.
|
||||||
ensure_yarn() {
|
ensure_yarn() {
|
||||||
if ! missing yarn; then return 0; fi
|
if ! missing yarn; then return 0; fi
|
||||||
|
yarn_bin="$TOOLCHAIN/corepack-shims"
|
||||||
|
mkdir -p "$yarn_bin"
|
||||||
if ! missing corepack; then
|
if ! missing corepack; then
|
||||||
corepack enable
|
corepack enable --install-directory "$yarn_bin"
|
||||||
corepack prepare "yarn@$YARN_VERSION" --activate
|
corepack prepare "yarn@$YARN_VERSION" --activate
|
||||||
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
|
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
|
nvm_sh "nvm use $NODE_VERSION >/dev/null && \
|
||||||
|
corepack enable --install-directory \"$yarn_bin\" && \
|
||||||
corepack prepare yarn@$YARN_VERSION --activate"
|
corepack prepare yarn@$YARN_VERSION --activate"
|
||||||
else
|
else
|
||||||
npm install -g "yarn@$YARN_VERSION"
|
yarn_bin="$TOOLCHAIN/npm-global/bin"
|
||||||
|
npm install -g --prefix "$TOOLCHAIN/npm-global" "yarn@$YARN_VERSION"
|
||||||
fi
|
fi
|
||||||
if [ -n "$NODE_BIN" ] && [ -e "$NODE_BIN/yarn" ]; then
|
if [ -e "$yarn_bin/yarn" ]; then
|
||||||
link_bin "$NODE_BIN/yarn" yarn
|
link_bin "$yarn_bin/yarn" yarn
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,14 +367,18 @@ go_sha256() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# go_ok: an already-installed go is acceptable if it is at least
|
# go_ok: an already-installed go is acceptable only inside the window
|
||||||
# GO_MIN_VERSION, mirroring how node is used when already present.
|
# described at GO_MAX_MINOR: at least GO_MIN_VERSION, and no newer in
|
||||||
|
# major.minor than the Go the pinned golangci-lint was built with. A
|
||||||
|
# newer host Go is not "good enough", it makes `make check` panic, so it
|
||||||
|
# is treated exactly like a missing one.
|
||||||
go_ok() {
|
go_ok() {
|
||||||
if missing go; then return 1; fi
|
if missing go; then return 1; fi
|
||||||
have="$(go version 2>/dev/null | awk '{print $3}')"
|
have="$(go version 2>/dev/null | awk '{print $3}')"
|
||||||
have="${have#go}"
|
have="${have#go}"
|
||||||
[ -n "$have" ] || return 1
|
[ -n "$have" ] || return 1
|
||||||
ver_ge "$have" "$GO_MIN_VERSION"
|
ver_ge "$have" "$GO_MIN_VERSION" || return 1
|
||||||
|
ver_ge "$GO_MAX_MINOR" "$(echo "$have" | cut -d. -f1,2)"
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_go() {
|
ensure_go() {
|
||||||
@@ -371,18 +464,62 @@ ensure_golangci_lint() {
|
|||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
fi
|
fi
|
||||||
link_bin "$GOLANGCI_LINT_DIR/golangci-lint" golangci-lint
|
link_bin "$GOLANGCI_LINT_DIR/golangci-lint" golangci-lint
|
||||||
if ! golangci_lint_ok; then
|
}
|
||||||
echo "bootstrap: a different golangci-lint precedes $BIN_DIR on your" >&2
|
|
||||||
echo " PATH; local lint findings may not match what CI gates on" >&2
|
# verify_toolchain: bootstrap must not exit 0 while the tools the gate
|
||||||
|
# will actually run are not the provisioned ones. Everything above only
|
||||||
|
# guarantees the right tools exist and are linked into $BIN_DIR; if
|
||||||
|
# something earlier on the caller's PATH shadows them, `make check` --
|
||||||
|
# and the pre-commit hook script/setup installs -- still break, and a
|
||||||
|
# warning buried in a long bootstrap log is not enough. So the checks
|
||||||
|
# re-run against the PATH the caller will have (theirs, plus $BIN_DIR at
|
||||||
|
# the front if bootstrap had to ask for it), and a failure is fatal.
|
||||||
|
verify_toolchain() {
|
||||||
|
verify_path="$ORIG_PATH"
|
||||||
|
if [ -n "$BIN_DIR" ]; then
|
||||||
|
case ":$ORIG_PATH:" in
|
||||||
|
*":$BIN_DIR:"*) ;;
|
||||||
|
*) verify_path="$BIN_DIR:$ORIG_PATH" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
saved_path="$PATH"
|
||||||
|
PATH="$verify_path"
|
||||||
|
export PATH
|
||||||
|
bad=""
|
||||||
|
go_ok || bad="$bad go"
|
||||||
|
golangci_lint_ok || bad="$bad golangci-lint"
|
||||||
|
for t in gofmt node yarn; do
|
||||||
|
if missing "$t"; then bad="$bad $t"; fi
|
||||||
|
done
|
||||||
|
PATH="$saved_path"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
[ -z "$bad" ] && return 0
|
||||||
|
|
||||||
|
echo "bootstrap: the toolchain on your PATH cannot run the gate." >&2
|
||||||
|
for t in $bad; do
|
||||||
|
where="$(
|
||||||
|
export PATH="$verify_path"
|
||||||
|
command -v "$t" || echo "not found"
|
||||||
|
)"
|
||||||
|
echo " $t: $where" >&2
|
||||||
|
done
|
||||||
|
echo " Expected these to come from $BIN_DIR. Something earlier on" >&2
|
||||||
|
echo " your PATH is shadowing them, or PATH does not reach it." >&2
|
||||||
|
echo " Put $BIN_DIR first in PATH, or remove the conflicting tool," >&2
|
||||||
|
echo " then re-run bootstrap. Failing rather than leaving you a" >&2
|
||||||
|
echo " bootstrap that reports success and a \`make check\` that does" >&2
|
||||||
|
echo " not run." >&2
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
toolchain="$HOME/.local/share/$("$ROOT/script/projectname")/toolchain"
|
TOOLCHAIN="$HOME/.local/share/$("$ROOT/script/projectname")/toolchain"
|
||||||
GO_DIR="$toolchain/go-$GO_VERSION"
|
GO_DIR="$TOOLCHAIN/go-$GO_VERSION"
|
||||||
GOLANGCI_LINT_DIR="$toolchain/golangci-lint-$GOLANGCI_LINT_VERSION"
|
GOLANGCI_LINT_DIR="$TOOLCHAIN/golangci-lint-$GOLANGCI_LINT_VERSION"
|
||||||
|
|
||||||
if missing make; then pkg_install gnumake make make make; fi
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
if missing git; then pkg_install git git git git; fi
|
if missing git; then pkg_install git git git git; fi
|
||||||
@@ -394,6 +531,8 @@ main() {
|
|||||||
ensure_go
|
ensure_go
|
||||||
ensure_golangci_lint
|
ensure_golangci_lint
|
||||||
|
|
||||||
|
verify_toolchain
|
||||||
|
|
||||||
echo "bootstrap complete"
|
echo "bootstrap complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user