ci: build the flake in a non-gating job (task 0041)
All checks were successful
CI / test (22) (pull_request) Successful in 48s
CI / test (true, 24) (pull_request) Successful in 1m5s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 49s
CI / test (true, 24) (push) Successful in 1m8s
CI / flake (push) Successful in 3s

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.
This commit was merged in pull request #50.
This commit is contained in:
2026-07-20 07:35:45 -04:00
parent ccc8dbe998
commit 710bbfdeac
3 changed files with 106 additions and 5 deletions

View File

@@ -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.<id>.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.

View File

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

View File

@@ -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.<id>.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.<id>.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.