build: unify the gate so root make check covers the backend (closes #16)
check / check (push) Successful in 11s
check / check (push) Successful in 11s
Root make check, and with it the pre-commit hook, now gates the Go backend too. The backend's Makefile targets are shims over backend/script/*; script/cibuild builds both images and is the workflow's only build step. Root make test runs both halves within one 30-second timeout. Root make lint runs golangci-lint only in Docker, by building the lint stage of Dockerfile.backend without the cache; the .golangci.yml drift check moved into backend/script/lint. script/bootstrap installs no linter: it reuses a Go at least as new as backend/go.mod asks for, otherwise installs the pinned, hash-verified release, linked into ~/.local/bin without replacing anything it did not create. With VERSION unset or empty, the backend version falls back to git describe inside a git checkout, then to dev. Model: opus-5-5
This commit was merged in pull request #38.
This commit is contained in:
+126
-11
@@ -5,7 +5,18 @@
|
||||
# or apk (detected in that order); assumes nothing is present. Node is
|
||||
# used directly if installed; otherwise it is installed at a pinned
|
||||
# version via nvm (installing nvm itself first, from a hash-verified
|
||||
# release archive, never curl | sh).
|
||||
# release archive, never curl | sh). Go, with its gofmt, is used
|
||||
# directly if it is at least the version backend/go.mod asks for;
|
||||
# otherwise the pinned Go 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
|
||||
# hook find it once that directory is on PATH. Nothing in ~/.local/bin
|
||||
# that this script did not create is ever replaced.
|
||||
#
|
||||
# golangci-lint is not installed: make lint runs it in Docker, which
|
||||
# this script does not install either.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -16,6 +27,12 @@ NVM_VERSION="0.40.3"
|
||||
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
|
||||
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
|
||||
YARN_VERSION="1.22.22"
|
||||
# The Go inside the golang:1.25-alpine image Dockerfile.backend builds
|
||||
# with, 2026-08-09. The archive hashes are in ensure_go.
|
||||
GO_VERSION="1.25.7"
|
||||
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
TOOLCHAIN="$HOME/.local/share/$("$ROOT/script/projectname")/toolchain"
|
||||
|
||||
PKGMGR=""
|
||||
SUDO=""
|
||||
@@ -79,6 +96,26 @@ verify_sha256() {
|
||||
fi
|
||||
}
|
||||
|
||||
# link_bin <target> <name>: make an installed tool reachable as
|
||||
# $BIN_DIR/<name>. Only a symlink this script made, one pointing into
|
||||
# $TOOLCHAIN or ~/.nvm, is ever replaced; if anything else is already
|
||||
# there, bootstrap stops.
|
||||
link_bin() {
|
||||
link="$BIN_DIR/$2"
|
||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
||||
case "$(readlink "$link" || true)" in
|
||||
"$TOOLCHAIN"/* | "$HOME"/.nvm/*) ;;
|
||||
*)
|
||||
echo "bootstrap: $link was not created by this script;" >&2
|
||||
echo " remove or rename it, then re-run bootstrap" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
mkdir -p "$BIN_DIR"
|
||||
ln -sf "$1" "$link"
|
||||
}
|
||||
|
||||
# nvm is a bash script; run a command in a bash with nvm loaded
|
||||
nvm_sh() {
|
||||
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
|
||||
@@ -103,39 +140,117 @@ ensure_node() {
|
||||
if ! missing node; then return 0; fi
|
||||
ensure_nvm
|
||||
nvm_sh "nvm install $NODE_VERSION"
|
||||
link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node
|
||||
}
|
||||
|
||||
# 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.
|
||||
ensure_yarn() {
|
||||
if ! missing yarn; then return 0; fi
|
||||
shims="$TOOLCHAIN/corepack-shims"
|
||||
mkdir -p "$shims"
|
||||
if ! missing corepack; then
|
||||
corepack enable
|
||||
corepack enable --install-directory "$shims"
|
||||
corepack prepare "yarn@$YARN_VERSION" --activate
|
||||
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 \"$shims\" && \
|
||||
corepack prepare yarn@$YARN_VERSION --activate"
|
||||
else
|
||||
npm install -g "yarn@$YARN_VERSION"
|
||||
npm install -g --prefix "$TOOLCHAIN/npm-global" "yarn@$YARN_VERSION"
|
||||
shims="$TOOLCHAIN/npm-global/bin"
|
||||
fi
|
||||
link_bin "$shims/yarn" yarn
|
||||
}
|
||||
|
||||
install_js_deps() {
|
||||
if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
|
||||
yarn install --frozen-lockfile"
|
||||
else
|
||||
yarn install --frozen-lockfile
|
||||
# go_ok: the go on PATH has its gofmt beside it (a Go release ships the
|
||||
# two together) and is at least the version backend/go.mod asks for.
|
||||
# GOTOOLCHAIN=local makes an older go fail here instead of fetching a
|
||||
# newer toolchain for itself.
|
||||
go_ok() {
|
||||
if missing go; then return 1; fi
|
||||
[ -x "$(dirname "$(command -v go)")/gofmt" ] || return 1
|
||||
(cd "$ROOT/backend" && GOTOOLCHAIN=local go list -m >/dev/null 2>&1)
|
||||
}
|
||||
|
||||
# ensure_go: unless go_ok, install GO_VERSION and link its go and gofmt.
|
||||
# They are linked on every run that needs them, so a deleted link is put
|
||||
# back, and the archive is unpacked again if either binary is missing.
|
||||
ensure_go() {
|
||||
if go_ok; then return 0; fi
|
||||
go_dir="$TOOLCHAIN/go-$GO_VERSION"
|
||||
if [ ! -x "$go_dir/bin/go" ] || [ ! -x "$go_dir/bin/gofmt" ]; then
|
||||
# sha256 of each archive, from https://go.dev/dl/?mode=json
|
||||
case "$(uname -s)-$(uname -m)" in
|
||||
Linux-x86_64)
|
||||
plat="linux-amd64"
|
||||
sha="12e6d6a191091ae27dc31f6efc630e3a3b8ba409baf3573d955b196fdf086005"
|
||||
;;
|
||||
Linux-aarch64)
|
||||
plat="linux-arm64"
|
||||
sha="ba611a53534135a81067240eff9508cd7e256c560edd5d8c2fef54f083c07129"
|
||||
;;
|
||||
Darwin-x86_64)
|
||||
plat="darwin-amd64"
|
||||
sha="bf5050a2152f4053837b886e8d9640c829dbacbc3370f913351eb0904cb706f5"
|
||||
;;
|
||||
Darwin-arm64)
|
||||
plat="darwin-arm64"
|
||||
sha="ff18369ffad05c57d5bed888b660b31385f3c913670a83ef557cdfd98ea9ae1b"
|
||||
;;
|
||||
*)
|
||||
echo "bootstrap: no pinned Go release for this platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if missing curl; then pkg_install curl curl curl curl; fi
|
||||
mkdir -p "$TOOLCHAIN"
|
||||
curl -fsSL -o "$go_dir.tar.gz" \
|
||||
"https://go.dev/dl/go$GO_VERSION.$plat.tar.gz"
|
||||
verify_sha256 "$go_dir.tar.gz" "$sha"
|
||||
# Unpacked beside its final place and then moved there, so an
|
||||
# interrupted run never leaves a partial Go that looks complete.
|
||||
rm -rf "$go_dir.partial"
|
||||
mkdir "$go_dir.partial"
|
||||
tar -xzf "$go_dir.tar.gz" -C "$go_dir.partial" --strip-components=1
|
||||
rm -rf "$go_dir" "$go_dir.tar.gz"
|
||||
mv "$go_dir.partial" "$go_dir"
|
||||
fi
|
||||
link_bin "$go_dir/bin/go" go
|
||||
link_bin "$go_dir/bin/gofmt" gofmt
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
# Tools linked on an earlier run count as installed, and tools linked
|
||||
# on this run are found by the steps after it.
|
||||
path_hint=""
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*) path_hint=yes ;;
|
||||
esac
|
||||
PATH="$BIN_DIR:$PATH"
|
||||
|
||||
if missing make; then pkg_install gnumake make make make; fi
|
||||
if missing git; then pkg_install git git git git; fi
|
||||
|
||||
ensure_node
|
||||
ensure_yarn
|
||||
install_js_deps
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
ensure_go
|
||||
(cd "$ROOT/backend" && go mod download)
|
||||
|
||||
if missing docker; then
|
||||
echo "bootstrap: docker not found; make lint, and so make check" >&2
|
||||
echo " and the pre-commit hook, need it to run the Go linter" >&2
|
||||
fi
|
||||
if [ -n "$path_hint" ] && [ -d "$BIN_DIR" ]; then
|
||||
echo "bootstrap: add $BIN_DIR to the front of your PATH, e.g." >&2
|
||||
echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2
|
||||
fi
|
||||
|
||||
echo "bootstrap complete"
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,13 +1,17 @@
|
||||
#!/bin/sh
|
||||
# script/cibuild: run the CI build. The Dockerfile runs make check, so
|
||||
# a successful build implies all checks pass.
|
||||
# script/cibuild: run the CI build. It builds both images: the frontend
|
||||
# from Dockerfile and the backend from Dockerfile.backend. Each runs its
|
||||
# half of the checks as build steps, so a successful cibuild implies the
|
||||
# whole repo is green. This is the only build step the Gitea workflow
|
||||
# runs.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build .
|
||||
timeout 300 docker build .
|
||||
timeout 300 docker build -f Dockerfile.backend .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+6
-3
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
# script/docker: build the Docker image tagged with the project name.
|
||||
# The tag comes from script/projectname.
|
||||
# script/docker: build both Docker images, tagged with the project name
|
||||
# from script/projectname: the frontend as <name>, from Dockerfile, and
|
||||
# the backend as <name>-server, from Dockerfile.backend.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
@@ -8,7 +9,9 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
timeout 300 docker build -t "$("$SCRIPT_DIR/projectname")" .
|
||||
name="$("$SCRIPT_DIR/projectname")"
|
||||
timeout 300 docker build -t "$name" .
|
||||
timeout 300 docker build -t "$name-server" -f Dockerfile.backend .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+4
-2
@@ -1,12 +1,14 @@
|
||||
#!/bin/sh
|
||||
# script/fmt: format all files (writes).
|
||||
# script/fmt: format the whole repo (writes): prettier over everything
|
||||
# it understands, then gofmt over the Go backend.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn prettier --write .
|
||||
"$ROOT/script/frontend-fmt"
|
||||
"$ROOT/backend/script/fmt"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+4
-3
@@ -1,13 +1,14 @@
|
||||
#!/bin/sh
|
||||
# script/fmt-check: check formatting (read-only). Same scope as
|
||||
# script/fmt, but fails instead of writing.
|
||||
# script/fmt-check: check formatting across the whole repo (read-only).
|
||||
# Same scope as script/fmt, but fails instead of writing.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn prettier --check .
|
||||
"$ROOT/script/frontend-fmt-check"
|
||||
"$ROOT/backend/script/fmt-check"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# script/frontend-check: run the frontend half of the checks only (test,
|
||||
# lint, fmt-check). This exists for the frontend Dockerfile, whose build
|
||||
# stage is a node image with neither Go nor Docker; the backend half is
|
||||
# gated by Dockerfile.backend. Everywhere else, use script/check, which
|
||||
# covers the whole repo. Must not modify any files.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
"$ROOT/script/frontend-test"
|
||||
"$ROOT/script/frontend-lint"
|
||||
"$ROOT/script/frontend-fmt-check"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# script/frontend-fmt: format the frontend and every other file prettier
|
||||
# understands, repo-wide (writes). backend/ is in .prettierignore; Go
|
||||
# sources are formatted by backend/script/fmt.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn prettier --write .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# script/frontend-fmt-check: check prettier formatting (read-only). Same
|
||||
# scope as script/frontend-fmt, but fails instead of writing.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn prettier --check .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# script/frontend-lint: run the frontend linter (prettier in check mode).
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn prettier --check .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# script/frontend-test: run the frontend test suite. The frontend has no
|
||||
# unit tests; the production build serves as the test (fails on broken
|
||||
# code).
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
timeout 30 yarn build
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -45,7 +45,7 @@ main() {
|
||||
cd "$ROOT"
|
||||
|
||||
# Test what ships: the production build, not a dev server.
|
||||
"$ROOT/script/test"
|
||||
"$ROOT/script/frontend-test"
|
||||
if [ ! -f "$ROOT/dist/index.html" ]; then
|
||||
echo "frontend-viewport-test: dist/index.html missing after build" >&2
|
||||
exit 1
|
||||
|
||||
+11
-2
@@ -1,12 +1,21 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linter (prettier in check mode).
|
||||
# script/lint: lint the whole repo: prettier over the frontend, then the
|
||||
# Go linter over backend/.
|
||||
#
|
||||
# The Go linter runs only in Docker: this builds the lint stage of
|
||||
# Dockerfile.backend, the digest-pinned golangci-lint image, which runs
|
||||
# the backend's fmt-check and lint targets. --no-cache makes the linter
|
||||
# really run every time rather than reuse an earlier result, and the
|
||||
# stage is built for its checks alone, so no image is kept.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn prettier --check .
|
||||
"$ROOT/script/frontend-lint"
|
||||
timeout 300 docker build --no-cache --target lint \
|
||||
--output type=cacheonly -f Dockerfile.backend .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+4
-3
@@ -1,13 +1,14 @@
|
||||
#!/bin/sh
|
||||
# script/test: run the test suite. This repo has no unit tests; the
|
||||
# production build serves as the test (fails on broken code).
|
||||
# script/test: run the test suite for the whole repo: the frontend at
|
||||
# the repo root, then the Go backend in backend/. Both halves together
|
||||
# get 30 seconds; each also keeps its own limit for the Dockerfiles.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
timeout 30 yarn build
|
||||
timeout 30 sh -c 'script/frontend-test && backend/script/test'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user