Files
gitea-axi/.claude/tasks/0039-dev-shell-and-checks.md
alexion 6a9d4d4770
All checks were successful
CI / test (pull_request) Successful in 54s
CI / test (push) Successful in 53s
feat(nix): add a dev shell and a checks output (task 0039)
`nix develop` now yields the toolchain the repository actually needs — Node,
`git`, `tea`, and `curl` — giving a declarative answer to "what do I need to
work on this", which the repository previously specified nowhere.

The shell takes its Node from the package's `passthru` rather than naming
`pkgs.nodejs` a second time, so development and the shipped artifact cannot
drift onto different majors and cannot be set independently. `package.nix`
declares that `passthru` as an interface rather than leaving the shell to read
an incidental build attribute; it does not enter the derivation, so the store
path is unchanged.

The checks output aliases the package, so `nix flake check` builds it and
thereby runs both its verification phases instead of being a silent no-op. No
per-stage checks: the only stage adding coverage is the full typecheck, which
spans `test/` and `bench/` and would drag the benchmark harness into the
derivation's inputs, undoing the source filtering. It stays in CI.

The shell deliberately omits `gitea-axi` itself. The benchmark's arm resolves
that binary by name off PATH and must get the locally built `dist/main.js`, so
supplying the packaged one would silently substitute the wrong artifact.
2026-07-19 23:36:40 -04:00

4.1 KiB

spec, blocked-by
spec blocked-by
nix-flake-packaging 0037-flake-package-and-wrapper

What to build

Two further flake outputs, so nix develop gives a declarative answer to "what do I need to work on this" and nix flake check is not a silent no-op.

The development shell carries the toolchain the repository actually needs: Node, git, and tea — enough for the build, the live end-to-end tier, and the benchmark harness, all of which the repository documents while specifying no toolchain anywhere. It references the same Node attribute as the package, so development and the shipped artifact cannot drift onto different major versions.

The checks output aliases the package, so the conventional health-check command builds it and thereby runs both its verification phases. Granular per-stage checks are deliberately not added: the one stage that would add real coverage is the full typecheck, which spans the test and benchmark directories and would therefore drag the benchmark harness into the derivation's inputs, undoing the source filtering. The full typecheck stays in continuous integration, where it already runs.

Acceptance criteria

  • nix develop yields a shell with Node, git, and tea available.
  • The build, the fast tier, and the benchmark harness's runner all work from inside that shell.
  • The shell's Node and the package's Node come from one reference — changing it moves both, and they cannot be set independently.
  • nix flake check builds the package and runs its tests, and fails when the package fails.
  • No per-stage check derivations are added.

Implementation Notes

The single Node reference is a passthru, not a second pkgs.nodejs

The shell takes self.packages.${system}.gitea-axi.nodejs rather than naming pkgs.nodejs again. Naming it twice would satisfy the criterion's letter while leaving two places to edit, which is the drift the criterion exists to prevent; reading it back off the derivation means there is genuinely one reference.

The first cut relied on buildNpmPackage incidentally surfacing its nodejs argument as a derivation attribute — which works, but only as a side effect of inherit src nodejs, with nothing marking it load-bearing. Review caught that: moving or dropping that inherit would have broken the shell silently at a distance. package.nix now declares passthru = { inherit nodejs; };, making it an interface with a comment saying what depends on it. passthru does not enter the derivation, so the store path is unchanged and the change costs no rebuild — verified: the drv hash before and after is identical.

curl was a missing part of the toolchain

The benchmark's raw-api arm shells out to curl (ARM_BINARY in bench/guard.ts), so the shell carries it alongside Node, git, and tea.

The shell deliberately does not supply gitea-axi

The criterion asks that the benchmark harness's runner work from inside the shell, and it does. A live arm run is a further step the shell cannot take: provisionArmBin resolves each arm's binary by name off PATH, and the gitea-axi arm's binary must be the locally built dist/main.js so a run measures the working tree rather than whatever the flake last packaged. Putting the packaged binary on PATH would satisfy the lookup with the wrong artifact — a silently misleading benchmark, worse than a missing one.

Exposing the built one was considered and rejected as well: tsc does not set an executable bit on dist/main.js (npm sets it at install time from the manifest's bin entry, which is what the packaging tier's bit assertion guards), so a shellHook would have had to chmod +x the build output on every shell entry — mutating build artifacts to work around a lookup that is the benchmark's own concern. Both the flake and the CLAUDE.md gotcha now state the boundary rather than implying the shell covers it.

CLAUDE.md's toolchain gotcha was stale by construction

It read "the repository has no dev shell yet (task 0039 adds one)". This slice is that task, so the entry was rewritten to point at nix develop --command, keeping nix shell nixpkgs#nodejs -c ... only as the one-off outside the repository.