ci: split CI — fast native gate every PR, hermetic nix flake check only on nix/dep changes #36

Merged
dominik.polakovics merged 1 commit from afk/32 into main 2026-07-08 00:54:56 +02:00

What

Splits the single nix flake check PR gate — ~12–13 min, the whole nix closure realized stone-cold on the ephemeral runner every run — into two gates so the common path skips nix entirely:

  • .forgejo/workflows/ci.yml → native gate (job native). Runs on every PR; the intended required check. Go + Node directly on the stock runner with actions/setup-go@v5 / actions/setup-node@v4 caching — no nix. Mirrors the flake's non-nix checks: SPA eslint + prettier + vitest + vite build → copy web/distinternal/webui/distgo build -tags ui ./cmd/...go test -tags ui ./...golangci-lint run ./... (untagged), CGO_ENABLED=0 throughout, with git/tmux/prlimit on PATH. ~2–4 min.
  • .forgejo/workflows/ci-nix.yml → hermetic gate (job flake-check). The existing nix flake check -L + in-job Determinate install, on: pull_request with paths: = **/*.nix, flake.lock, go.mod, go.sum. Stays authoritative; the deploy (bump-nixos-pintest-configuration) re-runs it as a merge-to-main backstop.

Version pins match nixpkgs at the current flake.lock (d407951): Go 1.26 (from go.mod), Node 24 (pkgs.nodejs), golangci-lint 2.12.2 (nix eval nixpkgs#golangci-lint.version).

Why go.mod/go.sum gate the nix job (the one correctness trap)

nix/package.nix pins vendorHash — a fixed-output hash over the vendored Go module set. A dependency bump stales that hash → nix flake check fails, but the native gate (go mod download live) passes green. Without gating the nix job on go.mod/go.sum, a dep bump would merge clean on the native gate and rot the nix build for the next nix-touching PR (and the deploy). web/package-lock.json is deliberately not a trigger — importNpmLock reads the lockfile directly (no npmDepsHash), so npm deps can't rot the nix build, and its vitest/build coverage is already in the native gate.

⚠️ Maintainer action required — branch-protection required-check switch

After merge, move branch protection's required status check from flake-check to native. The nix gate reports a status only when its paths: match, so on a Go/TS-only PR it never runs; a rule that still requires flake-check would wedge every such PR at "expected". The native job id is kept stable for exactly this reason. (This is a repo setting the agent cannot change — flagged per the issue's decision 4.)

Docs

  • docs/adr/0023-ci-split-native-and-nix-gates.md — records the trade; relaxes ADR-0002 on the common path (native-green no longer guarantees nix-green for non-dependency reasons: sandbox behavior, the ui-embed compile, flake-only checks), hermetic gate authoritative on nix/dep changes + the deploy backstop.
  • docs/ops.md — "CI runner prerequisites" now describes both gates (native egress, the actions-cache backend requirement, tmux install; nix egress unchanged).

Scope

Pure CI + docs. No diff to flake.nix, nix/**, product source (Go/TS), or deploy.yml. The store-postgres M2 template stays commented in ci.yml.

Two out-of-scope staleness notes for you to reconcile (deliberately not touched, since both files are outside this issue's file list):

  • deploy.yml's header comment now misdescribes reality — it says the flake-check gate "lives in ci.yml on pull_request" (it's now ci-nix.yml) and that branch protection requires the "flake-check status" (it'll be native). Comment-only; deploy behavior is unchanged and correct. Natural to fix alongside the required-check switch above.
  • docs/definition-of-done.md §9 still says "CI is exactly nix flake check". Now imprecise on the common path.

Verification

nix isn't available in this environment, so the gate behavior is reasoned from the paths: filter rather than a live push (per the issue's fallback): a Go/TS-only PR matches no ci-nix.yml path → only native runs; a PR touching **/*.nix / flake.lock / go.mod / go.sum additionally fires flake-check. The version pins were verified against nixpkgs d407951 (golangci-lint 2.12.2, pkgs.nodejs24, both cross-checked), and the forgejo-runner facts — setup-go@v5/setup-node@v4 resolving like the existing checkout@v4 (pin by tag, not SHA), tmux absent from the image (apt-installed) but prlimit present, setup-go caching by default vs setup-node needing cache: npm — confirmed against Forgejo docs and the catthehacker image scripts. A multi-dimension adversarial review of the diff against the acceptance criteria returned zero confirmed defects.

Closes #32

🤖 Generated with Claude Code

https://claude.ai/code/session_01TdXvXtsF3jTnSvyu5wH9it

## What Splits the single `nix flake check` PR gate — ~12–13 min, the whole nix closure realized stone-cold on the ephemeral runner every run — into two gates so the common path skips nix entirely: - **`.forgejo/workflows/ci.yml` → native gate (job `native`).** Runs on **every** PR; the intended **required** check. Go + Node directly on the stock runner with `actions/setup-go@v5` / `actions/setup-node@v4` caching — no nix. Mirrors the flake's non-nix checks: SPA `eslint` + `prettier` + `vitest` + `vite build` → copy `web/dist` → `internal/webui/dist` → `go build -tags ui ./cmd/...` → `go test -tags ui ./...` → `golangci-lint run ./...` (untagged), `CGO_ENABLED=0` throughout, with `git`/`tmux`/`prlimit` on PATH. ~2–4 min. - **`.forgejo/workflows/ci-nix.yml` → hermetic gate (job `flake-check`).** The existing `nix flake check -L` + in-job Determinate install, `on: pull_request` with `paths:` = `**/*.nix`, `flake.lock`, `go.mod`, `go.sum`. Stays authoritative; the deploy (`bump-nixos-pin` → `test-configuration`) re-runs it as a merge-to-main backstop. Version pins match nixpkgs at the current `flake.lock` (`d407951`): **Go 1.26** (from `go.mod`), **Node 24** (`pkgs.nodejs`), **golangci-lint 2.12.2** (`nix eval nixpkgs#golangci-lint.version`). ## Why `go.mod`/`go.sum` gate the nix job (the one correctness trap) `nix/package.nix` pins `vendorHash` — a fixed-output hash over the vendored Go module set. A dependency bump stales that hash → `nix flake check` fails, **but the native gate (`go mod download` live) passes green.** Without gating the nix job on `go.mod`/`go.sum`, a dep bump would merge clean on the native gate and rot the nix build for the next nix-touching PR (and the deploy). `web/package-lock.json` is deliberately **not** a trigger — `importNpmLock` reads the lockfile directly (no `npmDepsHash`), so npm deps can't rot the nix build, and its vitest/build coverage is already in the native gate. ## ⚠️ Maintainer action required — branch-protection required-check switch After merge, **move branch protection's required status check from `flake-check` to `native`.** The nix gate reports a status only when its `paths:` match, so on a Go/TS-only PR it never runs; a rule that still *requires* `flake-check` would wedge every such PR at "expected". The `native` job id is kept stable for exactly this reason. (This is a repo setting the agent cannot change — flagged per the issue's decision 4.) ## Docs - `docs/adr/0023-ci-split-native-and-nix-gates.md` — records the trade; **relaxes ADR-0002** on the common path (native-green no longer *guarantees* nix-green for non-dependency reasons: sandbox behavior, the `ui`-embed compile, flake-only checks), hermetic gate authoritative on nix/dep changes + the deploy backstop. - `docs/ops.md` — "CI runner prerequisites" now describes both gates (native egress, the actions-cache backend requirement, tmux install; nix egress unchanged). ## Scope Pure CI + docs. **No diff** to `flake.nix`, `nix/**`, product source (Go/TS), or `deploy.yml`. The `store-postgres` M2 template stays commented in `ci.yml`. **Two out-of-scope staleness notes for you to reconcile (deliberately not touched, since both files are outside this issue's file list):** - `deploy.yml`'s header comment now misdescribes reality — it says the flake-check gate "lives in `ci.yml` on pull_request" (it's now `ci-nix.yml`) and that branch protection requires the "`flake-check` status" (it'll be `native`). Comment-only; deploy behavior is unchanged and correct. Natural to fix alongside the required-check switch above. - `docs/definition-of-done.md` §9 still says "CI is exactly `nix flake check`". Now imprecise on the common path. ## Verification `nix` isn't available in this environment, so the gate behavior is reasoned from the `paths:` filter rather than a live push (per the issue's fallback): a Go/TS-only PR matches no `ci-nix.yml` path → only `native` runs; a PR touching `**/*.nix` / `flake.lock` / `go.mod` / `go.sum` additionally fires `flake-check`. The version pins were verified against nixpkgs `d407951` (golangci-lint `2.12.2`, `pkgs.nodejs` → `24`, both cross-checked), and the forgejo-runner facts — `setup-go@v5`/`setup-node@v4` resolving like the existing `checkout@v4` (pin by tag, not SHA), `tmux` absent from the image (apt-installed) but `prlimit` present, setup-go caching by default vs setup-node needing `cache: npm` — confirmed against Forgejo docs and the catthehacker image scripts. A multi-dimension adversarial review of the diff against the acceptance criteria returned zero confirmed defects. Closes #32 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TdXvXtsF3jTnSvyu5wH9it
ci: split CI into a fast native gate and a path-gated nix flake check
All checks were successful
ci / native (pull_request) Successful in 6m42s
26c957258f
Every PR ran a single `flake-check` job (~12-13 min) that realized the whole
nix closure stone-cold on the ephemeral runner; only the ~2 min installer was
overhead, the other ~10 min was `nix flake check` rebuilding the Go/node/
golangci-lint closures and re-fetching deps cold every run. Split the gate so
the common path skips nix entirely:

- ci.yml -> a native Go+Node gate (job `native`), on every PR, the required
  check. Mirrors the flake's non-nix checks with setup-go/setup-node caching:
  SPA eslint + prettier + vitest + `vite build`, copy web/dist to
  internal/webui/dist, `go build`/`go test` -tags ui against that dist, then
  untagged golangci-lint, CGO_ENABLED=0 throughout, with git/tmux/prlimit on
  PATH. ~2-4 min.
- ci-nix.yml -> the existing `nix flake check -L` + in-job Determinate install,
  path-gated to **/*.nix, flake.lock, go.mod, go.sum. go.mod/go.sum are
  load-bearing: a dependency bump stales nix/package.nix's vendorHash, which the
  native gate (live `go mod download`) cannot catch, so without gating the nix
  job on them a dep bump would merge clean and rot the nix build. package-lock
  .json is deliberately not a trigger (importNpmLock, no npmDepsHash).

Pins match nixpkgs at the current flake.lock (d407951): Go 1.26 (from go.mod),
Node 24 (pkgs.nodejs), golangci-lint 2.12.2.

Docs: ADR-0023 records the trade -- relaxes ADR-0002 on the common path
(native-green no longer guarantees nix-green for non-dependency reasons), the
hermetic gate stays authoritative on nix/dep changes and as the bump-nixos-pin
deploy backstop; docs/ops.md "CI runner prerequisites" now describes both gates.

Requires a repo-setting change this commit cannot make: branch protection's
required status check must move from `flake-check` to `native`.

Closes #32

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdXvXtsF3jTnSvyu5wH9it
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!36
No description provided.