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..f6d265634b3 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,92 @@ 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 = [ + { + 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) { + 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 }, + ); + writeFileSync(join(temp, `${testCase.stem}.${variant.fixtureExtension}`), "// fixture\n"); + + try { + const result = runWorkflowShellStep( + requiredStep(testCase.action, testCase.step), + { + EXPECTED_ENTRYPOINT: `${testCase.stem}.${variant.expectedEntrypointExtension}`, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + }, + temp, + ); + + expect(result.status, result.stderr).toBe(variant.expectedStatus); + } 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");