All checks were successful
CI / test (22) (pull_request) Successful in 49s
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 1m5s
CI / flake (push) Successful in 3s
Let a Nix configuration declare gitea-axi's ambient context instead of running a command that writes it. `setup` and `setup hooks` are write-only against files the operator is assumed to own, so an operator whose agent configuration is generated cannot use them at all. The package installs the bundled Agent Skill to share/gitea-axi/skills and publishes it as `passthru.skill`, alongside `passthru.sessionStartHook` read from `session-start-hook.json` — a committed declaration the fast tier reads too, so a test drives `setup hooks` and asserts the two agree. On top of that, `homeModules.gitea-axi` declares both from those attributes through home-manager's own Claude Code options, so an operator's existing skills and SessionStart hooks compose rather than collide. Importing it without enabling it yields a byte-identical generation. The spec's Out of Scope entry deferring a home-manager module is deleted; ADR 0020 records the reversal, and INSTALL.md describes both paths.
211 lines
9.0 KiB
Nix
211 lines
9.0 KiB
Nix
{
|
|
lib,
|
|
buildNpmPackage,
|
|
importNpmLock,
|
|
makeWrapper,
|
|
nodejs,
|
|
git,
|
|
tea,
|
|
which,
|
|
}:
|
|
|
|
let
|
|
# Where the install check finds the dev dependencies that `npmInstallHook`
|
|
# prunes out of the build tree. Named once: it is a contract between
|
|
# `preInstall`, which writes it, and `installCheckPhase`, which reads it.
|
|
devNodeModules = "$NIX_BUILD_TOP/node_modules-dev";
|
|
|
|
# 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;
|
|
|
|
# Where the bundled Agent Skill lands in the output, and the one address a
|
|
# consumer may depend on. The Skill's other copy — inside the installed node
|
|
# modules tree, where `setup` resolves it relative to its own module — is an
|
|
# artefact of how the command finds it at runtime, and moves whenever the
|
|
# packaging method changes.
|
|
skillSubdir = "share/gitea-axi/skills/gitea-axi";
|
|
|
|
# The SessionStart hook entry, read from the committed specification rather
|
|
# than written out here. The imperative `setup hooks` writes this same entry
|
|
# through the agent SDK, and a test drives it and asserts the two agree — so
|
|
# declaring it a second time in Nix would be a second source of truth with
|
|
# nothing checking it against the first.
|
|
sessionStartHook = lib.importJSON ./session-start-hook.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
|
|
./vitest.packaging.config.ts
|
|
# Read by the fast tier, which asserts the imperative hook install writes
|
|
# what this declares. Also read at evaluation time above, but that read is
|
|
# of the flake source rather than of `src` and would not require it here.
|
|
./session-start-hook.json
|
|
];
|
|
};
|
|
in
|
|
buildNpmPackage (finalAttrs: {
|
|
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 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
|
|
|
|
# vitest leaves a run cache under node_modules/.vite whose results.json
|
|
# records durations and timestamps. `npmInstallHook` copies node_modules
|
|
# into $out wholesale, so leaving it there both ships a stray cache in the
|
|
# closure and makes the output non-reproducible — `nix build --rebuild`
|
|
# reports the derivation "may not be deterministic" on that one file.
|
|
rm -rf node_modules/.vite
|
|
|
|
runHook postCheck
|
|
'';
|
|
|
|
# `npmInstallHook` prunes dev dependencies out of the build tree's
|
|
# node_modules on its way to assembling $out, which would take vitest with it
|
|
# and leave the install check with nothing to run. Snapshot the tree first —
|
|
# as hardlinks, so it costs neither time nor space, and so the prune's
|
|
# deletions do not follow through to the copy.
|
|
preInstall = ''
|
|
cp -al node_modules "${devNodeModules}"
|
|
'';
|
|
|
|
# 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.
|
|
#
|
|
# The Agent Skill is also published under a stable address (ADR 0020), so a
|
|
# Nix expression can install it declaratively without reaching into the node
|
|
# modules tree. `cp` failing on a missing source is the guard that this
|
|
# address keeps pointing at something.
|
|
postInstall = ''
|
|
wrapProgram $out/bin/gitea-axi \
|
|
--suffix PATH : ${lib.makeBinPath [ git tea ]}
|
|
|
|
mkdir -p "$(dirname "$out/${skillSubdir}")"
|
|
cp -R skills/gitea-axi "$out/${skillSubdir}"
|
|
'';
|
|
|
|
# Drive the binary that was just installed through the shared installed-binary
|
|
# tier, which the npm distribution path drives too — so the two cannot drift
|
|
# apart in what they guarantee about an installed gitea-axi.
|
|
#
|
|
# This guards a class of failure `checkPhase` structurally cannot reach,
|
|
# because it runs against the source tree rather than an installation. The
|
|
# one that bites here is Skill resolution: `setup` locates the bundled Agent
|
|
# Skill relative to its own module location, so the built output's position
|
|
# relative to that Skill is load-bearing — an arrangement that exists only
|
|
# once installed. A probe moving the installed `skills` aside does fail this
|
|
# phase.
|
|
#
|
|
# The tier's executable-bit assertion carries less weight under Nix than
|
|
# under npm, and deliberately so: `nodejsInstallExecutables` generates a
|
|
# wrapper invoking `node <path>` rather than symlinking the entrypoint, so
|
|
# the bit that matters is the one on `$out/bin/gitea-axi`, which makeWrapper
|
|
# always sets. That assertion earns its keep on the npm path, where npm sets
|
|
# the bit from the manifest's `bin` entry and `tsc` does not. Sharing one
|
|
# tier means neither path picks which guarantees it feels like offering.
|
|
#
|
|
# `installCheckPhase` runs after `fixupPhase`, so the binary named here is the
|
|
# wrapped one an operator would actually get. Naming it is all this phase
|
|
# does: the assertions live in the tier, not in shell script here.
|
|
doInstallCheck = true;
|
|
installCheckPhase = ''
|
|
runHook preInstallCheck
|
|
|
|
# Restore by copying, not moving, so the snapshot survives for a replayed
|
|
# phase — `--keep-failed` debugging, or `genericBuild` re-entered by hand.
|
|
rm -rf node_modules
|
|
cp -al "${devNodeModules}" node_modules
|
|
|
|
export HOME=$(mktemp -d)
|
|
GITEA_AXI_INSTALLED_BIN=$out/bin/gitea-axi npm run test:installed
|
|
|
|
runHook postInstallCheck
|
|
'';
|
|
|
|
# The package's declared interface to Nix expressions, published rather than
|
|
# left to be read off the build environment or guessed at from the output's
|
|
# layout. Every attribute here has a consumer that breaks if it is removed.
|
|
passthru = {
|
|
# The Node the package is built against. The flake's dev shell consumes
|
|
# exactly this, so development and the shipped artifact cannot drift onto
|
|
# different majors.
|
|
inherit nodejs;
|
|
|
|
# The bundled Agent Skill's directory, for a configuration that installs it
|
|
# declaratively. A directory rather than the SKILL.md inside it, so a Skill
|
|
# that grows helper files stays one reference.
|
|
skill = "${finalAttrs.finalPackage}/${skillSubdir}";
|
|
|
|
# The SessionStart hook entry, verbatim as it belongs in a Claude Code
|
|
# settings.json. Evaluating this builds nothing: it is the committed
|
|
# specification, and the command it records is a name resolved on PATH
|
|
# rather than a store path (ADR 0019).
|
|
inherit sessionStartHook;
|
|
};
|
|
|
|
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;
|
|
};
|
|
})
|