ci: matrix over Node 22 and 24, add the bench and packaging tiers (task 0040) #49

Merged
alexion merged 1 commits from task-0040-ci-node-matrix-and-new-tiers into main 2026-07-20 07:30:29 -04:00
6 changed files with 80 additions and 18 deletions

View File

@@ -39,7 +39,7 @@ It ships as both an installable npm CLI and a bundled Agent Skill, so any agent
### Language and Runtime
TypeScript on Node 20+, matching the `gh-axi` reference implementation.
TypeScript on the supported Node long-term-support majors — currently 22 and 24, as declared in the manifest's engine range and matrixed over by continuous integration.
ESM module format.
### Implementation Strategy

View File

@@ -24,11 +24,38 @@ The workflow keeps its container-and-npm shape and its GitHub Actions compatibil
## Acceptance criteria
- [ ] The workflow runs a matrix over the two supported Node versions, and no leg runs an end-of-life Node.
- [ ] The manifest's declared engine range names exactly the versions the matrix tests.
- [ ] The packaging tier's engine assertion matches the narrowed range and passes.
- [ ] The live end-to-end tier runs on the highest leg only.
- [ ] The benchmark harness tier runs on every leg, under its own runner configuration.
- [ ] The packaging tier runs on the highest leg only.
- [ ] The benchmark smoke tier does not run.
- [ ] The workflow syntax stays GitHub-Actions-compatible.
- [x] The workflow runs a matrix over the two supported Node versions, and no leg runs an end-of-life Node.
- [x] The manifest's declared engine range names exactly the versions the matrix tests.
- [x] The packaging tier's engine assertion matches the narrowed range and passes.
- [x] The live end-to-end tier runs on the highest leg only.
- [x] The benchmark harness tier runs on every leg, under its own runner configuration.
- [x] The packaging tier runs on the highest leg only.
- [x] The benchmark smoke tier does not run.
- [x] The workflow syntax stays GitHub-Actions-compatible.
## Implementation Notes
The supported majors are 22 and 24 — the two current long-term-support lines, and the pair that brackets the nixpkgs default the flake already builds against (24.18.0).
The engine range became `^22 || ^24` rather than `>=22`, so it names those two majors and nothing else; a bare floor would have re-promised the odd-numbered 23, which is itself end-of-life.
The single matrix job was kept rather than split into a matrixed job plus a separate single-version job for the once-only tiers.
The acceptance criteria are phrased in terms of matrix legs, one job keeps the version list in exactly one place, and the task asked that the workflow keep its container-and-npm shape.
The cost is that the Gitea service container starts on the Node 22 leg without the end-to-end tier consuming it — cheap next to running that tier twice, which is what the spec's rationale was actually guarding against.
The once-only steps condition on `matrix.highest`, a flag attached to the `24` leg through a `strategy.matrix.include` entry, rather than on `matrix.node == '24'` at each site.
An `include` entry whose keys match an existing combination augments that leg rather than adding a new one, on both GitHub Actions and Gitea Actions, and an undefined context property is falsy on the other leg.
Two consequential changes beyond the criteria, both surfaced by review:
`@types/node` moved from `^20.19.0` to `^22.20.1`.
It was the last Node 20 reference left in the manifest, and the typecheck runs on every leg.
It tracks the *floor* rather than the highest leg deliberately: typings for 24 would let source compile against APIs the Node 22 leg does not have.
`.claude/spec/gitea-axi.md`'s "Language and Runtime" section still read "TypeScript on Node 20+", which this change falsified.
Verified by running the whole matrix's worth of tiers locally through `nix develop`: typecheck, the fast tier with coverage thresholds (410 tests), the benchmark harness tier under its own runner configuration (117), and the full packaging tier (7), plus `nix build .#gitea-axi` after the lockfile moved.
The workflow was parsed with `yq` to confirm the matrix, the container expression, and the two `if:` conditions land where intended.
The live end-to-end tier was not run locally — it needs a disposable Gitea instance, and the diff only gates it.
One follow-up worth flagging: `vitest.bench.config.ts` sets `passWithNoTests: true`, so if its include glob ever broke, the new benchmark step would go green while running nothing — precisely the failure mode this task cites as the reason to add the step.
Pre-existing, and left alone here rather than widened into a test-configuration change.

View File

@@ -16,7 +16,22 @@ on:
jobs:
test:
runs-on: ubuntu-latest
container: node:20-bookworm
container: node:${{ matrix.node }}-bookworm
strategy:
# Every leg's result is wanted: a failure on one Node major says nothing
# about the other, and cancelling the sibling would hide half the answer.
fail-fast: false
matrix:
# The supported Node majors, matching the manifest's declared engine
# range. Node 20 is end-of-life and deliberately absent.
node: ["22", "24"]
# Augments the highest leg with a flag, so the steps that run once
# share one named condition rather than each restating a version
# number. Keep this entry's `node` matching the last element above.
include:
- node: "24"
highest: true
services:
gitea:
@@ -50,5 +65,20 @@ jobs:
- name: Unit and integration tiers (with coverage thresholds)
run: npm run test:coverage
# Deterministic, and needs neither network nor the agent SDK, so it runs
# everywhere. Its non-default runner configuration makes it an easy tier
# to believe is running when it is not.
- name: Benchmark harness tier
run: npm run test:bench
# Exercises the Gitea API contract rather than Node-version behaviour, so
# one leg is enough.
- name: End-to-end tier
if: matrix.highest
run: npm run test:e2e
# Slow, and near enough version-independent — but the only automated guard
# on the distribution artifact, since publishing is a manual command.
- name: Packaging tier
if: matrix.highest
run: npm run test:pack

10
package-lock.json generated
View File

@@ -17,14 +17,14 @@
"gitea-axi": "dist/main.js"
},
"devDependencies": {
"@types/node": "^20.19.0",
"@types/node": "^22.20.1",
"@vitest/coverage-v8": "^3.2.7",
"tsx": "^4.23.1",
"typescript": "^5.8.0",
"vitest": "^3.2.0"
},
"engines": {
"node": ">=20"
"node": "^22 || ^24"
},
"peerDependencies": {
"@anthropic-ai/claude-agent-sdk": ">=0.3.0"
@@ -1050,9 +1050,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -5,7 +5,7 @@
"type": "module",
"license": "MIT",
"engines": {
"node": ">=20"
"node": "^22 || ^24"
},
"bin": {
"gitea-axi": "dist/main.js"
@@ -47,7 +47,7 @@
"gitea-js": "^1.23.0"
},
"devDependencies": {
"@types/node": "^20.19.0",
"@types/node": "^22.20.1",
"@vitest/coverage-v8": "^3.2.7",
"tsx": "^4.23.1",
"typescript": "^5.8.0",

View File

@@ -63,8 +63,13 @@ describe("npm distribution artifact", () => {
expect(packedManifest.license).toBe("MIT");
// The declared range names exactly the majors continuous integration
// matrixes over, so nothing is promised that is never tested. Node 20 is
// end-of-life and deliberately no longer named.
const engines = packedManifest.engines as Record<string, string> | undefined;
expect(engines?.node).toMatch(/20/);
expect(engines?.node).toMatch(/22/);
expect(engines?.node).toMatch(/24/);
expect(engines?.node).not.toMatch(/20/);
expect(packedManifest.type).toBe("module");
});