Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/actions/ci-cli-coverage-merge/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }}
Expand Down
6 changes: 5 additions & 1 deletion .github/actions/ci-cli-coverage-shard/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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() }}
Expand Down
6 changes: 5 additions & 1 deletion .github/actions/ci-plugin-coverage/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/support/e2e-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { env: Record<string, unknown> }>;
};
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<string, { steps?: Array<{ name?: string; run?: string }> }>;
Expand Down
6 changes: 6 additions & 0 deletions test/pr-e2e-gate-shards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
78 changes: 78 additions & 0 deletions test/pr-workflow-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ function requiredWorkflowStepIndex(job: WorkflowJob, stepName: string): number {
function runWorkflowShellStep(
step: WorkflowStep,
env: Record<string, string>,
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,
Expand Down Expand Up @@ -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<CodebaseGrowthGuardrailsWorkflow>(
Expand Down
14 changes: 9 additions & 5 deletions tools/e2e/workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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");
Expand Down
Loading