feat(nix): package gitea-axi as a flake with a PATH-suffixing wrapper (task 0037)
All checks were successful
CI / test (pull_request) Successful in 54s
CI / test (push) Successful in 52s

Add a flake at the repository root exposing gitea-axi as a package, with the
derivation in its own callable expression so it stays buildable outside a flake
context and usable in an overlay unchanged.

Dependencies are fetched from the lockfile's integrity fields via importNpmLock
rather than a committed fixed-output hash, and the version is read from the
package manifest, so neither a dependency bump nor a release edits any Nix
expression. The source is an explicit allowlist, keeping ADR, spec, task and
bench churn out of the derivation's inputs.

The installed binary is wrapped with --suffix PATH per ADR 0018: the operator's
own git and tea win, and the closure's are a fresh-machine fallback.

Two things differ from the plan. Systems coverage is three targets, not four:
nixpkgs 26.11 dropped x86_64-darwin and now throws on evaluating it, which
would break nix flake show and nix flake check for every system at once. And
buildNpmPackage supplies no check hook, so doCheck alone was silently inert and
produced a green build whose tests never ran; running the fast tier needs an
explicit checkPhase.
This commit was merged in pull request #46.
This commit is contained in:
2026-07-19 23:00:48 -04:00
parent 488058630b
commit 5a2874d11c
6 changed files with 285 additions and 11 deletions

119
package.nix Normal file
View File

@@ -0,0 +1,119 @@
{
lib,
buildNpmPackage,
importNpmLock,
makeWrapper,
nodejs,
git,
tea,
which,
}:
let
# The manifest is the canonical version: the release flow bumps it, and
# reading it here means a store path and a released version cannot disagree.
manifest = lib.importJSON ./package.json;
# An explicit allowlist of what the build and its tests actually read. The
# repository's highest-churn directories — .claude, bench, prose docs — are
# all build-irrelevant, so a whole-repository source would let writing an ADR
# invalidate the derivation and force a rebuild with a full test run.
#
# Adding a build-relevant top-level file means adding it here too; the build
# otherwise fails on a missing file.
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./src
# The fast tier only. `test/e2e` needs a live Gitea host and is excluded
# from the runner config, so admitting it would let end-to-end churn
# invalidate the derivation — the very cost this allowlist exists to
# avoid. `test/packaging` stays: task 0038 drives it against the
# installed binary.
(lib.fileset.difference ./test ./test/e2e)
./skills
./package.json
./package-lock.json
./tsconfig.json
./tsconfig.build.json
./vitest.config.ts
];
};
in
buildNpmPackage {
pname = "gitea-axi";
inherit (manifest) version;
inherit src nodejs;
# Each dependency's fetch is derived from the integrity fields already in the
# lockfile, so a lockfile change needs no edit here. A single fixed-output
# hash would break on every dependency bump and be repaired by copying a hash
# out of an error message — a permanent recurring tax.
npmDeps = importNpmLock { npmRoot = src; };
inherit (importNpmLock) npmConfigHook;
nativeBuildInputs = [ makeWrapper ];
# The builder would otherwise unpack to a generic `source` directory, which no
# real installation resembles: under npm the tree lives at
# `node_modules/gitea-axi`, under Nix at `…-gitea-axi-<version>/…`. The fast
# tier's `setup hooks` test is sensitive to the difference, because the SDK
# records the entrypoint's absolute path and recognises its own managed hook by
# finding "gitea-axi" within it. Naming the tree makes the build representative
# rather than an environment no operator ever has.
#
# This coupling is a defect, not a property worth preserving — see task 0042,
# which removes the hook's dependence on the entrypoint path entirely. Once it
# lands this rename should go with it.
postUnpack = ''
mv "$sourceRoot" gitea-axi
export sourceRoot=gitea-axi
'';
# The fast tier only. The live end-to-end and benchmark smoke tiers need a
# live Gitea host. Two of these test files invoke `git` directly and one
# resolves it with `which`; `tea` is already stubbed within this tier.
doCheck = true;
nativeCheckInputs = [
git
which
];
# `buildNpmPackage` wires config, build and install hooks but no check hook, so
# `doCheck` alone is inert and the phase has to be spelled out. `git init` and
# `git commit` in the fast tier also need a writable HOME, which the sandbox
# otherwise points at a non-existent directory.
checkPhase = ''
runHook preCheck
export HOME=$(mktemp -d)
npm run test
runHook postCheck
'';
# ADR 0018: append, never prepend. The operator's own `tea` owns the
# credential store it refreshes in place, so the closure's copy is a
# fresh-machine fallback rather than an override.
postInstall = ''
wrapProgram $out/bin/gitea-axi \
--suffix PATH : ${lib.makeBinPath [ git tea ]}
'';
meta = {
inherit (manifest) description homepage;
# Looked up by SPDX identifier rather than hardcoded, for the same reason
# the version is read from the manifest: one canonical source, no second
# place to update on a relicence.
license = lib.licensesSpdx.${manifest.license};
mainProgram = "gitea-axi";
# Broader than the flake's `systems` list, deliberately. This describes what
# the package supports — everything, since it contains no compiled code —
# whereas that list encodes which systems the pinned nixpkgs can still
# evaluate. Consumed against 26.05, x86_64-darwin builds fine from here.
platforms = lib.platforms.linux ++ lib.platforms.darwin;
};
}