diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb59e7f..e2de7de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install locked dependencies run: npm ci @@ -59,15 +59,17 @@ jobs: run: npm run actionlint locked: - name: Locked OpenAI / Node.js ${{ matrix.node-version }} + name: Locked OpenAI / Node.js ${{ matrix.node-major }}.x runs-on: ubuntu-latest timeout-minutes: 25 strategy: fail-fast: false matrix: - node-version: - - 22.x - - 24.x + include: + - node-version: 22.23.4 + node-major: "22" + - node-version: 24.22.1 + node-major: "24" steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -118,7 +120,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install locked dependencies run: npm ci @@ -179,7 +181,7 @@ jobs: - name: Set up Node.js 22 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 22.x + node-version: 22.23.4 cache: npm - name: Install locked development dependencies run: npm ci @@ -202,7 +204,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install locked development dependencies run: npm ci @@ -222,7 +224,7 @@ jobs: - name: Set up Node.js 26 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 26.x + node-version: 26.12.1 cache: npm - name: Install locked dependencies run: npm ci @@ -249,7 +251,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install locked dependencies run: npm ci diff --git a/.github/workflows/live-smoke.yml b/.github/workflows/live-smoke.yml index c7003f1..f741712 100644 --- a/.github/workflows/live-smoke.yml +++ b/.github/workflows/live-smoke.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install locked dependencies run: npm ci diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4e124f0..ae1be7c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -66,7 +66,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Classify the exact Release Please handoff id: result @@ -544,7 +544,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install validation dependencies without lifecycle scripts run: npm ci --ignore-scripts @@ -695,7 +695,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install locked dependencies run: npm ci @@ -755,7 +755,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 package-manager-cache: false registry-url: https://registry.npmjs.org - name: Use a Trusted Publishing-capable npm CLI diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 997fbf0..13690a2 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -64,7 +64,7 @@ jobs: - name: Set up Node.js 24 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.x + node-version: 24.22.1 cache: npm - name: Install validation dependencies without lifecycle scripts run: npm ci --ignore-scripts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 40843f4..560775b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -232,10 +232,11 @@ does not establish GitHub Actions execution. ## Runtime policy -The package `engines` contract supports Node.js 22 and 24 only. Node.js 26 is an -advisory compatibility target and remains outside `engines` until it enters LTS -and the policy is deliberately updated. Node.js 18 and 20 are unsupported. The -normal publication runtime is Node.js 24 and must satisfy npm Trusted +The package `engines` contract supports patched Node.js 22.23.4+ and 24.22.1+ +within those major lines only. Node.js 26 is an advisory compatibility target +and remains outside `engines` until it enters LTS and the policy is deliberately +updated. Node.js 18 and 20 are unsupported. The normal publication runtime is +Node.js 24.22.1+ and must satisfy npm Trusted Publishing's minimum Node.js and npm CLI versions recorded in [RELEASING.md](./RELEASING.md). diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 8caf91e..e5ffde6 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -95,11 +95,11 @@ deferred. No compatibility aliases are provided for legacy client names. The consumer manifest range is `openai:^6.47.0`. Release documentation may list a version as verified only after the corresponding lane passes. -| Lane | Version selection | Runtime | Required evidence | -| ----------- | -------------------------------- | ----------------- | ---------------------------------------- | -| Minimum | `6.47.0` | Node.js 22 | Runtime and ESM/CommonJS consumer types | -| Development | Version from `package-lock.json` | Node.js 22 and 24 | Every blocking runtime | -| Canary | Latest compatible 6.x | Node.js 24 | Scheduled and dependency-update workflow | +| Lane | Version selection | Runtime | Required evidence | +| ----------- | -------------------------------- | ----------------------------- | ---------------------------------------- | +| Minimum | `6.47.0` | Node.js 22.23.4+ | Runtime and ESM/CommonJS consumer types | +| Development | Version from `package-lock.json` | Node.js 22.23.4+ and 24.22.1+ | Every blocking runtime | +| Canary | Latest compatible 6.x | Node.js 24.22.1+ | Scheduled and dependency-update workflow | The compatible manifest range can resolve an upstream minor before it appears in this verified table. If the canary finds an incompatibility, maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c67671..c10a568 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ incidental 0.1 change. Propose future scope through an issue and roadmap update. ## Development setup -Use Node.js 22 or 24 and install exactly from the lock file: +Use patched Node.js 22.23.4+ or 24.22.1+ and install exactly from the lock file: ```bash npm ci diff --git a/README.md b/README.md index 9733bc5..94a38c7 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,14 @@ keys are outside the 0.1 scope. ## Requirements -- Node.js 22 or 24 +- Node.js 22.23.4 or newer within the 22.x line, or Node.js 24.22.1 or newer + within the 24.x line - A CometAPI API key -Node.js 26 is an advisory test target until it enters LTS. Node.js 18 and 20 are -not supported. The package `engines` range includes Node.js 22 and 24 only; -passing the Node.js 26 advisory lane does not create a support claim. +Node.js 26 is an advisory test target until it enters LTS; the advisory lane uses +Node.js 26.12.1 or newer. Node.js 18 and 20 are not supported. The package +`engines` range includes only the patched Node.js 22 and 24 lines; passing the +Node.js 26 advisory lane does not create a support claim. Create an API key at . Keep it secret: do not embed it in source code, browser bundles, logs, or committed environment diff --git a/RELEASING.md b/RELEASING.md index 052ec4e..7437d6e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -438,10 +438,10 @@ dist-tag. Before authorizing release: call before marking the alpha released. Enable Release Please only after the initial manual alpha is complete. -The normal publication runner uses Node.js 22.14.0 or later and npm CLI 11.5.1 -or later; Node.js 24 is preferred. npm Trusted Publishing requires the manifest -`repository.url` to match the canonical GitHub repository exactly. Publication -is blocked until that canonical URL is configured. +The normal publication runner uses Node.js 24.22.1 or later and npm CLI 11.5.1 +or later; a Node.js 22 runner must use 22.23.4 or later. npm Trusted Publishing +requires the manifest `repository.url` to match the canonical GitHub repository +exactly. Publication is blocked until that canonical URL is configured. ## Conditional first-publication bootstrap diff --git a/ROADMAP.md b/ROADMAP.md index 00e5250..4312f5b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -513,9 +513,10 @@ verification. ## First npm Publication Bootstrap The normal publication path is npm Trusted Publishing from a GitHub-hosted -runner using Node.js 22.14.0 or later, npm CLI 11.5.1 or later, and a -`repository.url` that exactly matches the GitHub repository. Node.js 24 is the -preferred release runtime. +runner using Node.js 24.22.1 or later, npm CLI 11.5.1 or later, and a +`repository.url` that exactly matches the GitHub repository. A Node.js 22 +runner must use 22.23.4 or later; Node.js 24.22.1 is the preferred release +runtime. Maintainers must first attempt to configure the Trusted Publisher normally. If npm does not allow configuration before the first package publication, only diff --git a/SUPPORT.md b/SUPPORT.md index 3a042cc..f137e96 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -4,7 +4,7 @@ Support for the 0.1 line is limited to the tested operations and runtimes in [COMPATIBILITY.md](./COMPATIBILITY.md): Chat Completions, Responses, and Models -on Node.js 22 and 24. Node.js 26 is advisory. Inherited OpenAI methods and +on patched Node.js 22.23.4+ and 24.22.1+. Node.js 26 is advisory. Inherited OpenAI methods and future provider or CometAPI-specific resources are outside this support contract. diff --git a/package-lock.json b/package-lock.json index 2989dcb..99f02e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "yaml": "^2.9.0" }, "engines": { - "node": "^22.0.0 || ^24.0.0" + "node": "^22.23.4 || ^24.22.1" } }, "node_modules/@andrewbranch/untar.js": { diff --git a/package.json b/package.json index c194dbc..a7b651a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "README.md" ], "engines": { - "node": "^22.0.0 || ^24.0.0" + "node": "^22.23.4 || ^24.22.1" }, "repository": { "type": "git", @@ -69,6 +69,7 @@ "check:standalone-content": "node scripts/check-standalone-content.mjs", "check:self-contained": "node scripts/check-self-contained.mjs", "check:public-preview": "node scripts/check-public-preview.mjs", + "check:runtime": "node scripts/check-node-runtime.mjs", "actionlint": "node scripts/run-actionlint.mjs", "verify:offline": "node scripts/verify.mjs --offline", "verify": "node scripts/verify.mjs", diff --git a/scripts/check-node-runtime.mjs b/scripts/check-node-runtime.mjs new file mode 100644 index 0000000..f26bb0e --- /dev/null +++ b/scripts/check-node-runtime.mjs @@ -0,0 +1,13 @@ +import { + isPatchedNodeVersion, + MINIMUM_PATCHED_NODE_VERSIONS, +} from "./node-version-policy.mjs"; + +const version = `v${process.versions.node}`; +if (!isPatchedNodeVersion(version)) { + console.error( + `Node.js ${version} is below the patched runtime floor (22 >= ${MINIMUM_PATCHED_NODE_VERSIONS[22]}, 24 >= ${MINIMUM_PATCHED_NODE_VERSIONS[24]}, 26 >= ${MINIMUM_PATCHED_NODE_VERSIONS[26]}).`, + ); + process.exit(1); +} +console.log(`Node.js runtime ${version} satisfies the patched security floor.`); diff --git a/scripts/node-version-policy.mjs b/scripts/node-version-policy.mjs new file mode 100644 index 0000000..e647389 --- /dev/null +++ b/scripts/node-version-policy.mjs @@ -0,0 +1,30 @@ +const minimums = Object.freeze({ + 22: Object.freeze([22, 23, 4]), + 24: Object.freeze([24, 22, 1]), + 26: Object.freeze([26, 12, 1]), +}); + +export const MINIMUM_PATCHED_NODE_VERSIONS = Object.freeze({ + 22: "22.23.4", + 24: "24.22.1", + 26: "26.12.1", +}); + +export function parseNodeVersion(value) { + const match = /^(?:v)?(\d+)\.(\d+)\.(\d+)$/.exec(value ?? ""); + if (!match) return null; + return match.slice(1).map(Number); +} + +export function isPatchedNodeVersion( + value, + allowedMajors = Object.keys(minimums), +) { + const parsed = parseNodeVersion(value); + if (!parsed || !allowedMajors.map(Number).includes(parsed[0])) return false; + const minimum = minimums[parsed[0]]; + return ( + parsed[1] > minimum[1] || + (parsed[1] === minimum[1] && parsed[2] >= minimum[2]) + ); +} diff --git a/scripts/release-validation.mjs b/scripts/release-validation.mjs index 006bcec..02bcf61 100644 --- a/scripts/release-validation.mjs +++ b/scripts/release-validation.mjs @@ -16,7 +16,7 @@ const SEMVER_TOKEN_PATTERN = new RegExp( "gi", ); -export const SUPPORTED_NODE_ENGINES = "^22.0.0 || ^24.0.0"; +export const SUPPORTED_NODE_ENGINES = "^22.23.4 || ^24.22.1"; export const SUPPORTED_OPENAI_RANGE = "^6.47.0"; export const CANONICAL_IDENTITY = Object.freeze({ @@ -154,17 +154,23 @@ function assertSupportedNodeEngines(manifest, label) { } const majors = range.split(/\s*\|\|\s*/).map((clause) => { - const match = /^\^(0|[1-9]\d*)\.0\.0$/.exec(clause); + const match = /^\^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(clause); if (!match) { throw new Error( `${label} engines.node contains an unsupported range: ${clause}.`, ); } - return Number(match[1]); + return [Number(match[1]), Number(match[2]), Number(match[3])]; }); - if (majors.length !== 2 || majors[0] !== 22 || majors[1] !== 24) { + if ( + JSON.stringify(majors) !== + JSON.stringify([ + [22, 23, 4], + [24, 22, 1], + ]) + ) { throw new Error( - `${label} engines.node must support exactly Node.js 22 and 24.`, + `${label} engines.node must support patched Node.js 22.23.4 and 24.22.1 ranges.`, ); } } @@ -387,9 +393,19 @@ export function validatePublicationNeutralReadme(readme) { "README.md must link to the unversioned cometapi npm package page.", ); } + const readmeVersionText = [ + document.referenceText, + ...markdownCodeValues(document.root), + ] + .join("\n") + // Runtime patch floors are durable compatibility policy, not package + // release pins. Keep allowing the explicit Node.js versions required by + // the supported runtime matrix while rejecting package and dependency + // versions elsewhere in the README. + .replace(/\bNode(?:\.js)?\s+\d+\.\d+\.\d+/gi, "Node.js runtime"); if ( new RegExp(SEMVER_TOKEN_PATTERN.source, SEMVER_TOKEN_PATTERN.flags).test( - [document.referenceText, ...markdownCodeValues(document.root)].join("\n"), + readmeVersionText, ) ) { throw new Error( diff --git a/scripts/release-workflow-validation.mjs b/scripts/release-workflow-validation.mjs index ef00b5b..e34e859 100644 --- a/scripts/release-workflow-validation.mjs +++ b/scripts/release-workflow-validation.mjs @@ -18,7 +18,7 @@ const RELEASE_WORKFLOW_STEP = "Run Release Please"; const PUBLISH_OPERATION = "release"; const NPM_TAG_POLICY_ID = 55718965; const PUBLISH_WORKFLOW_CONTRACT_SHA256 = - "684e15c13835ae3b139929ae78e5bbd5ba618ea8f30ebba8dbb4c42004d57a06"; + "379ba8678416ff96d3bcf220a23e8602d2b55890164b2847331def10b4e99693"; const PUBLISH_HANDOFF_IF = "vars.RELEASE_PLEASE_ENABLED == 'true' && github.event_name == 'workflow_run' && github.run_attempt == 1 && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main'"; const PUBLISH_RESULT_IF = "steps.result.outputs.has-result == 'true'"; diff --git a/scripts/verify.mjs b/scripts/verify.mjs index 17ff2ff..6834426 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -1,6 +1,7 @@ import { run } from "./lib.mjs"; const offlineOnly = process.argv.includes("--offline"); +run("npm", ["run", "check:runtime"]); const offlineChecks = [ "format:check", "lint", diff --git a/tests/ci-workflow.test.mjs b/tests/ci-workflow.test.mjs index cd2a30b..6638206 100644 --- a/tests/ci-workflow.test.mjs +++ b/tests/ci-workflow.test.mjs @@ -107,8 +107,11 @@ describe("blocking CI workflow", () => { "fail-fast", "matrix", ]); - expect(Object.keys(locked.strategy.matrix)).toEqual(["node-version"]); - expect(locked.strategy?.matrix?.["node-version"]).toEqual(["22.x", "24.x"]); + expect(Object.keys(locked.strategy.matrix)).toEqual(["include"]); + expect(locked.strategy?.matrix?.include).toEqual([ + { "node-version": "22.23.4", "node-major": "22" }, + { "node-version": "24.22.1", "node-major": "24" }, + ]); const liveContractSteps = locked.steps.filter( (step) => step.run === "npm run test:live-contract", diff --git a/tests/node-runtime.test.mjs b/tests/node-runtime.test.mjs new file mode 100644 index 0000000..caf17c1 --- /dev/null +++ b/tests/node-runtime.test.mjs @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { + isPatchedNodeVersion, + MINIMUM_PATCHED_NODE_VERSIONS, +} from "../scripts/node-version-policy.mjs"; + +describe("patched Node.js runtime policy", () => { + it.each([ + ["22", "22.23.4"], + ["24", "24.22.1"], + ["26", "26.12.1"], + ])("accepts the minimum patch for Node %s", (major, version) => { + expect(version).toBe(MINIMUM_PATCHED_NODE_VERSIONS[major]); + expect(isPatchedNodeVersion(`v${version}`)).toBe(true); + }); + + it.each(["v22.23.3", "v24.22.0", "v26.12.0", "v20.20.0"])( + "rejects %s", + (version) => { + expect(isPatchedNodeVersion(version)).toBe(false); + }, + ); +}); diff --git a/tests/publication-neutral-docs.test.mjs b/tests/publication-neutral-docs.test.mjs index e12ed86..f2c9bfa 100644 --- a/tests/publication-neutral-docs.test.mjs +++ b/tests/publication-neutral-docs.test.mjs @@ -36,7 +36,7 @@ describe("publication-neutral release documents", () => { "For stable 0.1.2, CI run 30515861246 passed.", "Release 0.2.0 provider adapters are planned.", "The minimum supported OpenAI version is 6.47.0.", - "Node.js 22.14.0 or later is required for Trusted Publishing.", + "Node.js 22.23.4 or later is required for Trusted Publishing.", "Release Please 17.6.0 generated the historical release.", "```text\nThe current npm release is 0.1.2.\n```", "", diff --git a/tests/workflow-contract.test.mjs b/tests/workflow-contract.test.mjs index dfcf642..fcdf6fb 100644 --- a/tests/workflow-contract.test.mjs +++ b/tests/workflow-contract.test.mjs @@ -104,13 +104,16 @@ describe("GitHub Actions workflow contract", () => { const ci = workflow("ci.yml"); const locked = job(ci, "locked"); expect(locked).toMatch( - /matrix:\n {8}node-version:\n {10}- 22\.x\n {10}- 24\.x/, + /matrix:\n {8}include:\n {10}- node-version: 22\.23\.4\n {12}node-major: "22"\n {10}- node-version: 24\.22\.1\n {12}node-major: "24"/, + ); + expect(locked).toMatch( + /^ {4}name: Locked OpenAI \/ Node\.js \$\{\{ matrix\.node-major \}\}\.x$/m, ); expect(locked).not.toContain("- 26.x"); const advisory = job(ci, "current-node-advisory"); expect(advisory).toMatch(/^ {4}continue-on-error: true$/m); - expect(advisory).toMatch(/^ {10}node-version: 26\.x$/m); + expect(advisory).toMatch(/^ {10}node-version: 26\.12\.1$/m); }); it("runs the latest OpenAI lane for manually dispatched release-candidate CI", () => {