diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 860d9dd..368fc6e 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -6,5 +6,6 @@ jobs: steps: # actions/checkout v4.2.2, 2026-02-22 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + # script/cibuild builds both images; it is the only build + # step, so how the repo builds stays defined in script/. - run: script/cibuild - - run: docker build -f Dockerfile.backend . diff --git a/Dockerfile b/Dockerfile index ed37836..2503d81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,14 @@ 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 check-frontend runs script/frontend-check (test + lint + +# fmt-check for the frontend); 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. It is the +# frontend half of `make check` rather than all of it because this stage +# is a node image with no Go toolchain; the backend half is gated by +# Dockerfile.backend, and script/cibuild builds both images. +RUN make check-frontend # nginx:stable-alpine as of 2026-02-22 FROM nginx@sha256:15e96e59aa3b0aada3a121296e3bce117721f42d88f5f64217ef4b18f458c6ab diff --git a/Makefile b/Makefile index 353f708..261fbe7 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -.PHONY: bootstrap setup dev test lint fmt fmt-check check docker hooks +.PHONY: bootstrap setup dev test lint fmt fmt-check check check-frontend \ + check-backend 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 all cover the +# whole repo: the frontend at the root and the Go backend in backend/. bootstrap: @script/bootstrap @@ -28,6 +30,14 @@ fmt-check: check: @script/check +# Half-repo gates. Used by the two Dockerfiles, whose build stages only +# have the toolchain for their own half; prefer `make check` otherwise. +check-frontend: + @script/frontend-check + +check-backend: + @backend/script/check + docker: @script/docker diff --git a/README.md b/README.md index d4dbdd4..226d314 100644 --- a/README.md +++ b/README.md @@ -28,23 +28,55 @@ 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 repo holds two projects: the frontend at the repo root and the Go backend in +`backend/`, which has its own `script/` directory and its own shim Makefile. The +root scripts cover both, so `make check` at the root fails if either half is +broken. We provide: - `script/bootstrap` — install all dependencies (pinned node via nvm if needed, yarn via corepack, `yarn install --frozen-lockfile`) - `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/check` — run test, lint, and fmt-check -- `script/docker` — build the Docker image tagged via `script/projectname` -- `script/cibuild` — CI entrypoint: plain `docker build .` -- `script/precommit` — run by the git pre-commit hook; runs `script/check` -- `script/install-precommit` — install the git pre-commit hook +- `script/projectname` — print the project name (used for the Docker image tags) +- `script/test` — run the whole repo's tests: `script/frontend-test`, then + `backend/script/test` +- `script/lint` — lint the whole repo: `script/frontend-lint`, then + `backend/script/lint` +- `script/fmt` — format the whole repo (writes): `script/frontend-fmt`, then + `backend/script/fmt` +- `script/fmt-check` — check formatting across the whole repo (read-only) +- `script/check` — run test, lint, and fmt-check; this is the repo-wide gate +- `script/frontend-test` — the frontend's test: the production build (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`, used by + `Dockerfile`, whose build stage is a node image with no Go toolchain +- `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; the only build step in + the Gitea workflow +- `script/precommit` — run by the git pre-commit hook; runs `script/check`, so a + commit is gated on both halves of the repo +- `script/install-precommit` — install the git pre-commit hook; this is the + repo's only pre-commit hook installer + +The backend's scripts are shimmed by `backend/Makefile` and are also called by +the root scripts above: + +- `backend/script/build` — compile `netwatch-server` with version and + architecture stamped in +- `backend/script/test` — run the Go tests under a 30-second timeout +- `backend/script/lint` — assert `.golangci.yml` still matches its pinned + sha256, then run golangci-lint +- `backend/script/fmt` — format the Go sources (writes) +- `backend/script/fmt-check` — check Go formatting (read-only) +- `backend/script/check` — run the backend's test, lint, and fmt-check +- `backend/script/run` — build and run the server locally +- `backend/script/clean` — remove build artifacts ## Rationale diff --git a/TODO.md b/TODO.md index 587c805..7857a56 100644 --- a/TODO.md +++ b/TODO.md @@ -22,6 +22,11 @@ files, so merging it also closes most compliance gaps. # Completed Steps +- 2026-08-09: unified the gate: the root `make check` now covers the Go backend + as well as the frontend, the backend moved onto scripts-to-rule-them-all + (`backend/script/*` with `backend/Makefile` as thin shims), the duplicate + pre-commit hook installer in `backend/Makefile` was removed, and + `script/cibuild` now builds both images as the workflow's only build step - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile shims, README Entrypoints section - 2026-02-27: backend with buffered zstd-compressed report storage; CI workflow diff --git a/backend/Makefile b/backend/Makefile index 38cebcf..082354f 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,53 +1,38 @@ -UNAME_S := $(shell uname -s) -VERSION := $(shell git describe --always --dirty) -BUILDARCH := $(shell uname -m) -BINARY := netwatch-server +# Standard targets are thin shims; the implementations live in +# backend/script/ per the scripts-to-rule-them-all pattern (see the +# Entrypoints section of README.md). +# +# There is no `hooks` target here: the repo has exactly one pre-commit +# hook installer, the root `script/install-precommit`, and the hook it +# installs gates both halves of the repo. There is no `docker` target +# either: the backend image is built from Dockerfile.backend with the +# repo root as its context, so it belongs to the root `make docker` and +# `script/cibuild`. -GOLDFLAGS += -X main.Version=$(VERSION) -GOLDFLAGS += -X main.Buildarch=$(BUILDARCH) - -ifeq ($(UNAME_S),Darwin) - GOFLAGS := -ldflags "$(GOLDFLAGS)" -else - GOFLAGS = -ldflags "-linkmode external -extldflags -static $(GOLDFLAGS)" -endif - -.PHONY: all build test lint fmt fmt-check check docker hooks run clean +.PHONY: all build test lint fmt fmt-check check run clean all: build -build: ./$(BINARY) - -./$(BINARY): $(shell find . -name '*.go' -type f) go.mod go.sum - go build -o $@ $(GOFLAGS) ./cmd/netwatch-server/ +build: + @script/build test: - timeout 30 go test ./... + @script/test lint: - 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 +check: + @script/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 a7de988..0673acd 100644 --- a/backend/README.md +++ b/backend/README.md @@ -11,11 +11,38 @@ make run # Run tests, lint, and format check make check -# Docker -docker build -t netwatch-server . +# Docker (from the repo root; the image's build context is the repo root) +make docker docker run -p 8080:8080 netwatch-server ``` +## Entrypoints + +This project follows the same +[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) +pattern as the repo root: the implementations live in `backend/script/` and the +targets in `backend/Makefile` are thin shims that call them. The repo root's +`script/test`, `script/lint`, `script/fmt` and `script/fmt-check` call these +too, so the root `make check` covers the backend. + +- `script/build` — compile `netwatch-server` with the version and architecture + stamped in via ldflags (statically linked on Linux) +- `script/test` — run the Go tests under a 30-second timeout +- `script/lint` — assert `.golangci.yml` still matches its pinned sha256, then + run golangci-lint +- `script/fmt` — format the Go sources (writes) +- `script/fmt-check` — check Go formatting (read-only) +- `script/check` — run test, lint, and fmt-check +- `script/run` — build and run the server locally +- `script/clean` — remove build artifacts + +There is deliberately no `hooks` target here: the repo has exactly one +pre-commit hook installer, the root `script/install-precommit`, and the hook it +installs runs the root `script/check`, which gates both halves of the repo. +There is no `docker` target either: `Dockerfile.backend` lives at the repo root +and builds with the repo root as its context, so the backend image is built by +the root `make docker` and by `script/cibuild`. + ## 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..a157576 --- /dev/null +++ b/backend/script/build @@ -0,0 +1,27 @@ +#!/bin/sh +# script/build: compile the netwatch-server binary into the backend +# project root. Version and architecture are stamped into the binary via +# ldflags; on Linux the binary is statically linked. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +BINARY="netwatch-server" + +main() { + cd "$ROOT" + + # git describe fails outside a working repo (e.g. a source tarball), + # which must not abort the build. + version="$(git describe --always --dirty 2>/dev/null || echo unknown)" + buildarch="$(uname -m)" + + ldflags="-X main.Version=$version -X main.Buildarch=$buildarch" + if [ "$(uname -s)" != "Darwin" ]; then + ldflags="-linkmode external -extldflags -static $ldflags" + fi + + go build -o "$BINARY" -ldflags "$ldflags" ./cmd/netwatch-server/ +} + +main "$@" diff --git a/backend/script/check b/backend/script/check new file mode 100755 index 0000000..44f4fc8 --- /dev/null +++ b/backend/script/check @@ -0,0 +1,15 @@ +#!/bin/sh +# script/check: run all backend checks (test, lint, fmt-check). Must not +# modify any files. The root script/check calls this, so the repo-wide +# gate covers the backend. +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" + +main() { + "$SCRIPT_DIR/test" + "$SCRIPT_DIR/lint" + "$SCRIPT_DIR/fmt-check" +} + +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..f45b74b --- /dev/null +++ b/backend/script/fmt @@ -0,0 +1,12 @@ +#!/bin/sh +# script/fmt: format all Go sources in the backend (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..a979aab --- /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:" + echo "$unformatted" + exit 1 + fi +} + +main "$@" diff --git a/backend/script/lint b/backend/script/lint new file mode 100755 index 0000000..0f3d0e9 --- /dev/null +++ b/backend/script/lint @@ -0,0 +1,46 @@ +#!/bin/sh +# script/lint: run the Go linter over the backend. +# +# .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. This script therefore asserts the file still matches the +# pinned copy byte for byte before the linter runs. The check is a local +# hash comparison: no network, no remote schema, nothing unpinned in the +# build path. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +# sha256 of the pinned backend/.golangci.yml. +GOLANGCI_CONFIG_SHA256="33ba2bf7fe4a44779d09b0fb31d6daf03685f8dc9d2bc417f963d7aabb0d17dc" + +# sha256 : print the file's sha256, coreutils or Darwin/busybox. +sha256() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi +} + +check_config_hash() { + actual="$(sha256 .golangci.yml)" + if [ "$actual" != "$GOLANGCI_CONFIG_SHA256" ]; then + echo ".golangci.yml has drifted from the pinned config." + echo " expected $GOLANGCI_CONFIG_SHA256" + echo " actual $actual" + echo "Restore it verbatim from sneak/prompts; do not edit it." + echo "Only update GOLANGCI_CONFIG_SHA256 in this script when the" + echo "pinned config is deliberately replaced with a new standard." + exit 1 + fi +} + +main() { + cd "$ROOT" + check_config_hash + golangci-lint run ./... +} + +main "$@" diff --git a/backend/script/run b/backend/script/run new file mode 100755 index 0000000..5e9658f --- /dev/null +++ b/backend/script/run @@ -0,0 +1,14 @@ +#!/bin/sh +# script/run: build and run netwatch-server locally. +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" + +main() { + "$SCRIPT_DIR/build" + cd "$ROOT" + 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/check b/script/check index 3e1778c..07b036b 100755 --- a/script/check +++ b/script/check @@ -1,6 +1,7 @@ #!/bin/sh -# script/check: run all checks (test, lint, fmt-check). Our own -# extension to scripts-to-rule-them-all. Must not modify any files. +# script/check: run all checks (test, lint, fmt-check) across the whole +# repo, frontend and backend. Our own extension to +# scripts-to-rule-them-all. Must not modify any files. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" diff --git a/script/cibuild b/script/cibuild index 966f51d..ce7fb91 100755 --- a/script/cibuild +++ b/script/cibuild @@ -1,13 +1,24 @@ #!/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 every image in the repo: +# the frontend image from Dockerfile and the backend image from +# Dockerfile.backend. Each Dockerfile runs its half of make check as a +# build step, 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)" +# build_image : build one image from the repo root context. +# Every docker build CI performs goes through here, so build-wide flags +# only ever have to be added in one place. +build_image() { + timeout 300 docker build -f "$1" . +} + main() { cd "$ROOT" - docker build . + build_image Dockerfile + build_image Dockerfile.backend } main "$@" diff --git a/script/docker b/script/docker index aa9387f..7c3b594 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 the repo's Docker images, tagged from +# script/projectname: the frontend image as and the backend image +# as -server. Both build from the repo root as their context. 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" -f Dockerfile . + 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..249361a --- /dev/null +++ b/script/frontend-check @@ -0,0 +1,17 @@ +#!/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 no Go toolchain; 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 + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" + +main() { + "$SCRIPT_DIR/frontend-test" + "$SCRIPT_DIR/frontend-lint" + "$SCRIPT_DIR/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/lint b/script/lint index 054682a..183e710 100755 --- a/script/lint +++ b/script/lint @@ -1,12 +1,14 @@ #!/bin/sh -# script/lint: run the linter (prettier in check mode). +# script/lint: lint the whole repo: prettier over everything it +# understands, then golangci-lint over the Go backend. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - yarn prettier --check . + "$ROOT/script/frontend-lint" + "$ROOT/backend/script/lint" } main "$@" diff --git a/script/test b/script/test index 4e98401..96c32bf 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/. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - timeout 30 yarn build + "$ROOT/script/frontend-test" + "$ROOT/backend/script/test" } main "$@"