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 "$@"