diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 860d9dd..a732557 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -6,5 +6,5 @@ jobs: steps: # actions/checkout v4.2.2, 2026-02-22 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + # script/cibuild builds both images. - run: script/cibuild - - run: docker build -f Dockerfile.backend . diff --git a/Dockerfile b/Dockerfile index ed37836..492b41d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,12 @@ COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile RUN apk add --no-cache git make COPY . . -# make check runs script/check (test + lint + fmt-check); its test step -# is the production yarn build, so this both produces dist/ and gates the -# image on lint/fmt-check/test regressions, not merely a broken build. -RUN make check +# make frontend-check is the frontend half of make check (test + lint + +# fmt-check); its test step is the production yarn build, so this both +# produces dist/ and gates the image on lint/fmt-check/test regressions. +# This node stage has neither Go nor Docker for the other half, which +# Dockerfile.backend gates; script/cibuild builds both images. +RUN make frontend-check # nginx:stable-alpine as of 2026-02-22 FROM nginx@sha256:15e96e59aa3b0aada3a121296e3bce117721f42d88f5f64217ef4b18f458c6ab diff --git a/Dockerfile.backend b/Dockerfile.backend index 191a65e..51a530f 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -30,12 +30,11 @@ COPY backend/ . RUN make test -# The build is driven through the Makefile so there is exactly one -# definition of the build command; it expands to +# make build is a shim around backend/script/build, the one definition +# of the build command: # CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=... -X main.Buildarch=..." -# VERSION is handed over in the environment rather than as a make -# variable so it still reaches the build if the target is ever turned -# into a shim around a script. +# That script reads VERSION from the environment, so it is handed over +# there rather than as a make variable. ARG VERSION=dev RUN VERSION="${VERSION}" make build diff --git a/Makefile b/Makefile index 20f313f..eb7bb43 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ -.PHONY: bootstrap setup dev test lint fmt fmt-check check \ +.PHONY: bootstrap setup dev test lint fmt fmt-check check frontend-check \ frontend-viewport-test docker hooks # Standard targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section -# of README.md). +# of README.md). test, lint, fmt, fmt-check and check cover the whole +# repo: the frontend here and the Go backend in backend/. bootstrap: @script/bootstrap @@ -29,6 +30,11 @@ fmt-check: check: @script/check +# The frontend half of check, for Dockerfile's node build stage, which +# has neither Go nor Docker. Use check everywhere else. +frontend-check: + @script/frontend-check + # Responsive-layout verification in a containerised browser. Kept out of # check: it needs Docker and takes minutes, where make test has to stay # under 20 seconds. diff --git a/README.md b/README.md index 6924046..52e6382 100644 --- a/README.md +++ b/README.md @@ -31,25 +31,41 @@ docker run -p 8080:8080 netwatch This repository adheres to the [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) standard: normalized scripts in `script/` are the entrypoints for the -development workflow, and the Makefile targets are thin shims that call them. We -provide: +development workflow, and the Makefile targets are thin shims that call them. +The Go backend in `backend/` has its own `script/` directory and shim Makefile +(see [backend/README.md](backend/README.md)). The root scripts cover both +halves, so the root `make check` fails if either one is broken. We provide: - `script/bootstrap` — install all dependencies (pinned node via nvm if needed, - yarn via corepack, `yarn install --frozen-lockfile`) + yarn via corepack, `yarn install --frozen-lockfile`, the pinned Go unless one + at least as new as `backend/go.mod` asks for is installed, 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 lint` runs the + linter in Docker - `script/setup` — make a fresh clone ready for development: bootstrap plus the git pre-commit hook -- `script/projectname` — print the project name (used for the Docker image tag) -- `script/test` — run the production build as the test (no unit tests yet) -- `script/lint` — run prettier in check mode -- `script/fmt` — format all files (writes) -- `script/fmt-check` — check formatting (read-only) +- `script/projectname` — print the project name (used for the Docker image tags) +- `script/test` — run `script/frontend-test`, then the backend's Go tests, both + within one 30-second timeout +- `script/lint` — run `script/frontend-lint`, then golangci-lint in Docker, by + building the lint stage of `Dockerfile.backend` without the cache +- `script/fmt` — format all files (writes): prettier, then gofmt over `backend/` +- `script/fmt-check` — check formatting (read-only): prettier, then gofmt - `script/check` — run test, lint, and fmt-check +- `script/frontend-test` — run the production build as the frontend's test (no + unit tests yet) +- `script/frontend-lint` — run prettier in check mode +- `script/frontend-fmt` — format everything prettier understands (writes) +- `script/frontend-fmt-check` — check prettier formatting (read-only) +- `script/frontend-check` — the frontend half of `script/check`, for + `Dockerfile`, whose node build stage has neither Go nor Docker - `script/frontend-viewport-test` — responsive-layout verification of the built frontend in a containerised headless Chrome (see [test/viewport/README.md](test/viewport/README.md)). Not part of `script/check`: it needs Docker and takes minutes. -- `script/docker` — build the Docker image tagged via `script/projectname` -- `script/cibuild` — CI entrypoint: plain `docker build .` +- `script/docker` — build both images, tagged via `script/projectname`: + `netwatch` from `Dockerfile` and `netwatch-server` from `Dockerfile.backend` +- `script/cibuild` — CI entrypoint: builds both images - `script/precommit` — run by the git pre-commit hook; runs `script/check` - `script/install-precommit` — install the git pre-commit hook diff --git a/TODO.md b/TODO.md index ae23deb..ecb3301 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,18 @@ latest run passes. # Completed Steps +- 2026-09-28: unified the gate (issue #16): the root `make check` covers the Go + backend as well as the frontend, and the pre-commit hook with it; the backend + moved onto scripts-to-rule-them-all (`backend/script/*`, `backend/Makefile` as + shims, its duplicate hook installer removed); `script/cibuild` builds both + images and is the workflow's only build step. The root `make lint` runs + golangci-lint only in Docker, by building the lint stage of + `Dockerfile.backend` without the cache. `script/bootstrap` installs the pinned + Go unless the installed one is at least what `backend/go.mod` asks for, links + what it installs into `~/.local/bin` without replacing anything it did not + create, and installs no linter. Root `make test` runs both halves within one + 30-second timeout. When `VERSION` is unset or empty, the backend binary's + version falls back to `git describe` inside a git checkout, then to `dev` - 2026-09-28: frontend reporting client (issue #53): a `Reporter` class posts collected samples to `/api/v1/reports` every `reportInterval` (default 60s) as a per-host delta, with the report-building step a pure exported function of diff --git a/backend/Makefile b/backend/Makefile index 96edfb9..5724459 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,69 +1,30 @@ -# VERSION is overridable (the Dockerfile passes its ARG VERSION in) and -# degrades to "dev" when git is unavailable or there is no .git — the -# build must not depend on the repository history being in the build -# context. -VERSION ?= $(shell { git describe --always --dirty; } 2>/dev/null || echo dev) -BUILDARCH := $(shell uname -m) -BINARY := netwatch-server +# Thin shims; the implementations live in backend/script/ (see the +# Entrypoints section of README.md). There is no check, hooks or docker +# target here: the root Makefile's check covers this directory, its +# hooks target installs the repo's only pre-commit hook, and its docker +# target builds this image, whose build context is the repo root. -GOLDFLAGS += -s -w -GOLDFLAGS += -X main.Version=$(VERSION) -GOLDFLAGS += -X main.Buildarch=$(BUILDARCH) - -# macOS ships shasum rather than sha256sum. -SHA256SUM := $(shell command -v sha256sum >/dev/null 2>&1 && echo sha256sum || echo shasum -a 256) - -# .golangci.yml is standardized org-wide and must never be edited here -# (REPO_POLICIES.md). Its last silent drift replaced the v2 schema with -# v1 keys, which left every threshold in the file inert while the build -# stayed green. The lint target therefore asserts the file still matches -# the canonical copy byte for byte. The check is a local hash comparison: -# no network, no remote schema, nothing unpinned in the build path. -GOLANGCI_CONFIG_SHA256 := 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb - -.PHONY: all build test lint fmt fmt-check check docker hooks run clean +.PHONY: all build test lint fmt fmt-check run clean all: build build: - CGO_ENABLED=0 go build -trimpath -ldflags "$(GOLDFLAGS)" \ - -o ./$(BINARY) ./cmd/netwatch-server/ + @script/build test: - timeout 30 go test ./... + @script/test lint: - @actual=$$($(SHA256SUM) .golangci.yml | cut -d' ' -f1); \ - if [ "$$actual" != "$(GOLANGCI_CONFIG_SHA256)" ]; then \ - echo ".golangci.yml has drifted from the org standard."; \ - echo " expected $(GOLANGCI_CONFIG_SHA256)"; \ - echo " actual $$actual"; \ - echo "Restore it verbatim from sneak/prompts; do not edit it."; \ - exit 1; \ - fi - golangci-lint run ./... + @script/lint fmt: - go fmt ./... + @script/fmt fmt-check: - @test -z "$$(gofmt -l .)" || \ - (echo "Files not formatted:"; gofmt -l .; exit 1) + @script/fmt-check -check: test lint fmt-check - -docker: - timeout 300 docker build -t netwatch-server -f ../Dockerfile.backend .. - -hooks: - @printf '#!/bin/sh\ncd backend && make check\n' > \ - $$(git rev-parse --show-toplevel)/.git/hooks/pre-commit - @chmod +x \ - $$(git rev-parse --show-toplevel)/.git/hooks/pre-commit - @echo "Pre-commit hook installed" - -run: build - ./$(BINARY) +run: + @script/run clean: - rm -f ./$(BINARY) + @script/clean diff --git a/backend/README.md b/backend/README.md index 3cad737..1230c12 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,18 +4,50 @@ SPA and persists them as zstd-compressed JSONL files on disk. ## Getting Started +From this directory: + ```bash # Build and run locally make run +``` -# Run tests, lint, and format check +From the repo root, which is also the build context of `Dockerfile.backend`: + +```bash +# Run tests, lint, and format check over the frontend and this backend make check -# Docker -docker build -t netwatch-server . +# Build both images, including netwatch-server +make docker docker run -p 8080:8080 netwatch-server ``` +## Entrypoints + +This directory follows the same +[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) +pattern as the repo root: the targets in `backend/Makefile` are thin shims over +`backend/script/`. `Dockerfile.backend` runs them, and the root scripts call +`test`, `fmt` and `fmt-check`: + +- `script/build` — compile the static `netwatch-server` binary with its version + and architecture stamped in. The version is `VERSION` from the environment; + when that is unset or empty, it falls back to `git describe` inside a git + checkout, then to `dev` +- `script/test` — run the Go tests under a 30-second timeout +- `script/lint` — check `.golangci.yml` against its pinned sha256, then run + golangci-lint. It runs inside the golangci-lint image of the lint stage of + `Dockerfile.backend`; from a checkout, run `make lint` at the repo root, which + builds that stage +- `script/fmt` — format the Go sources (writes) +- `script/fmt-check` — check Go formatting (read-only) +- `script/run` — build and run the server locally +- `script/clean` — remove build artifacts + +There is no `check`, `hooks` or `docker` target here: the root `make check` +covers this directory, the root `make hooks` installs the repo's only pre-commit +hook, and the root `make docker` builds this image. + ## Rationale The NetWatch frontend collects latency measurements from the browser but has no diff --git a/backend/script/build b/backend/script/build new file mode 100755 index 0000000..36b3e96 --- /dev/null +++ b/backend/script/build @@ -0,0 +1,21 @@ +#!/bin/sh +# script/build: compile the static netwatch-server binary into the +# backend project root, with its version and architecture stamped in. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + + # VERSION comes from the environment (Dockerfile.backend passes its + # ARG VERSION in). Unset or empty, it is git describe, or "dev" where + # there is no git or no repository history. + version="${VERSION:-$(git describe --always --dirty 2>/dev/null || echo dev)}" + + CGO_ENABLED=0 go build -trimpath \ + -ldflags "-s -w -X main.Version=$version -X main.Buildarch=$(uname -m)" \ + -o netwatch-server ./cmd/netwatch-server/ +} + +main "$@" diff --git a/backend/script/clean b/backend/script/clean new file mode 100755 index 0000000..1f4e638 --- /dev/null +++ b/backend/script/clean @@ -0,0 +1,12 @@ +#!/bin/sh +# script/clean: remove build artifacts. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + rm -f netwatch-server +} + +main "$@" diff --git a/backend/script/fmt b/backend/script/fmt new file mode 100755 index 0000000..9fa78bf --- /dev/null +++ b/backend/script/fmt @@ -0,0 +1,12 @@ +#!/bin/sh +# script/fmt: format the Go sources (writes). +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + go fmt ./... +} + +main "$@" diff --git a/backend/script/fmt-check b/backend/script/fmt-check new file mode 100755 index 0000000..57405d0 --- /dev/null +++ b/backend/script/fmt-check @@ -0,0 +1,18 @@ +#!/bin/sh +# script/fmt-check: check Go formatting (read-only). Same scope as +# script/fmt, but fails instead of writing. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + unformatted="$(gofmt -l .)" + if [ -n "$unformatted" ]; then + echo "Files not formatted:" >&2 + echo "$unformatted" >&2 + exit 1 + fi +} + +main "$@" diff --git a/backend/script/lint b/backend/script/lint new file mode 100755 index 0000000..d00c5b0 --- /dev/null +++ b/backend/script/lint @@ -0,0 +1,32 @@ +#!/bin/sh +# script/lint: run golangci-lint over the backend. This runs inside the +# lint stage of Dockerfile.backend, whose digest-pinned golangci-lint +# image provides the linter; nothing installs golangci-lint on the host. +# From a checkout, run `make lint` at the repo root, which builds that +# stage. +# +# .golangci.yml is standardized org-wide and must never be edited here +# (REPO_POLICIES.md). Its last silent drift replaced the v2 schema with +# v1 keys, which left every threshold in the file inert while the build +# stayed green. So the file is first checked against the canonical +# copy's sha256: a local comparison, no network, nothing unpinned. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +GOLANGCI_CONFIG_SHA256="021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb" + +main() { + cd "$ROOT" + actual="$(sha256sum .golangci.yml | cut -d' ' -f1)" + if [ "$actual" != "$GOLANGCI_CONFIG_SHA256" ]; then + echo ".golangci.yml has drifted from the org standard." >&2 + echo " expected $GOLANGCI_CONFIG_SHA256" >&2 + echo " actual $actual" >&2 + echo "Restore it verbatim from sneak/prompts; do not edit it." >&2 + exit 1 + fi + golangci-lint run ./... +} + +main "$@" diff --git a/backend/script/run b/backend/script/run new file mode 100755 index 0000000..5ea87ba --- /dev/null +++ b/backend/script/run @@ -0,0 +1,13 @@ +#!/bin/sh +# script/run: build and run netwatch-server locally. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + "$ROOT/script/build" + exec ./netwatch-server "$@" +} + +main "$@" diff --git a/backend/script/test b/backend/script/test new file mode 100755 index 0000000..cbfc889 --- /dev/null +++ b/backend/script/test @@ -0,0 +1,12 @@ +#!/bin/sh +# script/test: run the backend test suite. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + timeout 30 go test ./... +} + +main "$@" diff --git a/script/bootstrap b/script/bootstrap index 4df1d8c..f94201d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -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 : make an installed tool reachable as +# $BIN_DIR/. 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" } diff --git a/script/cibuild b/script/cibuild index 966f51d..d3eb606 100755 --- a/script/cibuild +++ b/script/cibuild @@ -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 "$@" diff --git a/script/docker b/script/docker index aa9387f..6476087 100755 --- a/script/docker +++ b/script/docker @@ -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 , from Dockerfile, and +# the backend as -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 "$@" diff --git a/script/fmt b/script/fmt index e7d63e2..7270800 100755 --- a/script/fmt +++ b/script/fmt @@ -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 "$@" diff --git a/script/fmt-check b/script/fmt-check index 07ad5ea..28518f7 100755 --- a/script/fmt-check +++ b/script/fmt-check @@ -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 "$@" diff --git a/script/frontend-check b/script/frontend-check new file mode 100755 index 0000000..c2b8314 --- /dev/null +++ b/script/frontend-check @@ -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 "$@" diff --git a/script/frontend-fmt b/script/frontend-fmt new file mode 100755 index 0000000..1af33b9 --- /dev/null +++ b/script/frontend-fmt @@ -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 "$@" diff --git a/script/frontend-fmt-check b/script/frontend-fmt-check new file mode 100755 index 0000000..1501fce --- /dev/null +++ b/script/frontend-fmt-check @@ -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 "$@" diff --git a/script/frontend-lint b/script/frontend-lint new file mode 100755 index 0000000..12b5b29 --- /dev/null +++ b/script/frontend-lint @@ -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 "$@" diff --git a/script/frontend-test b/script/frontend-test new file mode 100755 index 0000000..4c74c65 --- /dev/null +++ b/script/frontend-test @@ -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 "$@" diff --git a/script/frontend-viewport-test b/script/frontend-viewport-test index aa1b2f6..53a2ee1 100755 --- a/script/frontend-viewport-test +++ b/script/frontend-viewport-test @@ -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 diff --git a/script/lint b/script/lint index 054682a..8eea8a2 100755 --- a/script/lint +++ b/script/lint @@ -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 "$@" diff --git a/script/test b/script/test index 4e98401..f0ad7e8 100755 --- a/script/test +++ b/script/test @@ -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 "$@"