All checks were successful
CI / test (22) (pull_request) Successful in 47s
CI / test (true, 24) (pull_request) Successful in 1m6s
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 2s
The SDK records a bare, upgrade-stable binary name only when a PATH entry realpath-matches the execPath it is handed. From the module-relative entrypoint that can only succeed under npm, which symlinks its bin entry straight at it; a wrapper-based install never can, because a script that invokes a file does not resolve to that file. So every wrapper install -- Nix, a shim, a generated .cmd -- recorded an absolute path that moves on upgrade, and a session-start hook that cannot execute fails silently. `setup hooks` now resolves gitea-axi on PATH itself and hands that location to the SDK, so the bare name is recorded. A candidate qualifies only if it resolves to the running entrypoint -- by realpath for a symlink, or by naming it in its text for a wrapper, following the chain, since a Nix install is two hops. A same-named binary that is some other program does not qualify, and the absolute entrypoint path stands as the fallback exactly as before. The related defect on the same line goes too: the SDK recognises its hook by finding the marker inside the recorded command, so an entrypoint path without "gitea-axi" in it made a re-run append a duplicate rather than update in place. `setup hooks` now prunes duplicates by matching the exact command it records, which is independent of that command's shape and cannot mistake another tool's hook for its own. With the coupling gone, package.nix no longer renames its build tree; the fast tier runs from /build/source and its idempotency test passes there. The help text's instruction to re-run hooks after an upgrade is deleted, having become false. Verified against the built Nix binary and a globally npm-installed pack: both record the bare name, both fall back to the absolute path when the name is absent, an impostor on PATH is refused, and re-runs leave one entry. Decision recorded as ADR 0019; ADR 0009's addendum is amended.
170 lines
6.9 KiB
Nix
170 lines
6.9 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;
|
|
|
|
# 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
|
|
];
|
|
};
|
|
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 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.
|
|
postInstall = ''
|
|
wrapProgram $out/bin/gitea-axi \
|
|
--suffix PATH : ${lib.makeBinPath [ git tea ]}
|
|
'';
|
|
|
|
# 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 Node the package is built against, published as a declared interface
|
|
# rather than left to be read off the build environment. The flake's dev shell
|
|
# consumes exactly this, so the two cannot drift onto different majors — and
|
|
# this attribute is why that holds, so removing it breaks the shell.
|
|
passthru = { inherit nodejs; };
|
|
|
|
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;
|
|
};
|
|
}
|