diff --git a/.github/workflows/actions-ci.yml b/.github/workflows/actions-ci.yml index 8274493e..251423cd 100644 --- a/.github/workflows/actions-ci.yml +++ b/.github/workflows/actions-ci.yml @@ -13,26 +13,32 @@ jobs: uses: ./.github/workflows/ci-changes.yml test: - name: Build action suite + name: Build action suite${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.build_action == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: 24 + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: actions/build/package-lock.json - - run: npm ci --ignore-scripts + - run: npm ci --ignore-scripts --engine-strict working-directory: actions/build - run: npm run lint working-directory: actions/build - run: actions/build/node_modules/.bin/playwright install --with-deps chromium - run: npm test --prefix actions/build - name: Prepare composite action fixture + if: ${{ matrix.node == 24 }} run: cp actions/build/test/fixture/package.json actions/build/test/fixture/package-lock.json actions/build/test/fixture/fixture-build.mjs . - uses: ./actions/build + if: ${{ matrix.node == 24 }} result: name: Replica action tests diff --git a/.github/workflows/hub-ci.yml b/.github/workflows/hub-ci.yml index c529fa75..fc223583 100644 --- a/.github/workflows/hub-ci.yml +++ b/.github/workflows/hub-ci.yml @@ -17,10 +17,14 @@ jobs: uses: ./.github/workflows/ci-changes.yml rails_test: - name: Rails tests + name: Rails tests${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.hub == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 @@ -29,13 +33,13 @@ jobs: working-directory: hub - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: | hub/package-lock.json actions/build/package-lock.json - - run: npm ci - - run: npm ci --ignore-scripts --omit=dev + - run: npm ci --engine-strict + - run: npm ci --ignore-scripts --omit=dev --engine-strict working-directory: actions/build - run: ../actions/build/node_modules/.bin/playwright install --with-deps chromium - run: bundle exec vite build @@ -119,43 +123,52 @@ jobs: - run: bin/bundler-audit check --update typecheck: - name: TypeScript types + name: TypeScript types${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.hub == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: hub/package-lock.json - - run: npm ci + - run: npm ci --engine-strict - run: npm run typecheck frontend_lint: - name: Frontend lint + name: Frontend lint${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.hub == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: hub/package-lock.json - - run: npm ci + - run: npm ci --engine-strict - run: npm run lint frontend_test_shard: - name: Frontend tests shard ${{ matrix.shard }}/4 + name: Frontend tests shard ${{ matrix.shard }}/4${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ always() }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: + node: [24, 26] shard: [1, 2, 3, 4] steps: - name: Require valid suite selection @@ -174,28 +187,32 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 if: ${{ needs.changes.outputs.hub == 'true' }} with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: hub/package-lock.json - - run: npm ci + - run: npm ci --engine-strict if: ${{ needs.changes.outputs.hub == 'true' }} - run: npm test -- --shard=${{ matrix.shard }}/4 if: ${{ needs.changes.outputs.hub == 'true' }} frontend_integration_test: - name: Controls and extension tests + name: Controls and extension tests${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.hub == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: hub/package-lock.json - - run: npm ci - - run: npm ci --ignore-scripts --omit=dev + - run: npm ci --engine-strict + - run: npm ci --ignore-scripts --omit=dev --engine-strict working-directory: actions/build - run: ../actions/build/node_modules/.bin/playwright install --with-deps chromium - run: npm run controls:test @@ -227,35 +244,43 @@ jobs: esac frontend_build: - name: Frontend build + name: Frontend build${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.hub == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: hub/package-lock.json - - run: npm ci + - run: npm ci --engine-strict - run: npm run build - run: npm run controls:assets:check - run: npm run extension:assets:check storybook_build: - name: Storybook build + name: Storybook build${{ matrix.node == 26 && ' (Node 26)' || '' }} needs: changes if: ${{ needs.changes.outputs.hub == 'true' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: hub/package-lock.json - - run: npm ci + - run: npm ci --engine-strict - run: npm run storybook:build production_database: diff --git a/.github/workflows/learning-ci.yml b/.github/workflows/learning-ci.yml index b4c789c5..bddec7b4 100644 --- a/.github/workflows/learning-ci.yml +++ b/.github/workflows/learning-ci.yml @@ -16,8 +16,12 @@ permissions: jobs: test: - name: Learning checks + name: Learning checks${{ matrix.node == 26 && ' (Node 26)' || '' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] defaults: run: working-directory: packages/learning @@ -25,10 +29,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: 24 + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: packages/learning/package-lock.json - - run: npm ci --ignore-scripts + - run: npm ci --ignore-scripts --engine-strict - run: npm run schema:check - run: npm test - run: npm run demo diff --git a/.github/workflows/starter-ci.yml b/.github/workflows/starter-ci.yml index 4c1e8887..4f1b35f5 100644 --- a/.github/workflows/starter-ci.yml +++ b/.github/workflows/starter-ci.yml @@ -24,8 +24,12 @@ permissions: jobs: test: - name: Starter checks + name: Starter checks${{ matrix.node == 26 && ' (Node 26)' || '' }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [24, 26] defaults: run: working-directory: starter @@ -33,10 +37,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: 24 + node-version: ${{ matrix.node }} cache: npm cache-dependency-path: starter/package-lock.json - - run: npm ci + - run: npm ci --engine-strict - run: npm run format:check - run: npm run typecheck - run: npm test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8513cc6..5cac783b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,8 @@ For improvements, explain the problem you want to solve and who would benefit. D ## Set up the development environment +Each Node.js project's `engines.node` declares its supported versions. The existing Hub, Starter, build-action, and Learning CI checks run on both supported Node majors. The devcontainer, production image, and composite build action retain their pinned runtime defaults; choosing another supported major locally does not change them. + ### Replica Hub Replica Hub uses a devcontainer so contributors run the same Ruby, Node, SQLite, GitHub CLI, and Chromium versions. @@ -85,7 +87,7 @@ npm test ### Replica learning -The learning package requires Node.js 24, npm, and Git: +The learning package requires npm, Git, and a Node.js version matching `engines.node` in [packages/learning/package.json](packages/learning/package.json): ```sh cd packages/learning diff --git a/actions/build/package-lock.json b/actions/build/package-lock.json index 90fa4b43..d1c26901 100644 --- a/actions/build/package-lock.json +++ b/actions/build/package-lock.json @@ -19,7 +19,7 @@ "prettier": "3.9.9" }, "engines": { - "node": "^24.0.0" + "node": "^24.15.0 || ^26.0.0" } }, "node_modules/@cacheable/memory": { diff --git a/actions/build/package.json b/actions/build/package.json index beb2888b..7326e55d 100644 --- a/actions/build/package.json +++ b/actions/build/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": "^24.0.0" + "node": "^24.15.0 || ^26.0.0" }, "scripts": { "lint": "eslint . && prettier --check .", diff --git a/docs/testing-and-review.md b/docs/testing-and-review.md index 05d8c0df..962f693f 100644 --- a/docs/testing-and-review.md +++ b/docs/testing-and-review.md @@ -56,10 +56,14 @@ The [shared selection workflow](../.github/workflows/ci-changes.yml) calls [the | Root contributor documents, shared and Hub guides, skills, and documentation assets | Neither | | Workflow changes, selector changes, and unclassified paths | Both | -The workflows preserve the existing required check names; skipping an entire workflow with a path filter could leave those checks pending. Static jobs use job-level conditions. The four required Vitest shard jobs always expand their matrix and validate selection, but skip checkout, setup, and tests when Hub is unaffected. The required `Frontend tests`, `Production image`, and `Replica action tests` result jobs accept omitted work only after successful selection explicitly excluded it. The production-image matrix checks AMD64 and ARM64 independently without cancelling the other architecture on failure. Selection failures, invalid outputs, and failures in selected suites fail those result checks. Keep required-check configuration unchanged. +The workflows preserve the existing required check names; skipping an entire workflow with a path filter could leave those checks pending. Static jobs use job-level conditions. The Vitest shard jobs always expand their Node and shard matrix and validate selection, but skip checkout, setup, and tests when Hub is unaffected. The required `Frontend tests`, `Production image`, and `Replica action tests` result jobs accept omitted work only after successful selection explicitly excluded it. The frontend and build-action result jobs require every selected Node version to pass. The production-image matrix checks AMD64 and ARM64 independently without cancelling the other architecture on failure. Selection failures, invalid outputs, and failures in selected suites fail those result checks. Keep required-check configuration unchanged. When adding a shared dependency or a new project, update the selector and its behavioral tests together. Unclassified paths deliberately run both suites until their dependencies are known. Run `node --test scripts/select-ci-suites.test.mjs` and `ruby scripts/check-ci-workflows.rb` from the repository root. The latter reads the actual workflow gates and checks their success, failure, cancellation, and missing-output behavior; both commands run before CI selects suites. Check changed workflows with `actionlint` when available. Actual job skipping and check reporting still need verification on GitHub after publishing. +Existing Node-dependent jobs in Hub, Starter, build-action, and Learning CI use a Node 24 and 26 matrix rather than a separate compatibility workflow. Each check keeps its existing name on Node 24 and adds a `(Node 26)` suffix on Node 26. A failure does not cancel the other version. npm installation enforces dependency engines, and `scripts/check-ci-workflows.rb` checks the supported ranges, matching lockfile metadata, and matrix wiring. These checks track the supported majors, not their exact minimum patch releases. + +Ruby-only jobs, CI selection, and container builds retain their configured runtimes. The build-action suite tests its JavaScript and browser capture on both Node versions, but runs the composite-action smoke test only on Node 24: the composite action pins that runtime itself and uploads a single named artifact. Container and composite-action runtime defaults are unchanged. + ## Update dependencies Hub and the shared build action wait seven days before resolving newly published dependencies. npm enforces the delay through each project's committed `.npmrc`. Hub's RubyGems source in `Gemfile` applies the delay to Bundler. Dependabot applies the same cooldown to npm, Bundler, and GitHub Actions updates across the repository. The starter manages its dependency resolution independently. diff --git a/hub/package-lock.json b/hub/package-lock.json index 1337d773..97c56e2b 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -49,7 +49,7 @@ "vitest": "5.0.3" }, "engines": { - "node": "^24.0.0" + "node": "^24.15.0 || ^26.0.0" } }, "node_modules/@adobe/css-tools": { diff --git a/hub/package.json b/hub/package.json index 63809ad3..632a1066 100644 --- a/hub/package.json +++ b/hub/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": "^24.0.0" + "node": "^24.15.0 || ^26.0.0" }, "scripts": { "controls:build": "node script/build-controls-bridge.mjs", diff --git a/packages/learning/README.md b/packages/learning/README.md index a224feb5..fa15fced 100644 --- a/packages/learning/README.md +++ b/packages/learning/README.md @@ -1,6 +1,6 @@ # Replica learning -`@github/replica-learning` lets Replica learn from review evidence without training a model. It owns the version 1 learning contracts, the memory provider interface, built-in SQLite, JSON Lines, and in-memory providers, two reference consolidators, a conformance suite, the `replica-learning` command, and an example that plugs in [Agent Memory Repo](examples/agent-memory-repo/README.md). It isn't published to npm yet, so run it from a Replica checkout with Node.js 24. +`@github/replica-learning` lets Replica learn from review evidence without training a model. It owns the version 1 learning contracts, the memory provider interface, built-in SQLite, JSON Lines, and in-memory providers, two reference consolidators, a conformance suite, the `replica-learning` command, and an example that plugs in [Agent Memory Repo](examples/agent-memory-repo/README.md). It isn't published to npm yet, so run it from a Replica checkout with a Node.js version matching `engines.node` in [package.json](package.json). The design and its trade-offs are in [ADR 0001](../../docs/adr/0001-learning-extension-architecture.md). @@ -135,7 +135,7 @@ The module must be inside the checkout and export `createMemoryProvider(options) | Provider | Choose it with | Stores | Redaction | | --- | --- | --- | --- | -| SQLite, the default | `"sqlite"` | One database file, with the SQLite driver built into Node.js 24 | Yes | +| SQLite, the default | `"sqlite"` | One database file, with the SQLite driver built into Node.js | Yes | | JSON Lines | `"jsonl"` | One file per stream that a person can read | Yes | | In memory | `"in-memory"` | Process memory, for tests | Yes | | Single file | [`test/fixtures/json-file-provider.mjs`](test/fixtures/json-file-provider.mjs) | One JSON file, written only from the contract with no imports from this package | No | diff --git a/packages/learning/package-lock.json b/packages/learning/package-lock.json index 56b60e8b..5406e0ff 100644 --- a/packages/learning/package-lock.json +++ b/packages/learning/package-lock.json @@ -18,7 +18,7 @@ "ajv": "8.20.0" }, "engines": { - "node": ">=24 <25" + "node": "^24.15.0 || ^26.0.0" }, "optionalDependencies": { "@github/copilot-sdk": "1.0.16" diff --git a/packages/learning/package.json b/packages/learning/package.json index 1eb8a7c5..522680cd 100644 --- a/packages/learning/package.json +++ b/packages/learning/package.json @@ -28,7 +28,7 @@ "demo:agent-memory-repo": "node examples/agent-memory-repo/demo.mjs" }, "engines": { - "node": ">=24 <25" + "node": "^24.15.0 || ^26.0.0" }, "devDependencies": { "ajv": "8.20.0" diff --git a/scripts/check-ci-workflows.rb b/scripts/check-ci-workflows.rb index be38183d..a86f35f0 100644 --- a/scripts/check-ci-workflows.rb +++ b/scripts/check-ci-workflows.rb @@ -2,6 +2,7 @@ require "open3" require "tmpdir" require "fileutils" +require "json" def check(condition, message) abort(message) unless condition @@ -16,7 +17,7 @@ def expression(value) [name, YAML.safe_load(File.read(File.join(root, ".github/workflows/#{name}.yml"))).fetch("jobs")] end expected_gates = { - ["hub-ci", "frontend_test_shard"] => ["Frontend tests shard ${{ matrix.shard }}/4", ["changes"]], + ["hub-ci", "frontend_test_shard"] => ["Frontend tests shard ${{ matrix.shard }}/4${{ matrix.node == 26 && ' (Node 26)' || '' }}", ["changes"]], ["hub-ci", "frontend_test"] => ["Frontend tests", %w[changes frontend_test_shard frontend_integration_test]], ["hub-ci", "production_image"] => ["Production image", %w[changes production_image_platform]], ["actions-ci", "result"] => ["Replica action tests", %w[changes test]] @@ -83,7 +84,7 @@ def expression(value) check(expression(image_platform["runs-on"]) == "matrix.runner", "Production image platform must use its native matrix runner") check(image_platform.fetch("steps").none? { |step| step["uses"].to_s.start_with?("docker/setup-qemu-action@") }, "Production image checks must not use QEMU") check(image_platform.dig("strategy", "fail-fast") == false, "Production image platform must not cancel other architectures on failure") -check(workflows.fetch("actions-ci").fetch("test")["name"] == "Build action suite", "Build action suite name changed") +check(workflows.fetch("actions-ci").fetch("test")["name"] == "Build action suite${{ matrix.node == 26 && ' (Node 26)' || '' }}", "Build action suite name changed") states = %w[success failure cancelled skipped] + [""] checks = 0 @@ -117,6 +118,38 @@ def expression(value) end puts "CI workflow guards and wiring passed (#{checks} shell cases)." +projects = %w[hub starter actions/build packages/learning] +projects.each do |project| + manifest = JSON.parse(File.read(File.join(root, project, "package.json"))) + lock = JSON.parse(File.read(File.join(root, project, "package-lock.json"))) + check(manifest.dig("engines", "node") == "^24.15.0 || ^26.0.0", "#{project}: supported Node versions changed") + check(lock.dig("packages", "", "engines") == manifest.fetch("engines"), "#{project}: locked Node engines differ from the manifest") +end +{ + "hub-ci" => %w[rails_test typecheck frontend_lint frontend_test_shard frontend_integration_test frontend_build storybook_build], + "actions-ci" => ["test"], + "starter-ci" => ["test"], + "learning-ci" => ["test"] +}.each do |workflow, job_ids| + jobs = YAML.safe_load(File.read(File.join(root, ".github/workflows/#{workflow}.yml"))).fetch("jobs") + job_ids.each do |id| + job = jobs.fetch(id) + label = "#{workflow}/#{id}" + check(job.dig("strategy", "matrix", "node") == [24, 26], "#{label}: existing checks must cover Node 24 and 26") + check(job.dig("strategy", "fail-fast") == false, "#{label}: failures must not cancel other versions") + check(job.fetch("name").end_with?("${{ matrix.node == 26 && ' (Node 26)' || '' }}"), "#{label}: preserve Node 24 check names and distinguish Node 26") + setup_node = job.fetch("steps").find { |step| step["uses"].to_s.start_with?("actions/setup-node@") } + check(setup_node && expression(setup_node.dig("with", "node-version")) == "matrix.node", "#{label}: execute the selected Node version") + installs = job.fetch("steps").select { |step| step["run"].to_s.start_with?("npm ci") } + check(!installs.empty? && installs.all? { |step| step.fetch("run").split.include?("--engine-strict") }, "#{label}: enforce dependency engines") + end +end +action_steps = workflows.fetch("actions-ci").fetch("test").fetch("steps") +fixture = action_steps.find { |step| step["name"] == "Prepare composite action fixture" } +composite = action_steps.find { |step| step["uses"] == "./actions/build" } +check(fixture && composite && [fixture, composite].all? { |step| expression(step["if"]) == "matrix.node == 24" }, "The Node 24 composite smoke test must run only once to avoid duplicate artifact uploads") +puts "Node support ranges, lockfiles and existing CI matrices passed." + release = YAML.safe_load(File.read(File.join(root, ".github/workflows/hub-release.yml"))).fetch("jobs") prepare = release.fetch("prepare") build = release.fetch("build") diff --git a/starter/package-lock.json b/starter/package-lock.json index 2f2c5fa4..e167cd34 100644 --- a/starter/package-lock.json +++ b/starter/package-lock.json @@ -27,7 +27,7 @@ "vitest": "5.0.3" }, "engines": { - "node": ">=24 <25" + "node": "^24.15.0 || ^26.0.0" } }, "node_modules/@adobe/css-tools": { diff --git a/starter/package.json b/starter/package.json index 19e553bb..533f0fe7 100644 --- a/starter/package.json +++ b/starter/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": ">=24 <25" + "node": "^24.15.0 || ^26.0.0" }, "scripts": { "dev": "vite",