From ccc8dbe9985e19a536ae237cc941baa0f73e5fa8 Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 19 Jul 2026 23:45:55 -0400 Subject: [PATCH] ci: matrix over Node 22 and 24, add the bench and packaging tiers (task 0040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow pinned Node 20, which reached end-of-life in April 2026, while the manifest promised support down to it — so the entire claimed range below the single tested version went unverified and its floor was unsupported. Matrix over the two supported long-term-support majors and narrow the engine range to `^22 || ^24`, naming exactly what is tested. Narrowing is free now because nothing has been published and no tags exist. The benchmark harness tier joins every leg: it is deterministic and needs no network, and its non-default runner configuration made it easy to believe it was running when it was not. The end-to-end and packaging tiers run on the highest leg only, conditioned on a flag attached to that leg through a matrix `include` entry rather than on a version number restated at each site. The benchmark smoke tier stays out, since it would pass by skipping. `@types/node` follows the new floor; it was the last Node 20 reference in the manifest, and the typecheck runs on every leg. --- .claude/spec/gitea-axi.md | 2 +- .../0040-ci-node-matrix-and-new-tiers.md | 43 +++++++++++++++---- .gitea/workflows/ci.yml | 32 +++++++++++++- package-lock.json | 10 ++--- package.json | 4 +- test/packaging/tarball.test.ts | 7 ++- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/.claude/spec/gitea-axi.md b/.claude/spec/gitea-axi.md index e17375d..9af2465 100644 --- a/.claude/spec/gitea-axi.md +++ b/.claude/spec/gitea-axi.md @@ -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 diff --git a/.claude/tasks/0040-ci-node-matrix-and-new-tiers.md b/.claude/tasks/0040-ci-node-matrix-and-new-tiers.md index 55686ff..a8d4c75 100644 --- a/.claude/tasks/0040-ci-node-matrix-and-new-tiers.md +++ b/.claude/tasks/0040-ci-node-matrix-and-new-tiers.md @@ -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. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 21143e1..b95599a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/package-lock.json b/package-lock.json index 81d8092..8895a44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index a0a9efe..1e6fb88 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/packaging/tarball.test.ts b/test/packaging/tarball.test.ts index 463cc90..0df6272 100644 --- a/test/packaging/tarball.test.ts +++ b/test/packaging/tarball.test.ts @@ -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 | 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"); }); -- 2.47.3