From 4311889ca6627653a97a287a2547afd14c37e830 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:48:05 +0000 Subject: [PATCH 1/2] chore: support Node 26 alongside Node 24 Declare the dependency-compatible Node 24 floor and add explicit Node 26 support across all four projects. Exercise supported lower bounds and current Node 26 separately while preserving Node 24 CI and container defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/hub-ci.yml | 14 +-- .github/workflows/node-compatibility.yml | 105 +++++++++++++++++++++++ CONTRIBUTING.md | 4 +- actions/build/package-lock.json | 2 +- actions/build/package.json | 2 +- docs/testing-and-review.md | 2 + hub/package-lock.json | 2 +- hub/package.json | 2 +- packages/learning/README.md | 4 +- packages/learning/package-lock.json | 2 +- packages/learning/package.json | 2 +- scripts/check-ci-workflows.rb | 40 +++++++++ starter/package-lock.json | 2 +- starter/package.json | 2 +- 14 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/node-compatibility.yml diff --git a/.github/workflows/hub-ci.yml b/.github/workflows/hub-ci.yml index c529fa7..8356ade 100644 --- a/.github/workflows/hub-ci.yml +++ b/.github/workflows/hub-ci.yml @@ -29,7 +29,7 @@ jobs: working-directory: hub - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: | hub/package-lock.json @@ -127,7 +127,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: hub/package-lock.json - run: npm ci @@ -142,7 +142,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: hub/package-lock.json - run: npm ci @@ -174,7 +174,7 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 if: ${{ needs.changes.outputs.hub == 'true' }} with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: hub/package-lock.json - run: npm ci @@ -191,7 +191,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: hub/package-lock.json - run: npm ci @@ -235,7 +235,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: hub/package-lock.json - run: npm ci @@ -252,7 +252,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version-file: hub/package.json + node-version: 24 cache: npm cache-dependency-path: hub/package-lock.json - run: npm ci diff --git a/.github/workflows/node-compatibility.yml b/.github/workflows/node-compatibility.yml new file mode 100644 index 0000000..c54617c --- /dev/null +++ b/.github/workflows/node-compatibility.yml @@ -0,0 +1,105 @@ +name: Node compatibility + +on: + pull_request: + paths: + - ".github/workflows/**" + - ".github/extensions/replica-prototype/**" + - "actions/build/**" + - "hub/app/javascript/**" + - "hub/script/**" + - "hub/.storybook/**" + - "hub/*.json" + - "hub/.npmrc" + - "hub/*.ts" + - "hub/*.mjs" + - "packages/learning/**" + - "starter/**" + - "scripts/**" + - "eslint.config.mjs" + push: + branches: [main] + paths: + - ".github/workflows/**" + - ".github/extensions/replica-prototype/**" + - "actions/build/**" + - "hub/app/javascript/**" + - "hub/script/**" + - "hub/.storybook/**" + - "hub/*.json" + - "hub/.npmrc" + - "hub/*.ts" + - "hub/*.mjs" + - "packages/learning/**" + - "starter/**" + - "scripts/**" + - "eslint.config.mjs" + +permissions: + contents: read + +jobs: + check: + name: Node ${{ matrix.node }} / ${{ matrix.project }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ["24.15.0", "26.0.0", "26"] + project: [hub, starter, actions/build, packages/learning] + defaults: + run: + working-directory: ${{ matrix.project }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: ${{ matrix.node }} + check-latest: true + cache: npm + cache-dependency-path: ${{ matrix.project }}/package-lock.json + - run: npm ci --ignore-scripts --engine-strict + - name: Install Hub browser-test dependencies + if: ${{ matrix.project == 'hub' }} + run: npm ci --ignore-scripts --omit=dev --engine-strict + working-directory: actions/build + - name: Install Chromium + if: ${{ matrix.project == 'hub' || matrix.project == 'actions/build' }} + run: actions/build/node_modules/.bin/playwright install --with-deps chromium + working-directory: . + - name: Check Hub + if: ${{ matrix.project == 'hub' }} + run: | + npm run lint + npm test + npm run controls:test + REPLICA_CHROMIUM_PATH="$(node -p 'require("../actions/build/node_modules/playwright").chromium.executablePath()')" + export REPLICA_CHROMIUM_PATH + npm run extension:test + npm run build + npm run storybook:build + npm run controls:assets:check + npm run extension:assets:check + - name: Check Starter + if: ${{ matrix.project == 'starter' }} + run: | + npm run format:check + npm run typecheck + npm test + npm run build + - name: Check starter setup and root scripts + if: ${{ matrix.project == 'starter' }} + run: node --test scripts/*.test.mjs + working-directory: . + - name: Check build action + if: ${{ matrix.project == 'actions/build' }} + run: | + npm run lint + npm test + - name: Check Learning + if: ${{ matrix.project == 'packages/learning' }} + run: | + npm run schema:check + npm test + npm run demo + npm run demo:agent-memory-repo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8513cc..0d75930 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 [Node compatibility workflow](.github/workflows/node-compatibility.yml) checks the lower bounds and current releases across Hub, Starter, the build action, and Learning. 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 90fa4b4..d1c2690 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 beb2888..7326e55 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 05d8c0d..8f11f2a 100644 --- a/docs/testing-and-review.md +++ b/docs/testing-and-review.md @@ -60,6 +60,8 @@ The workflows preserve the existing required check names; skipping an entire wor 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. +The separate [Node compatibility workflow](../.github/workflows/node-compatibility.yml) runs on Node source, dependency, tooling, and workflow changes. It checks every Node project at each supported major's lower bound and the current release, alongside the existing default-runtime checks. It includes browser capture, Controls and extension integration, frontend builds, and Learning demos without changing the container runtime defaults or existing required check names. `scripts/check-ci-workflows.rb` also checks the supported ranges, matching lockfile metadata, and compatibility workflow wiring. + ## 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 1337d77..97c56e2 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 63809ad..632a106 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 a224feb..fa15fce 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 56b60e8..5406e0f 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 1eb8a7c..522680c 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 be38183..204ed68 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 @@ -117,6 +118,45 @@ 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 +workflows.fetch("hub-ci").each do |id, job| + job.fetch("steps", []).each do |step| + next unless step["uses"].to_s.start_with?("actions/setup-node@") + + check(step.dig("with", "node-version") == 24, "hub-ci/#{id}: default checks must keep the pinned Node major") + check(!step.fetch("with").key?("node-version-file"), "hub-ci/#{id}: supported ranges must not select the default CI runtime") + end +end +compatibility = YAML.safe_load(File.read(File.join(root, ".github/workflows/node-compatibility.yml"))) +check(compatibility.fetch("permissions") == { "contents" => "read" }, "Node compatibility must use read-only repository access") +node_checks = compatibility.fetch("jobs").fetch("check") +check(node_checks.dig("strategy", "matrix", "node") == ["24.15.0", "26.0.0", "26"], "Node compatibility must cover supported lower bounds and current Node 26") +check(node_checks.dig("strategy", "matrix", "project") == projects, "Node compatibility must cover every Node project") +check(node_checks.dig("strategy", "fail-fast") == false, "Node compatibility must report each project and version independently") +check(expression(node_checks.dig("defaults", "run", "working-directory")) == "matrix.project", "Node compatibility must run inside the selected project") +setup_node = node_checks.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", "Node compatibility must execute the selected version") +check(setup_node.dig("with", "check-latest") == true, "Node compatibility must resolve the current supported release") +check(node_checks.fetch("steps").any? { |step| step["run"] == "npm ci --ignore-scripts --engine-strict" && !step.key?("if") }, "Node compatibility must reject unsupported dependency engines in every project") +{ + "hub" => ["npm test", "npm run controls:test", "npm run extension:test", "npm run build", "npm run storybook:build", "npm run lint", "npm run controls:assets:check", "npm run extension:assets:check"], + "starter" => ["npm run format:check", "npm run typecheck", "npm test", "npm run build"], + "actions/build" => ["npm run lint", "npm test"], + "packages/learning" => ["npm run schema:check", "npm test", "npm run demo", "npm run demo:agent-memory-repo"] +}.each do |project, commands| + step = node_checks.fetch("steps").find { |candidate| expression(candidate["if"]) == "matrix.project == '#{project}'" && candidate.key?("run") && candidate["name"].to_s.start_with?("Check ") } + check(step, "Node compatibility must check #{project}") + lines = step.fetch("run").lines.map(&:strip) + commands.each { |command| check(lines.include?(command), "Node compatibility must run #{command} for #{project}") } +end +puts "Node support ranges, lockfiles and compatibility workflow wiring 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 2f2c5fa..e167cd3 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 19e553b..533f0fe 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", From d88cd1012839781ce125d58dba27eb5e31641c5b Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:07:10 +0000 Subject: [PATCH 2/2] ci: test supported Node versions in existing jobs Replace bundled compatibility checks with Node 24 and 26 matrices. Preserve required check names and keep the pinned composite smoke test single-run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/actions-ci.yml | 12 ++- .github/workflows/hub-ci.yml | 71 ++++++++++----- .github/workflows/learning-ci.yml | 10 ++- .github/workflows/node-compatibility.yml | 105 ----------------------- .github/workflows/starter-ci.yml | 10 ++- CONTRIBUTING.md | 2 +- docs/testing-and-review.md | 6 +- scripts/check-ci-workflows.rb | 55 ++++++------ 8 files changed, 100 insertions(+), 171 deletions(-) delete mode 100644 .github/workflows/node-compatibility.yml diff --git a/.github/workflows/actions-ci.yml b/.github/workflows/actions-ci.yml index 8274493..251423c 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 8356ade..fc22358 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: 24 + 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: 24 + 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: 24 + 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: 24 + 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: 24 + 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: 24 + 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: 24 + 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 b4c789c..bddec7b 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/node-compatibility.yml b/.github/workflows/node-compatibility.yml deleted file mode 100644 index c54617c..0000000 --- a/.github/workflows/node-compatibility.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Node compatibility - -on: - pull_request: - paths: - - ".github/workflows/**" - - ".github/extensions/replica-prototype/**" - - "actions/build/**" - - "hub/app/javascript/**" - - "hub/script/**" - - "hub/.storybook/**" - - "hub/*.json" - - "hub/.npmrc" - - "hub/*.ts" - - "hub/*.mjs" - - "packages/learning/**" - - "starter/**" - - "scripts/**" - - "eslint.config.mjs" - push: - branches: [main] - paths: - - ".github/workflows/**" - - ".github/extensions/replica-prototype/**" - - "actions/build/**" - - "hub/app/javascript/**" - - "hub/script/**" - - "hub/.storybook/**" - - "hub/*.json" - - "hub/.npmrc" - - "hub/*.ts" - - "hub/*.mjs" - - "packages/learning/**" - - "starter/**" - - "scripts/**" - - "eslint.config.mjs" - -permissions: - contents: read - -jobs: - check: - name: Node ${{ matrix.node }} / ${{ matrix.project }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - node: ["24.15.0", "26.0.0", "26"] - project: [hub, starter, actions/build, packages/learning] - defaults: - run: - working-directory: ${{ matrix.project }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: ${{ matrix.node }} - check-latest: true - cache: npm - cache-dependency-path: ${{ matrix.project }}/package-lock.json - - run: npm ci --ignore-scripts --engine-strict - - name: Install Hub browser-test dependencies - if: ${{ matrix.project == 'hub' }} - run: npm ci --ignore-scripts --omit=dev --engine-strict - working-directory: actions/build - - name: Install Chromium - if: ${{ matrix.project == 'hub' || matrix.project == 'actions/build' }} - run: actions/build/node_modules/.bin/playwright install --with-deps chromium - working-directory: . - - name: Check Hub - if: ${{ matrix.project == 'hub' }} - run: | - npm run lint - npm test - npm run controls:test - REPLICA_CHROMIUM_PATH="$(node -p 'require("../actions/build/node_modules/playwright").chromium.executablePath()')" - export REPLICA_CHROMIUM_PATH - npm run extension:test - npm run build - npm run storybook:build - npm run controls:assets:check - npm run extension:assets:check - - name: Check Starter - if: ${{ matrix.project == 'starter' }} - run: | - npm run format:check - npm run typecheck - npm test - npm run build - - name: Check starter setup and root scripts - if: ${{ matrix.project == 'starter' }} - run: node --test scripts/*.test.mjs - working-directory: . - - name: Check build action - if: ${{ matrix.project == 'actions/build' }} - run: | - npm run lint - npm test - - name: Check Learning - if: ${{ matrix.project == 'packages/learning' }} - run: | - npm run schema:check - npm test - npm run demo - npm run demo:agent-memory-repo diff --git a/.github/workflows/starter-ci.yml b/.github/workflows/starter-ci.yml index 4c1e888..4f1b35f 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 0d75930..5cac783 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ 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 [Node compatibility workflow](.github/workflows/node-compatibility.yml) checks the lower bounds and current releases across Hub, Starter, the build action, and Learning. The devcontainer, production image, and composite build action retain their pinned runtime defaults; choosing another supported major locally does not change them. +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 diff --git a/docs/testing-and-review.md b/docs/testing-and-review.md index 8f11f2a..962f693 100644 --- a/docs/testing-and-review.md +++ b/docs/testing-and-review.md @@ -56,11 +56,13 @@ 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. -The separate [Node compatibility workflow](../.github/workflows/node-compatibility.yml) runs on Node source, dependency, tooling, and workflow changes. It checks every Node project at each supported major's lower bound and the current release, alongside the existing default-runtime checks. It includes browser capture, Controls and extension integration, frontend builds, and Learning demos without changing the container runtime defaults or existing required check names. `scripts/check-ci-workflows.rb` also checks the supported ranges, matching lockfile metadata, and compatibility workflow wiring. +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 diff --git a/scripts/check-ci-workflows.rb b/scripts/check-ci-workflows.rb index 204ed68..a86f35f 100644 --- a/scripts/check-ci-workflows.rb +++ b/scripts/check-ci-workflows.rb @@ -17,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]] @@ -84,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 @@ -125,37 +125,30 @@ def expression(value) 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 -workflows.fetch("hub-ci").each do |id, job| - job.fetch("steps", []).each do |step| - next unless step["uses"].to_s.start_with?("actions/setup-node@") - - check(step.dig("with", "node-version") == 24, "hub-ci/#{id}: default checks must keep the pinned Node major") - check(!step.fetch("with").key?("node-version-file"), "hub-ci/#{id}: supported ranges must not select the default CI runtime") - end -end -compatibility = YAML.safe_load(File.read(File.join(root, ".github/workflows/node-compatibility.yml"))) -check(compatibility.fetch("permissions") == { "contents" => "read" }, "Node compatibility must use read-only repository access") -node_checks = compatibility.fetch("jobs").fetch("check") -check(node_checks.dig("strategy", "matrix", "node") == ["24.15.0", "26.0.0", "26"], "Node compatibility must cover supported lower bounds and current Node 26") -check(node_checks.dig("strategy", "matrix", "project") == projects, "Node compatibility must cover every Node project") -check(node_checks.dig("strategy", "fail-fast") == false, "Node compatibility must report each project and version independently") -check(expression(node_checks.dig("defaults", "run", "working-directory")) == "matrix.project", "Node compatibility must run inside the selected project") -setup_node = node_checks.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", "Node compatibility must execute the selected version") -check(setup_node.dig("with", "check-latest") == true, "Node compatibility must resolve the current supported release") -check(node_checks.fetch("steps").any? { |step| step["run"] == "npm ci --ignore-scripts --engine-strict" && !step.key?("if") }, "Node compatibility must reject unsupported dependency engines in every project") { - "hub" => ["npm test", "npm run controls:test", "npm run extension:test", "npm run build", "npm run storybook:build", "npm run lint", "npm run controls:assets:check", "npm run extension:assets:check"], - "starter" => ["npm run format:check", "npm run typecheck", "npm test", "npm run build"], - "actions/build" => ["npm run lint", "npm test"], - "packages/learning" => ["npm run schema:check", "npm test", "npm run demo", "npm run demo:agent-memory-repo"] -}.each do |project, commands| - step = node_checks.fetch("steps").find { |candidate| expression(candidate["if"]) == "matrix.project == '#{project}'" && candidate.key?("run") && candidate["name"].to_s.start_with?("Check ") } - check(step, "Node compatibility must check #{project}") - lines = step.fetch("run").lines.map(&:strip) - commands.each { |command| check(lines.include?(command), "Node compatibility must run #{command} for #{project}") } + "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 -puts "Node support ranges, lockfiles and compatibility workflow wiring passed." +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")