From 710bbfdeac4770817287a0fd657f90a985b36cca Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 20 Jul 2026 07:35:45 -0400 Subject: [PATCH] ci: build the flake in a non-gating job (task 0041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A distinct `flake` job runs `nix flake check` on push and pull request, catching flake rot — most concretely a build-relevant file left out of package.nix's source allowlist — at the commit that causes it rather than weeks later at the maintainer's next system rebuild. The job is non-gating by two mechanisms: no `needs` edge, so it neither waits on the test job nor is waited on, and `continue-on-error` so a red build does not fail the run. That flag sits on the steps rather than the job because Gitea's `act` fork declares it on its Step struct only and silently ignores the job-level key; recorded as a Gotcha. --- .claude/tasks/0041-ci-flake-build-job.md | 59 ++++++++++++++++++++++-- .gitea/workflows/ci.yml | 46 +++++++++++++++++- CLAUDE.md | 6 +++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/.claude/tasks/0041-ci-flake-build-job.md b/.claude/tasks/0041-ci-flake-build-job.md index 1a2c404..5c4a2be 100644 --- a/.claude/tasks/0041-ci-flake-build-job.md +++ b/.claude/tasks/0041-ci-flake-build-job.md @@ -14,7 +14,58 @@ Its cost is honest and accepted: because the checks output aliases the package, ## Acceptance criteria -- [ ] A distinct job builds the flake on push and on pull request. -- [ ] Its failure does not block or fail the other jobs. -- [ ] Removing a build-relevant file from the source allowlist makes this job fail. -- [ ] The job's cost and its non-gating intent are stated in the workflow so neither reads as an oversight. +- [x] A distinct job builds the flake on push and on pull request. +- [x] Its failure does not block or fail the other jobs. +- [x] Removing a build-relevant file from the source allowlist makes this job fail. +- [x] The job's cost and its non-gating intent are stated in the workflow so neither reads as an oversight. + +## Implementation Notes + +The job sits alongside `test` in the existing workflow rather than in a file of its own, so it inherits the `on:` triggers already there — push to `main` and every pull request — with no second copy to keep in step. + +Non-gating is achieved two ways, and both are needed. +The absence of a `needs:` edge means the two jobs run concurrently and neither waits on the other, so a slow or failing flake build cannot hold the test job back. +`continue-on-error: true` then keeps a red flake job from failing the workflow run as a whole, which is the part that would otherwise block a merge. +That combination is what the criterion asks for; either alone leaves a gap. + +### `continue-on-error` goes on the steps, not the job + +The obvious spelling — `continue-on-error: true` as a job key — is inert on the platform this workflow primarily targets, and the first draft of this task had it there. + +Gitea Actions runs on a fork of `act`, and that fork's `pkg/model/workflow.go` declares `RawContinueOnError` on its **`Step`** struct only; the `Job` struct has no such field. +A job-level flag is therefore parsed as an unknown key and silently ignored, so a failing `nix flake check` would have failed the whole workflow run — exactly the merge-blocking outcome the criterion forbids, and with nothing in the logs to say why. +Gitea's own comparison page does not list the gap, which is presumably how it survives; go-gitea#25897 mentions it in passing while reporting the sibling gap in `jobs..if`. + +Moving the flag onto both steps fixes it and costs no portability: step-level `continue-on-error` is honoured by `act` and GitHub Actions alike, and a job whose every step carries it concludes green on either platform. +The install step carries it too, not just the build — an action that fails to fetch or install Nix is precisely the infrastructure failure the non-gating stance exists to absorb. + +### `nix flake check`, not `nix build` + +The two build the same derivation, since the `checks` output aliases the package. +The check output is what a consumer would verify with, though, so exercising that path additionally catches a `checks` output that has stopped evaluating. + +One limit worth recording: `nix flake check` builds the current system's outputs and merely *evaluates* the others, so the two systems the runner is not — `aarch64-linux` and `aarch64-darwin` — are type-checked rather than built. +`--all-systems` would not change that; building them needs runners of those architectures. +The job's value is the allowlist guard, which is architecture-independent, so this is a limit rather than a shortfall. + +### The allowlist criterion was verified, not assumed + +Deleting `./tsconfig.build.json` from `package.nix`'s `lib.fileset.unions` and re-running `nix flake check` locally fails the build in `buildPhase`: + +``` +> tsc -p tsconfig.build.json +error TS5058: The specified path does not exist: 'tsconfig.build.json'. +``` + +`package.nix` was restored immediately afterwards; the probe is not in the diff. +This is the failure mode the job exists to catch, and it confirms the loud-but-disconnected shape the 0037 Gotcha describes — the error names the missing file, never the allowlist that omitted it. + +### `cachix/install-nix-action` is a third-party action + +Nix is not in the runner image, so the job installs it. +That is the one dependency in this file on an action outside `actions/`, resolved from github.com by Gitea Actions the same way `actions/checkout` is. +It is also the most likely source of the infrastructure flakiness `continue-on-error` exists to absorb, which is part of why that flag is set rather than merely tolerated, and why the install step carries it as well as the build step. + +It is pinned to a mutable major tag (`@v31`), matching how `actions/checkout@v4` is pinned elsewhere in the file rather than introducing a second convention. +A commit SHA would be the supply-chain-tight choice; the exposure here is a non-gating job holding no credentials and no `secrets` access, and pinning one action by SHA while the rest of the file uses tags would be inconsistent without being materially safer. +Worth revisiting as a file-wide decision rather than a local one. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b95599a..535d572 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,7 +2,7 @@ # is kept GitHub-Actions-compatible so the GitHub mirror can adopt this file # nearly verbatim (copy it to .github/workflows/). # -# The job runs inside a node container so the disposable Gitea service is +# The `test` job runs inside a node container so the disposable Gitea service is # reachable by its service name (`gitea:3000`) on both Gitea Actions and GitHub # Actions — avoiding the host-vs-service-name networking difference between the # two platforms. @@ -82,3 +82,47 @@ jobs: - name: Packaging tier if: matrix.highest run: npm run test:pack + + # Builds the flake, catching flake rot — most concretely a build-relevant file + # left out of package.nix's source allowlist — at the commit that causes it, + # rather than weeks later at the maintainer's next system rebuild. + # + # `continue-on-error` is deliberate, not an oversight: this job is non-gating. + # Nix is not part of the runner image, so an infrastructure problem installing + # or reaching it must not block an otherwise legitimate change. Read its result + # as a signal, not as a verdict — a red mark here still merges. + # + # Its cost is likewise accepted rather than accidental. The flake's `checks` + # output aliases the package, so this builds the whole dependency closure from + # cold — nothing warms the store between runs — and re-runs the fast tier and + # the installed-binary tier inside the derivation, both of which the `test` job + # has already run. That duplication buys the allowlist guard, which nothing + # else provides. + flake: + runs-on: ubuntu-latest + # No `needs`: it neither waits on the test job nor is waited on, so the two + # run concurrently and neither can hold the other back. + # + # `continue-on-error` is set per step rather than on the job, which reads as + # the odd spelling but is the only one that works here: Gitea's `act` fork + # has the field on its Step struct and not on its Job struct, so a job-level + # flag is parsed and silently ignored, and a red build would fail the run + # after all. Step-level is honoured by both act and GitHub Actions, and a job + # whose every step is continue-on-error concludes green on either — so this + # spelling keeps the file portable as well as correct. + + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v31 + continue-on-error: true + with: + extra_nix_config: | + experimental-features = nix-command flakes + + # `checks` is the package, so this builds exactly what `nix build` does, + # entered through the output a consumer would verify with — which also + # catches a `checks` output that has stopped evaluating. + - name: Check the flake (builds the package) + continue-on-error: true + run: nix flake check --print-build-logs diff --git a/CLAUDE.md b/CLAUDE.md index 9a37727..1edf9fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,3 +65,9 @@ Left alone it ships a stray cache in the closure and makes the output non-reprod nixpkgs 26.11 (the `nixos-unstable` the flake tracks) has **dropped `x86_64-darwin`**. `legacyPackages.x86_64-darwin` now *throws* rather than merely failing to build, so listing that system in the flake's `systems` breaks `nix flake show` and `nix flake check` for every system at once, not just that one. Intel macOS would need the 26.05 branch. + +Gitea Actions ignores **job-level `continue-on-error`**. +Its `act` fork declares `RawContinueOnError` on the `Step` struct only — `pkg/model/workflow.go` has no such field on `Job` — so `jobs..continue-on-error` is parsed as an unknown key and silently dropped, and the job fails the run as if the flag were never written. +Gitea's own syntax-comparison page does not list the gap. +Put `continue-on-error` on each step instead: `act` and GitHub Actions both honour it there, and a job whose every step carries it concludes green on either platform. +The same fork historically ignored `jobs..if` (go-gitea#25897), so treat any job-level key as needing a check against the fork's structs rather than against GitHub's documentation. -- 2.47.3