From 7bb79eeec5e7c6160ec7d796a999d2297ae128a5 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 15 Jul 2026 11:47:13 -0700 Subject: [PATCH 1/4] ci: unblock TypeScript migration release gates Signed-off-by: Carlos Villela --- .../actions/ci-cli-coverage-merge/action.yaml | 12 ++- .../actions/ci-cli-coverage-shard/action.yaml | 6 +- .../actions/ci-plugin-coverage/action.yaml | 6 +- .github/workflows/e2e.yaml | 1 + test/e2e/support/e2e-workflow.test.ts | 18 +++++ test/pr-e2e-gate-shards.test.ts | 6 ++ test/pr-workflow-contract.test.ts | 78 +++++++++++++++++++ tools/e2e/workflow-boundary.mts | 14 ++-- 8 files changed, 132 insertions(+), 9 deletions(-) diff --git a/.github/actions/ci-cli-coverage-merge/action.yaml b/.github/actions/ci-cli-coverage-merge/action.yaml index c24686e3fe6..697415ce85d 100644 --- a/.github/actions/ci-cli-coverage-merge/action.yaml +++ b/.github/actions/ci-cli-coverage-merge/action.yaml @@ -51,7 +51,11 @@ runs: shell: bash run: | test -s dist/nemoclaw.js - npx tsx scripts/check-dist-sourcemaps.ts dist + dist_sourcemap_check=scripts/check-dist-sourcemaps.mts + if [ ! -f "$dist_sourcemap_check" ]; then + dist_sourcemap_check=scripts/check-dist-sourcemaps.ts + fi + npx tsx "$dist_sourcemap_check" dist - name: Download CLI shard blob reports uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -95,7 +99,11 @@ runs: --coverage.include="src/**/*.ts" \ --coverage.exclude="test/**/*.js" \ --coverage.exclude="test/**/*.ts" - npx tsx scripts/check-coverage-ratchet.ts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json "CLI coverage" + coverage_ratchet=scripts/check-coverage-ratchet.mts + if [ ! -f "$coverage_ratchet" ]; then + coverage_ratchet=scripts/check-coverage-ratchet.ts + fi + npx tsx "$coverage_ratchet" coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json "CLI coverage" - name: Upload CLI coverage report if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} diff --git a/.github/actions/ci-cli-coverage-shard/action.yaml b/.github/actions/ci-cli-coverage-shard/action.yaml index 0d433054c0e..4bb786089b1 100644 --- a/.github/actions/ci-cli-coverage-shard/action.yaml +++ b/.github/actions/ci-cli-coverage-shard/action.yaml @@ -102,7 +102,11 @@ runs: run: | node -e "require('node:fs').rmSync('dist', { recursive: true, force: true })" npm run build:cli - npx tsx scripts/check-dist-sourcemaps.ts dist + dist_sourcemap_check=scripts/check-dist-sourcemaps.mts + if [ ! -f "$dist_sourcemap_check" ]; then + dist_sourcemap_check=scripts/check-dist-sourcemaps.ts + fi + npx tsx "$dist_sourcemap_check" dist - name: Upload compiled CLI artifact if: ${{ steps.validate-shard-inputs.outputs.upload_build_artifact == 'true' && success() }} diff --git a/.github/actions/ci-plugin-coverage/action.yaml b/.github/actions/ci-plugin-coverage/action.yaml index ddbb9e77f8b..0d07bf0f18d 100644 --- a/.github/actions/ci-plugin-coverage/action.yaml +++ b/.github/actions/ci-plugin-coverage/action.yaml @@ -31,7 +31,11 @@ runs: --coverage.include="nemoclaw/src/**/*.ts" \ --coverage.include="nemoclaw/src/**/*.cts" \ --coverage.exclude="**/*.test.ts" - npx tsx scripts/check-coverage-ratchet.ts coverage/plugin/coverage-summary.json ci/coverage-threshold-plugin.json "Plugin coverage" + coverage_ratchet=scripts/check-coverage-ratchet.mts + if [ ! -f "$coverage_ratchet" ]; then + coverage_ratchet=scripts/check-coverage-ratchet.ts + fi + npx tsx "$coverage_ratchet" coverage/plugin/coverage-summary.json ci/coverage-threshold-plugin.json "Plugin coverage" - name: Upload plugin coverage report if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 00bdb5e382d..7ee99364fb1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -4351,6 +4351,7 @@ jobs: NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" NEMOCLAW_RECREATE_SANDBOX: "1" NEMOCLAW_AGENT: ${{ matrix.agent }} + NEMOCLAW_E2E_SHARD: ${{ matrix.agent }} NEMOCLAW_SANDBOX_NAME: e2e-bedrock-${{ matrix.agent }} OPENSHELL_GATEWAY: "nemoclaw" steps: diff --git a/test/e2e/support/e2e-workflow.test.ts b/test/e2e/support/e2e-workflow.test.ts index b2256a093ac..85f5ac96e6b 100644 --- a/test/e2e/support/e2e-workflow.test.ts +++ b/test/e2e/support/e2e-workflow.test.ts @@ -37,6 +37,24 @@ describe("e2e workflow boundary", () => { expect(validateE2eWorkflowBoundary()).toEqual([]); }); + it("rejects Bedrock matrix shard identity drift (#6938)", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-bedrock-shard-workflow-")); + const workflowPath = path.join(tmp, "workflow.yaml"); + const workflow = readWorkflow() as { + jobs: Record }>; + }; + delete workflow.jobs["bedrock-runtime-compatible-anthropic"].env.NEMOCLAW_E2E_SHARD; + fs.writeFileSync(workflowPath, YAML.stringify(workflow)); + + try { + expect(validateE2eWorkflowBoundary(workflowPath)).toContain( + "bedrock-runtime-compatible-anthropic job must pass matrix.agent through NEMOCLAW_E2E_SHARD", + ); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + it("requires unknown inference modes to be rejected before planning", () => { const workflow = readWorkflow() as { jobs: Record }>; diff --git a/test/pr-e2e-gate-shards.test.ts b/test/pr-e2e-gate-shards.test.ts index 20944158d08..67cf027096a 100644 --- a/test/pr-e2e-gate-shards.test.ts +++ b/test/pr-e2e-gate-shards.test.ts @@ -25,6 +25,12 @@ function temporaryE2eWorkflow(source: string): string { } describe("PR E2E shard policy", () => { + it("derives Bedrock signal shards from the agent matrix (#6938)", () => { + expect(expectedSignalShards(["bedrock-runtime-compatible-anthropic"])).toEqual({ + "bedrock-runtime-compatible-anthropic": ["openclaw", "hermes"], + }); + }); + // source-shape-contract: security -- Malformed matrix shard selectors must fail closed before exact-SHA evidence dispatch it("rejects malformed configured matrix shard selectors", () => { const workflow = fs.readFileSync(".github/workflows/e2e.yaml", "utf8"); diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index 87a1260f0e9..2f8a6e2974f 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -133,8 +133,10 @@ function requiredWorkflowStepIndex(job: WorkflowJob, stepName: string): number { function runWorkflowShellStep( step: WorkflowStep, env: Record, + cwd = process.cwd(), ): { status: number | null; stdout: string; stderr: string } { const result = spawnSync("bash", ["-c", step.run ?? ""], { + cwd, encoding: "utf8", env: { ...process.env, ...step.env, ...env }, timeout: 5_000, @@ -1019,6 +1021,82 @@ describe("pull request and main workflow contracts", () => { } }); + it("keeps trusted coverage actions compatible across the .ts to .mts migration (#6935)", () => { + const cases = [ + { + action: sharedActions.cliCoverageShard, + step: "Build CLI for coverage shard", + stem: "scripts/check-dist-sourcemaps", + }, + { + action: sharedActions.cliCoverageMerge, + step: "Verify compiled CLI artifact", + stem: "scripts/check-dist-sourcemaps", + }, + { + action: sharedActions.cliCoverageMerge, + step: "Merge CLI coverage", + stem: "scripts/check-coverage-ratchet", + }, + { + action: sharedActions.pluginCoverage, + step: "Run plugin coverage", + stem: "scripts/check-coverage-ratchet", + }, + ] as const; + const variants = ["mts", "ts", "missing"] as const; + + for (const testCase of cases) { + for (const variant of variants) { + const temp = mkdtempSync(join(tmpdir(), "nemoclaw-coverage-entrypoint-")); + const fakeBin = join(temp, "bin"); + mkdirSync(fakeBin); + mkdirSync(join(temp, "dist")); + mkdirSync(join(temp, "scripts")); + writeFileSync(join(temp, "dist", ["nemoclaw", "js"].join(".")), "built\n"); + for (const command of ["node", "npm"]) { + writeFileSync(join(fakeBin, command), "#!/usr/bin/env bash\nexit 0\n", { + mode: 0o755, + }); + } + writeFileSync( + join(fakeBin, "npx"), + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + 'if [ "${1:-}" = "tsx" ] && [[ "${2:-}" == scripts/check-* ]]; then', + ' test "${2}" = "${EXPECTED_ENTRYPOINT}"', + ' test -f "${2}"', + "fi", + ].join("\n"), + { mode: 0o755 }, + ); + if (variant !== "missing") { + writeFileSync(join(temp, `${testCase.stem}.${variant}`), "// fixture\n"); + } + + try { + const result = runWorkflowShellStep( + requiredStep(testCase.action, testCase.step), + { + EXPECTED_ENTRYPOINT: `${testCase.stem}.${variant === "missing" ? "ts" : variant}`, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + }, + temp, + ); + + if (variant === "missing") { + expect(result.status).not.toBe(0); + } else { + expect(result.status, result.stderr).toBe(0); + } + } finally { + rmSync(temp, { force: true, recursive: true }); + } + } + } + }); + // source-shape-contract: security -- Growth-budget changes must inspect trusted GitHub data without fetching PR-authored URLs it("keeps the trusted test-size guard closed around budget policy changes", () => { const growthGuardrails = readYaml( diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index aee51d3f48e..8681ac60d7a 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -447,8 +447,7 @@ function requireDockerEngineRebuilds( const hasSeparateCacheBuilder = steps.some((step) => { const uses = stringValue(step.uses); return ( - uses.startsWith("docker/setup-buildx-action@") || - uses.startsWith("docker/build-push-action@") + uses.startsWith("docker/setup-buildx-action@") || uses.startsWith("docker/build-push-action@") ); }); const routesBuildsAwayFromDocker = steps.some((step) => { @@ -1336,7 +1335,6 @@ function validateRebuildOpenClawJob(errors: string[], jobs: WorkflowRecord): voi requireRunContains(errors, runVitest, "OPENSHELL_BIN"); requireRunContains(errors, runVitest, "npx vitest run --project e2e-live"); requireRunContains(errors, runVitest, "test/e2e/live/rebuild-openclaw.test.ts"); - } function validateRebuildHermesJob( @@ -1442,7 +1440,6 @@ function validateRebuildHermesJob( } requireRunContains(errors, runVitest, "npx vitest run --project e2e-live"); requireRunContains(errors, runVitest, "test/e2e/live/rebuild-hermes.test.ts"); - } function validateSandboxRebuildJob(errors: string[], jobs: WorkflowRecord): void { @@ -3481,6 +3478,11 @@ function validateBedrockRuntimeCompatibleAnthropicJob( "bedrock-runtime-compatible-anthropic job must pass matrix.agent through NEMOCLAW_AGENT", ); } + if (jobEnv.NEMOCLAW_E2E_SHARD !== "${{ matrix.agent }}") { + errors.push( + "bedrock-runtime-compatible-anthropic job must pass matrix.agent through NEMOCLAW_E2E_SHARD", + ); + } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-bedrock-${{ matrix.agent }}") { errors.push( "bedrock-runtime-compatible-anthropic job must derive NEMOCLAW_SANDBOX_NAME from matrix.agent", @@ -3976,7 +3978,9 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { Object.hasOwn(asRecord(dcodeProfileImportGate?.env), "BUILDX_BUILDER") || routesDcodeBuildsThroughBuildx ) { - errors.push("live DCode profile import gate must keep its local image chain on the Docker engine"); + errors.push( + "live DCode profile import gate must keep its local image chain on the Docker engine", + ); } const runVitest = requireStep(errors, steps, "Run live E2E tests"); From 1c48bbc50af972f1d33fef3094431266ae13a891 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 15 Jul 2026 11:57:07 -0700 Subject: [PATCH 2/4] test(ci): keep coverage selector cases linear Signed-off-by: Carlos Villela --- test/pr-workflow-contract.test.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index 2f8a6e2974f..f6d265634b3 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -1044,7 +1044,23 @@ describe("pull request and main workflow contracts", () => { stem: "scripts/check-coverage-ratchet", }, ] as const; - const variants = ["mts", "ts", "missing"] as const; + const variants = [ + { + fixtureExtension: "mts", + expectedEntrypointExtension: "mts", + expectedStatus: 0, + }, + { + fixtureExtension: "ts", + expectedEntrypointExtension: "ts", + expectedStatus: 0, + }, + { + fixtureExtension: "missing", + expectedEntrypointExtension: "ts", + expectedStatus: 1, + }, + ] as const; for (const testCase of cases) { for (const variant of variants) { @@ -1071,25 +1087,19 @@ describe("pull request and main workflow contracts", () => { ].join("\n"), { mode: 0o755 }, ); - if (variant !== "missing") { - writeFileSync(join(temp, `${testCase.stem}.${variant}`), "// fixture\n"); - } + writeFileSync(join(temp, `${testCase.stem}.${variant.fixtureExtension}`), "// fixture\n"); try { const result = runWorkflowShellStep( requiredStep(testCase.action, testCase.step), { - EXPECTED_ENTRYPOINT: `${testCase.stem}.${variant === "missing" ? "ts" : variant}`, + EXPECTED_ENTRYPOINT: `${testCase.stem}.${variant.expectedEntrypointExtension}`, PATH: `${fakeBin}:${process.env.PATH ?? ""}`, }, temp, ); - if (variant === "missing") { - expect(result.status).not.toBe(0); - } else { - expect(result.status, result.stderr).toBe(0); - } + expect(result.status, result.stderr).toBe(variant.expectedStatus); } finally { rmSync(temp, { force: true, recursive: true }); } From 41b6235115a24196f89cffd3b23700b5fd6df466 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 15 Jul 2026 12:17:57 -0700 Subject: [PATCH 3/4] test(e2e): validate Bedrock shard selection Signed-off-by: Carlos Villela --- .../bedrock-runtime-compatible-anthropic.test.ts | 5 +++++ test/pr-risk-plan.test.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts b/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts index 48da2c017aa..d40396e7902 100644 --- a/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts +++ b/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts @@ -1307,6 +1307,11 @@ test("bedrock runtime compatible Anthropic endpoint routes through managed infer timeout: TEST_TIMEOUT_MS, }, async ({ artifacts, cleanup, host, sandbox, secrets, skip }) => { assertAgent(AGENT); + const shard = + process.env.GITHUB_ACTIONS === "true" + ? process.env.NEMOCLAW_E2E_SHARD + : (process.env.NEMOCLAW_E2E_SHARD ?? AGENT); + expect(shard).toBe(AGENT); validateSandboxName(SANDBOX_NAME); const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-bedrock-runtime-home-")); diff --git a/test/pr-risk-plan.test.ts b/test/pr-risk-plan.test.ts index e94050c53b4..61476bb7e2e 100644 --- a/test/pr-risk-plan.test.ts +++ b/test/pr-risk-plan.test.ts @@ -84,6 +84,20 @@ describe("deterministic PR risk plan", () => { expect(result.planHash).not.toBe(withoutFocusedSelection.planHash); }); + it("selects the Bedrock matrix when its live shard contract changes (#6938)", () => { + const changedFiles = ["test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts"]; + const focusedE2eJobs = focusedE2eJobsForChangedFiles(changedFiles); + const result = buildRiskPlan({ headSha: HEAD_SHA, changedFiles, focusedE2eJobs }); + + expect(focusedE2eJobs).toEqual([ + { + id: "bedrock-runtime-compatible-anthropic", + matchedFiles: changedFiles, + }, + ]); + expect(riskPlanRequiredJobIds(result)).toContain("bedrock-runtime-compatible-anthropic"); + }); + it("does not infer security or inference risk from unrelated path substrings", () => { const result = plan("src/lib/actions/sandbox/mcp-bridge-provider.ts", "src/lib/secretary.ts"); From 43330e629f1cdd1f76cf4c9cb507bae394a64891 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 15 Jul 2026 12:33:23 -0700 Subject: [PATCH 4/4] test(e2e): respect trusted workflow bootstrap Signed-off-by: Carlos Villela --- .../bedrock-runtime-compatible-anthropic.test.ts | 5 ----- test/pr-risk-plan.test.ts | 14 -------------- 2 files changed, 19 deletions(-) diff --git a/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts b/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts index d40396e7902..48da2c017aa 100644 --- a/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts +++ b/test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts @@ -1307,11 +1307,6 @@ test("bedrock runtime compatible Anthropic endpoint routes through managed infer timeout: TEST_TIMEOUT_MS, }, async ({ artifacts, cleanup, host, sandbox, secrets, skip }) => { assertAgent(AGENT); - const shard = - process.env.GITHUB_ACTIONS === "true" - ? process.env.NEMOCLAW_E2E_SHARD - : (process.env.NEMOCLAW_E2E_SHARD ?? AGENT); - expect(shard).toBe(AGENT); validateSandboxName(SANDBOX_NAME); const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-bedrock-runtime-home-")); diff --git a/test/pr-risk-plan.test.ts b/test/pr-risk-plan.test.ts index 61476bb7e2e..e94050c53b4 100644 --- a/test/pr-risk-plan.test.ts +++ b/test/pr-risk-plan.test.ts @@ -84,20 +84,6 @@ describe("deterministic PR risk plan", () => { expect(result.planHash).not.toBe(withoutFocusedSelection.planHash); }); - it("selects the Bedrock matrix when its live shard contract changes (#6938)", () => { - const changedFiles = ["test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts"]; - const focusedE2eJobs = focusedE2eJobsForChangedFiles(changedFiles); - const result = buildRiskPlan({ headSha: HEAD_SHA, changedFiles, focusedE2eJobs }); - - expect(focusedE2eJobs).toEqual([ - { - id: "bedrock-runtime-compatible-anthropic", - matchedFiles: changedFiles, - }, - ]); - expect(riskPlanRequiredJobIds(result)).toContain("bedrock-runtime-compatible-anthropic"); - }); - it("does not infer security or inference risk from unrelated path substrings", () => { const result = plan("src/lib/actions/sandbox/mcp-bridge-provider.ts", "src/lib/secretary.ts");