From f4237689757a2ede0160c1532b470cceb7f11da9 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 29 Sep 2026 11:55:52 +0200 Subject: [PATCH] cibuild: the org model, which runs every check uncached (closes #37) script/cibuild was a plain docker build ., so on a tree Docker had seen before every check step came from the build cache and the build still passed. It is now the org model from sneak/prompts, byte for byte: script/bootstrap, script/check, then docker build --no-cache with the git describe version as the VERSION build argument. The workflow puts ~/.local/bin, where bootstrap links what it installs, on the step's PATH. Bootstrap now installs its pinned node when the installed one is older than 22.12.0, the oldest the frontend's dependencies accept (puppeteer-core's engines field), as it already does for Go against backend/go.mod. Model: opus-5-5 --- .gitea/workflows/check.yml | 6 ++++-- README.md | 15 ++++++++------- TODO.md | 8 ++++++++ script/bootstrap | 32 +++++++++++++++++++++++++------- script/cibuild | 26 ++++++++++++++++++++------ 5 files changed, 65 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 06b80af..08c2ebc 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -6,5 +6,7 @@ jobs: steps: # actions/checkout v4.2.2, 2026-02-22 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - # script/cibuild builds the image, whose stages run every check. - - run: script/cibuild + # script/cibuild bootstraps, runs every check and builds the + # image. script/bootstrap links what it installs into + # ~/.local/bin, so that has to be on PATH for the rest. + - run: PATH="$HOME/.local/bin:$PATH" script/cibuild diff --git a/README.md b/README.md index 415457e..c7c826a 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,12 @@ 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`, 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/bootstrap` — install all dependencies (the pinned node via nvm unless + one new enough for the frontend's dependencies is installed, 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) @@ -65,7 +65,8 @@ halves, so the root `make check` fails if either one is broken. We provide: `script/check`: it needs Docker and takes minutes. - `script/docker` — build the image from `Dockerfile` without the build cache, tagged `netwatch` via `script/projectname` -- `script/cibuild` — CI entrypoint: builds the image +- `script/cibuild` — CI entrypoint: runs `script/bootstrap` and `script/check`, + then builds the image as `script/docker` does, without the build cache - `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 212faab..1135681 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,14 @@ latest run passes. # Completed Steps +- 2026-09-29: CI can no longer pass on checks that did not run (issue #37): + `script/cibuild` is now the org model, byte for byte. It runs + `script/bootstrap` and `script/check`, then builds the image with `--no-cache` + and the version from `git describe` as the `VERSION` build argument, where it + used to be a plain `docker build .` whose check steps could come from the + build cache. The workflow puts `~/.local/bin`, where bootstrap links what it + installs, on the step's `PATH`, and bootstrap now installs its pinned node + when the installed one is older than the frontend's dependencies need - 2026-09-29: `backend/.golangci.yml` re-vendored from `sneak/prompts` (issue #41): `gomodguard`, deprecated in golangci-lint v2.12.0, is disabled and its successor `gomodguard_v2` enabled with the org block list, so lint runs print diff --git a/script/bootstrap b/script/bootstrap index 66e5873..d807ee3 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -3,12 +3,12 @@ # this repo. Idempotent: every install is guarded by a check so already # installed tools are skipped. Base tooling comes from nix, apt, brew, # 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). 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. +# used directly if it is at least NODE_MIN_VERSION; otherwise it is +# installed at a pinned version via nvm (installing nvm itself first, +# from a hash-verified 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 @@ -23,6 +23,10 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Pinned versions, 2026-07-07 NODE_VERSION="22.17.0" +# The oldest node the frontend's dependencies accept: the "engines" +# field of puppeteer-core 25.5.0, the most demanding of them, asks for +# 22.12.0 or newer, 2026-09-29. An older installed node is not used. +NODE_MIN_VERSION="22.12.0" NVM_VERSION="0.40.3" # sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0" @@ -136,8 +140,22 @@ ensure_nvm() { rm -rf "$tmp" } +# node_ok: the node on PATH is at least NODE_MIN_VERSION. node itself +# compares the two: major, then minor, then patch. +node_ok() { + if missing node; then return 1; fi + node -e ' + const have = process.versions.node.split(".").map(Number); + const want = process.argv[1].split(".").map(Number); + for (let i = 0; i < 3; i++) { + if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1); + } + ' "$NODE_MIN_VERSION" +} + +# ensure_node: unless node_ok, install NODE_VERSION and link its node. ensure_node() { - if ! missing node; then return 0; fi + if node_ok; then return 0; fi ensure_nvm nvm_sh "nvm install $NODE_VERSION" link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node diff --git a/script/cibuild b/script/cibuild index d860bf5..688299f 100755 --- a/script/cibuild +++ b/script/cibuild @@ -1,15 +1,29 @@ #!/bin/sh -# script/cibuild: run the CI build: build the one image from Dockerfile, -# whose stages run the checks as build steps (the backend's fmt-check, -# lint and tests, and the frontend's test, lint and fmt-check). This is -# the only build step the Gitea workflow runs. +# script/cibuild: run the CI build. It bootstraps first: a CI runner +# checks out and runs this and nothing else, and script/fmt-check runs +# the formatter on the host, which a pristine checkout cannot do. +# --no-cache for the same reason as script/docker: the gate phases the +# final stage depends on are RUN steps, and a cached one is a check that +# did not run. set -eu -ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" main() { cd "$ROOT" - timeout 300 docker build . + "$SCRIPT_DIR/bootstrap" + "$SCRIPT_DIR/check" + # Own line: a failing command substitution inside an argument does + # not trip `set -e`, so the inline form degrades silently to an + # empty constant. VERSION is computed here because .dockerignore + # excludes .git, so `git describe` in a build stage yields an empty + # version without failing. + version="$(git describe --tags --always --dirty 2>/dev/null || true)" + [ -n "$version" ] || version="unknown" + docker build --no-cache \ + --build-arg VERSION="$version" \ + -t "$("$SCRIPT_DIR/projectname")" . } main "$@"