ci: split CI — fast native gate every PR, hermetic nix flake check only on nix/dep changes #32
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#32
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Request
Split CI so the common path is fast. Today
.forgejo/workflows/ci.ymlruns a singleflake-checkjob on every PR (install Determinate nix →nix flake check -L) at a measured ~12–13 min/run. Keep a fast native gate (Go + Node directly, withsetup-*caching) on every PR, and run the hermeticnix flake checkgate only when nix or Go-dependency files change.Why (measured)
From the Forgejo Actions API (last ~10 runs):
flake-check(PR gate): 12m18s / 12m35s / 13m05s / 12m25s; 16m35s on a failure.bump-nixos-pin(deploy, same Determinate installer): ~2m — the natural control (it early-exits beforetest-configuration).So the installer is only ~1.5–2 min; the other ~10 min is
nix flake checkrealizing the whole closure stone-cold every run — the Docker-backed runner is ephemeral,/nixis wiped per job, so the Go toolchain + node + golangci-lint closures (hundreds of MB from cache.nixos.org), the ~160 MB npm deps, and the Go vendor modules are re-fetched and rebuilt every time. Merely "putting nix in an image" would save only the ~2 min install, not the ~10 min cold store — hence: skip nix entirely on the common path.Current behavior
.forgejo/workflows/ci.yml:on: pull_request, one jobflake-checkonubuntu-latest, curl Determinate installer (--init none) →nix flake check -L.checks(flake.nixL58–154):lab(Go build + fullgo testwith real git/tmux/util-linux innativeCheckInputs, built-tags uiagainst the copied SPA dist),labctl(build only),web(npm run lint+format:check+npm test/vitest +vite build),golangci-lint(whole-program, untagged,CGO_ENABLED=0),nixos-module(eval-only — greps the systemd unit text).flake-checkstatus.Design decisions (binding)
1. Two workflows.
ci.yml→ fast native gate.on: pull_request(no path filter). Always runs. Becomes the required status check. Give the job a stable id/name (e.g.native) so the required-check string is deterministic.ci-nix.yml→ the existingnix flake check -L(same in-job Determinate install).on: pull_requestwith thepaths:filter below.2. Path filter for the nix gate. Run the nix job only when these change:
Rationale: the only intrinsically-nix check is
nixos-module(depends onnix/module.nix). The critical entries arego.mod/go.sum—nix/package.nixpinsvendorHash(a fixed-output hash over the vendored Go module set). A dependency change makes that hash stale →nix flake checkfails, but the native gate (go mod downloadlive) passes green — a dep bump would merge clean and rot the nix build for the next nix-touching PR (and the deploy). Gating the nix job ongo.mod/go.sumcloses that trap.web/package-lock.jsonis deliberately not a trigger:importNpmLockreads the lockfile directly (nonpmDepsHash), so npm deps never rot the nix build (per thenix/package.nixcomment); its vitest/build coverage is already in the native gate.3. The native gate mirrors the flake's non-nix checks, version-matched to the flake.
go.modgate) viaactions/setup-gowith module + build cache.actions/setup-nodewith npm cache; pin the major to match the flake'snodejs.flake.lock(e.g.nix eval nixpkgs#golangci-lint.versionat revd407951…), rungolangci-lint run ./...untagged,CGO_ENABLED=0(matches the flake's golangci-lint check). Drift between pin bumps is acceptable — the full untagged lint re-runs in the nix gate wheneverflake.lockchanges.git,tmux,prlimit(util-linux) are on PATH forgo test(the D17 real-subprocess bar) — installtmuxif the base image lacks it.make+ local dev runs, without nix"):cd web && npm ci && npm run lint && npm run format:check && npm test && npm run build→ copyweb/dist→internal/webui/dist(go:embed target, same asmake build-ui) →go build -tags ui ./cmd/...→go test -tags ui ./...(needs the dist for theui-tagged embed; mirrors the flake's tagged checkPhase) →golangci-lint run ./...(untagged). KeepCGO_ENABLED=0throughout.setup-*actions must resolve on the runner (same actions source as the existingactions/checkout@v4). If they don't, the fallback is acontainer:image carrying Go 1.26 + Node + golangci-lint + tmux — but prefersetup-*+ caching (no image to maintain).4. Required-status-check switch (must get right). After the split, the native
ci.ymljob is the required check; the nix job reports a status only when its paths match, so on a Go/TS-only PR it won't run. A forge that "expects" a never-reporting required check can wedge the PR. Therefore the maintainer must switch branch protection's required check fromflake-checkto the native job's name — a repo setting the agent cannot change; call it out explicitly in the PR description. Keep the native job name stable for that reason.5. ADR-0023. Write
docs/adr/0023-ci-split-native-and-nix-gates.mdrecording the trade: this relaxes ADR-0002's "CI == a singlenix flake check, local green == CI green" on the common path — native-green no longer guarantees nix-green for non-dependency reasons (sandbox behavior, theui-embed placeholder compile, checks that exist only in the flake). The hermetic gate stays authoritative and still runs on every nix/dep change and as the merge-to-main backstop (bump-nixos-pin→test-configurationrebuildspackages.lab). Updatedocs/ops.md"CI runner prerequisites" to describe the two gates.6. Scope. Pure CI + docs (+ ADR). No change to
flake.nix,nix/**, product source (Go/TS), ordeploy.yml. Leave thestore-postgrescommented template where it is.Acceptance criteria
-tags ui, runsgo test -tags ui ./...andgolangci-lint, all green; the nix workflow is skipped.**/*.nix/flake.lock/go.mod/go.sum: thenix flake check -Lgate additionally runs and is green.git/tmux/prlimitpresent forgo test.docs/ops.mdCI section updated.flake.nix,nix/**, Go/TS source, ordeploy.yml.Agent brief follows as a comment.
Agent Brief
Category: enhancement (CI / build infra)
Summary: Split the single
nix flake checkPR gate into (a) a fast native Go+Node gate that runs on every PR (the required check) and (b) anix flake checkgate path-gated to**/*.nix,flake.lock,go.mod,go.sum. Cuts the common-path PR from ~12–13 min to ~2–4 min. Write ADR-0023 relaxing ADR-0002 for the common path; flag the branch-protection required-check switch for the maintainer.Files
.forgejo/workflows/ci.yml— replace theflake-checkjob with the native gate (stable job id, e.g.native)..forgejo/workflows/ci-nix.yml— new; the existingnix flake check -L+ in-job Determinate install,on: pull_requestwith thepaths:filter.docs/adr/0023-ci-split-native-and-nix-gates.md— new ADR (relaxes ADR-0002 on the common path; hermetic gate authoritative on nix/dep changes +bump-nixos-pinbackstop).docs/ops.md— "CI runner prerequisites" → describe the two gates.Watch out for
go.mod/go.summust be in the nixpaths:filter (decision 2). This is the one correctness trap: without it, a dependency bump goes green on the native gate and rots the nix build.go testmust be-tags uiand run after the SPA dist is built and copied tointernal/webui/dist, else theui-taggedgo:embedwon't compile. golangci-lint runs untagged (the placeholder embed variant),CGO_ENABLED=0— same as the flake.flake.lock;git/tmux/prlimit(util-linux) on PATH forgo test.flake-check→nativeswitch in the PR description.setup-go/setup-nodemust resolve on the forgejo-runner (same actions source ascheckout@v4); if not, fall back to acontainer:with the toolchain baked in.Verify: open the PR with a Go/TS-only change → confirm only
nativeruns and is green, nix workflow skipped; then push ago.modorflake.locktouch on a scratch branch → confirm the nix gate fires and is green. (Or reason it from thepaths:filter if a live push isn't practical.)Out of scope: warming the nix store / persistent runner / binary cache (tracked in #33); any change to
flake.nix,nix/**, product source, ordeploy.yml.