fix(ci): declare repo e2e shard capability

This commit is contained in:
Vincent Koc
2026-08-21 02:42:15 -07:00
parent ee923af4dd
commit a190f4d5c2
8 changed files with 378 additions and 65 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"schema": "openclaw.repo-e2e-capabilities/v1",
"schemaVersion": 1,
"repoE2eShards": {
"agentPluginGateway": true,
"gatewayShards": 4,
"realGatewayUi": true,
"runtimeBuildProfile": "repoE2eRuntime",
"sandboxArtifact": true,
"uiShards": 4
}
}
@@ -544,7 +544,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 30
outputs:
repo_e2e_shards_available: ${{ steps.validate.outputs.repo_e2e_shards_available }}
repo_e2e_shards_available: ${{ steps.repo_e2e.outputs.repo_e2e_shards_available }}
selected_sha: ${{ steps.validate.outputs.selected_sha }}
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
workflow_repository: ${{ steps.workflow.outputs.workflow_repository }}
@@ -587,6 +587,15 @@ jobs:
with:
fetch-depth: 0
- name: Checkout trusted repo E2E capability validator
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ steps.workflow.outputs.workflow_repository }}
ref: ${{ steps.workflow.outputs.workflow_sha }}
path: .release-harness
fetch-depth: 1
persist-credentials: false
- name: Validate selected ref
id: validate
env:
@@ -617,7 +626,6 @@ jobs:
SHARED_IMAGE_ARTIFACT_RUN_ID: ${{ inputs.shared_image_artifact_run_id }}
SHARED_IMAGE_ARCHIVE_SHA256: ${{ inputs.shared_image_archive_sha256 }}
SHARED_IMAGE_POLICY: ${{ inputs.shared_image_policy }}
WORKFLOW_SHA: ${{ steps.workflow.outputs.workflow_sha }}
shell: bash
run: |
set -euo pipefail
@@ -768,49 +776,37 @@ jobs:
;;
esac
repo_e2e_shards_available=true
if [[ "$selected_sha" != "$WORKFLOW_SHA" ]]; then
repo_e2e_shards_available=false
fi
required_repo_e2e_paths=(
scripts/agent-plugin-gateway-e2e.ts
scripts/build-all.mts
scripts/ensure-playwright-chromium.mts
test/vitest/vitest.e2e.config.ts
test/vitest/vitest.e2e.global-setup.ts
test/vitest/vitest.ui-e2e.config.ts
test/vitest/vitest.ui-e2e.global-setup.ts
test/vitest/vitest.ui-e2e.sequencer.ts
)
for required_path in "${required_repo_e2e_paths[@]}"; do
if ! git cat-file -e "${selected_sha}:${required_path}" 2>/dev/null; then
repo_e2e_shards_available=false
break
fi
done
if [[ "$repo_e2e_shards_available" == "true" ]] &&
! git grep -Fq 'repoE2eRuntime:' "$selected_sha" -- scripts/build-all.mts; then
repo_e2e_shards_available=false
fi
if [[ "$repo_e2e_shards_available" == "true" ]] &&
! git grep -Fq 'OPENCLAW_E2E_USE_PREBUILT_DIST' "$selected_sha" -- test/vitest/vitest.e2e.global-setup.ts; then
repo_e2e_shards_available=false
fi
if [[ "$repo_e2e_shards_available" == "true" ]] &&
! git grep -Fq 'OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY' "$selected_sha" -- test/vitest/vitest.ui-e2e.config.ts; then
repo_e2e_shards_available=false
fi
echo "repo_e2e_shards_available=$repo_e2e_shards_available" >> "$GITHUB_OUTPUT"
echo "selected_sha=$selected_sha" >> "$GITHUB_OUTPUT"
echo "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"
{
echo "Validated ref: \`${INPUT_REF}\`"
echo "Resolved SHA: \`$selected_sha\`"
echo "Trust reason: \`$trusted_reason\`"
echo "Repo E2E mode: \`$([[ "$repo_e2e_shards_available" == "true" ]] && echo sharded || echo legacy)\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Resolve repo E2E capability
id: repo_e2e
env:
CAPABILITY_PATH: .github/repo-e2e-capabilities.json
SELECTED_SHA: ${{ steps.validate.outputs.selected_sha }}
shell: bash
run: |
set -euo pipefail
manifest_path="${RUNNER_TEMP}/repo-e2e-capabilities.json"
rm -f "$manifest_path"
if git cat-file -e "${SELECTED_SHA}:${CAPABILITY_PATH}" 2>/dev/null; then
manifest_size="$(git cat-file -s "${SELECTED_SHA}:${CAPABILITY_PATH}")"
if [[ "$manifest_size" =~ ^[0-9]+$ && "$manifest_size" -le 8192 ]]; then
git show "${SELECTED_SHA}:${CAPABILITY_PATH}" > "$manifest_path"
else
printf '{"invalid":"oversized"}\n' > "$manifest_path"
fi
fi
node .release-harness/scripts/validate-repo-e2e-capability.mjs \
--manifest "$manifest_path" \
--github-output "$GITHUB_OUTPUT" \
--github-step-summary "$GITHUB_STEP_SUMMARY"
validate_live_suite_filter:
runs-on: ubuntu-24.04
if: inputs.live_suite_filter != ''
@@ -1039,6 +1035,7 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
build-all-cache-scope: full
@@ -1055,8 +1052,9 @@ jobs:
WORKFLOW_SHA: ${{ needs.validate_selected_ref.outputs.workflow_sha }}
run: |
set -euo pipefail
artifact_name="repo-e2e-runtime-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
archive_name="${artifact_name}.tar.zst"
artifact_stem="repo-e2e-runtime-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
archive_name="${artifact_stem}.tar.zst"
artifact_name="$archive_name"
archive_path="${RUNNER_TEMP}/${archive_name}"
bundle_root="${RUNNER_TEMP}/repo-e2e-runtime-bundle"
archive_root="repo-e2e-runtime"
@@ -1235,6 +1233,7 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
@@ -1249,7 +1248,7 @@ jobs:
TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
run: |
set -euo pipefail
expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}"
expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst"
[[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || {
echo "Repo E2E runtime artifact name does not match the target and producer run attempt." >&2
exit 1
@@ -1396,6 +1395,7 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
@@ -1410,7 +1410,7 @@ jobs:
TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
run: |
set -euo pipefail
expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}"
expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst"
[[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || {
echo "Repo E2E runtime artifact name does not match the target and producer run attempt." >&2
exit 1
@@ -1501,6 +1501,7 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
@@ -1560,6 +1561,7 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
@@ -1574,7 +1576,7 @@ jobs:
TARGET_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
run: |
set -euo pipefail
expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}"
expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst"
[[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || {
echo "Repo E2E runtime artifact name does not match the target and producer run attempt." >&2
exit 1
+5 -6
View File
@@ -341,13 +341,17 @@ export const BUILD_ALL_PROFILES: Record<string, string[]> = {
],
repoE2eRuntime: [
"plugins:assets:build",
"tsdown",
"tsdown-ai",
"tsdown-packages",
"tsdown-unified",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
"build-stamp",
"runtime-postbuild-stamp",
"write-plugin-sdk-entry-dts",
"check-plugin-sdk-exports",
"ui:build",
],
sourcePerformance: [
@@ -405,11 +409,6 @@ export const BUILD_ALL_PROFILE_STEP_ENV: Record<string, Record<string, NodeJS.Pr
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
},
},
repoE2eRuntime: {
tsdown: {
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
},
},
sourcePerformance: {
tsdown: {
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
+2 -1
View File
@@ -146,7 +146,8 @@ verify_uploaded_artifact() {
if [[ -z "${artifact_name// }" || "$artifact_name" == *$'\n'* || "$artifact_name" == *$'\r'* ]]; then
fail "$artifact_label artifact name is missing or invalid."
fi
if [[ "$artifact_name" != *"-${artifact_run_id}-${artifact_run_attempt}" ]]; then
local producer_suffix="-${artifact_run_id}-${artifact_run_attempt}"
if [[ "$artifact_name" != *"$producer_suffix" && "$artifact_name" != *"${producer_suffix}.tar.zst" ]]; then
fail "$artifact_label artifact name does not bind the producer run attempt."
fi
if [[ ! "${GITHUB_REPOSITORY:-}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
import { appendFileSync, lstatSync, readFileSync } from "node:fs";
const MAX_MANIFEST_BYTES = 8 * 1024;
const EXPECTED_SCHEMA = "openclaw.repo-e2e-capabilities/v1";
const EXPECTED_ROOT_KEYS = ["repoE2eShards", "schema", "schemaVersion"];
const EXPECTED_SHARD_KEYS = [
"agentPluginGateway",
"gatewayShards",
"realGatewayUi",
"runtimeBuildProfile",
"sandboxArtifact",
"uiShards",
];
function parseArgs(argv) {
const values = new Map();
for (let index = 0; index < argv.length; index += 2) {
const name = argv[index];
const value = argv[index + 1];
if (!name?.startsWith("--") || value === undefined) {
throw new Error(
"usage: validate-repo-e2e-capability.mjs --manifest <path> --github-output <path> --github-step-summary <path>",
);
}
values.set(name, value);
}
for (const required of ["--manifest", "--github-output", "--github-step-summary"]) {
if (!values.has(required)) {
throw new Error(`${required} is required`);
}
}
return values;
}
function hasExactKeys(value, expectedKeys) {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const actualKeys = Object.keys(value).toSorted();
return (
actualKeys.length === expectedKeys.length &&
actualKeys.every((key, index) => key === expectedKeys[index])
);
}
function validateManifest(manifestPath) {
let stat;
try {
stat = lstatSync(manifestPath);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return { available: false, reason: "missing" };
}
throw error;
}
if (!stat.isFile() || stat.size > MAX_MANIFEST_BYTES) {
return { available: false, reason: "invalid" };
}
let value;
try {
value = JSON.parse(readFileSync(manifestPath, "utf8"));
} catch {
return { available: false, reason: "invalid" };
}
if (!hasExactKeys(value, EXPECTED_ROOT_KEYS)) {
return { available: false, reason: "invalid" };
}
if (value.schema !== EXPECTED_SCHEMA || value.schemaVersion !== 1) {
return { available: false, reason: "unsupported" };
}
if (!hasExactKeys(value.repoE2eShards, EXPECTED_SHARD_KEYS)) {
return { available: false, reason: "invalid" };
}
const capability = value.repoE2eShards;
const valid =
capability.agentPluginGateway === true &&
capability.gatewayShards === 4 &&
capability.realGatewayUi === true &&
capability.runtimeBuildProfile === "repoE2eRuntime" &&
capability.sandboxArtifact === true &&
capability.uiShards === 4;
return valid
? { available: true, reason: "contract-v1" }
: { available: false, reason: "unsupported" };
}
const args = parseArgs(process.argv.slice(2));
const result = validateManifest(args.get("--manifest"));
const mode = result.available ? "sharded" : "legacy";
appendFileSync(
args.get("--github-output"),
`repo_e2e_shards_available=${result.available}\nrepo_e2e_capability_reason=${result.reason}\n`,
);
appendFileSync(
args.get("--github-step-summary"),
`Repo E2E mode: \`${mode}\`\nRepo E2E capability: \`${result.reason}\`\n`,
);
+31 -8
View File
@@ -452,7 +452,6 @@ describe("resolveBuildAllSteps", () => {
for (const profile of [
"gatewayWatch",
"qaRuntime",
"repoE2eRuntime",
"sourcePerformance",
"cliStartup",
] as const) {
@@ -474,6 +473,20 @@ describe("resolveBuildAllSteps", () => {
}
});
it("keeps repository E2E runtime declarations for package compatibility proofs", () => {
const steps = resolveBuildAllSteps("repoE2eRuntime");
expect(steps.map((step) => step.label)).toEqual(
expect.arrayContaining([
"tsdown-ai",
"tsdown-packages",
"tsdown-unified",
"write-plugin-sdk-entry-dts",
"check-plugin-sdk-exports",
]),
);
expect(steps.some((step) => step.label === "tsdown")).toBe(false);
});
it("skips global declarations on CI artifacts and self-builds the plugin-sdk gate", () => {
// Global dts emission is ~95% of the tsdown wall clock; PR CI dist
// consumers are runtime JS, and the plugin-sdk export gate validates
@@ -552,7 +565,7 @@ describe("resolveBuildAllSteps", () => {
});
}
for (const profile of ["gatewayWatch", "qaRuntime", "repoE2eRuntime"]) {
for (const profile of ["gatewayWatch", "qaRuntime"]) {
const tsdown = resolveBuildAllSteps(profile).find((step) => step.label === "tsdown");
if (!tsdown) {
throw new Error(`Missing ${profile} tsdown step`);
@@ -562,6 +575,16 @@ describe("resolveBuildAllSteps", () => {
"OPENCLAW_PRESERVE_CLI_STARTUP_METADATA",
);
}
const repoE2eTsdown = resolveBuildAllSteps("repoE2eRuntime").find(
(step) => step.label === "tsdown-unified",
);
if (!repoE2eTsdown) {
throw new Error("Missing repoE2eRuntime tsdown-unified step");
}
expect(resolveBuildAllStep(repoE2eTsdown, { env: {} }).options.env).not.toHaveProperty(
"OPENCLAW_PRESERVE_CLI_STARTUP_METADATA",
);
});
it("uses a minimal built runtime profile for gateway watch regression", () => {
@@ -591,13 +614,17 @@ describe("resolveBuildAllSteps", () => {
it("uses a repo E2E runtime profile with private QA runtime and Control UI assets", () => {
expect(resolveBuildAllSteps("repoE2eRuntime").map((step) => step.label)).toEqual([
"plugins:assets:build",
"tsdown",
"tsdown-ai",
"tsdown-packages",
"tsdown-unified",
"external-plugins:local-dist",
"check-cli-bootstrap-imports",
"plugins:assets:copy",
"runtime-postbuild",
"build-stamp",
"runtime-postbuild-stamp",
"write-plugin-sdk-entry-dts",
"check-plugin-sdk-exports",
"ui:build",
]);
});
@@ -664,11 +691,7 @@ describe("resolveBuildAllSteps", () => {
throw new Error(`Missing ${profile} runtime-postbuild step`);
}
expect(
expectDefined(BUILD_ALL_PROFILE_STEP_ENV[profile], `${profile} build step env`)[
"runtime-postbuild"
],
).toBeUndefined();
expect(BUILD_ALL_PROFILE_STEP_ENV[profile]?.["runtime-postbuild"]).toBeUndefined();
expect(
resolveBuildAllStep(runtimePostbuild, {
env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "1" },
+164 -8
View File
@@ -4135,17 +4135,38 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
const validateStep = validateRef.steps.find(
(step: WorkflowStep) => step.name === "Validate selected ref",
);
const capabilityCheckout = validateRef.steps.find(
(step: WorkflowStep) => step.name === "Checkout trusted repo E2E capability validator",
);
const capabilityStep = validateRef.steps.find(
(step: WorkflowStep) => step.name === "Resolve repo E2E capability",
);
expect(validateRef.outputs.repo_e2e_shards_available).toBe(
"${{ steps.validate.outputs.repo_e2e_shards_available }}",
"${{ steps.repo_e2e.outputs.repo_e2e_shards_available }}",
);
expect(validateStep.run).toContain("required_repo_e2e_paths=(");
expect(validateStep.env.WORKFLOW_SHA).toBe("${{ steps.workflow.outputs.workflow_sha }}");
expect(validateStep.run).toContain('if [[ "$selected_sha" != "$WORKFLOW_SHA" ]]');
expect(validateStep.run).toContain("repoE2eRuntime:");
expect(validateStep.run).toContain("OPENCLAW_E2E_USE_PREBUILT_DIST");
expect(validateStep.run).toContain("OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY");
expect(validateStep.run).toContain(
expect(capabilityCheckout?.with).toMatchObject({
repository: "${{ steps.workflow.outputs.workflow_repository }}",
ref: "${{ steps.workflow.outputs.workflow_sha }}",
path: ".release-harness",
"persist-credentials": false,
});
expect(capabilityStep?.env).toMatchObject({
CAPABILITY_PATH: ".github/repo-e2e-capabilities.json",
SELECTED_SHA: "${{ steps.validate.outputs.selected_sha }}",
});
expect(capabilityStep?.run).toContain(
"node .release-harness/scripts/validate-repo-e2e-capability.mjs",
);
expect(capabilityStep?.run).toContain('git show "${SELECTED_SHA}:${CAPABILITY_PATH}"');
expect(capabilityStep?.run).not.toContain("WORKFLOW_SHA");
expect(validateStep.run).not.toContain("required_repo_e2e_paths=(");
expect(validateStep.env).not.toHaveProperty("WORKFLOW_SHA");
expect(validateStep.run).not.toContain('if [[ "$selected_sha" != "$WORKFLOW_SHA" ]]');
expect(validateStep.run).not.toContain("repoE2eRuntime:");
expect(validateStep.run).not.toContain("OPENCLAW_E2E_USE_PREBUILT_DIST");
expect(validateStep.run).not.toContain("OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY");
expect(validateStep.run).not.toContain(
'echo "repo_e2e_shards_available=$repo_e2e_shards_available" >> "$GITHUB_OUTPUT"',
);
@@ -4180,6 +4201,7 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
expect(packRuntime?.run).toContain('--arg buildCommand "pnpm build repoE2eRuntime"');
expect(packRuntime?.run).toContain("manifest.json");
expect(packRuntime?.run).toContain('archive_sha256=$(sha256sum "$archive_path"');
expect(packRuntime?.run).toContain('artifact_name="$archive_name"');
const uploadRuntime = runtime.steps.find(
(step: WorkflowStep) => step.name === "Upload repo E2E runtime",
);
@@ -4244,6 +4266,13 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
(step: WorkflowStep) => step.name === "Validate repo E2E runtime artifact binding",
)?.run,
).toContain('verify-upload "Repo E2E runtime"');
expect(
job.steps.find(
(step: WorkflowStep) => step.name === "Validate repo E2E runtime artifact binding",
)?.run,
).toContain(
'expected_artifact_name="repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst"',
);
expect(
job.steps.find((step: WorkflowStep) => step.name === "Download repo E2E runtime")?.with,
).toMatchObject({
@@ -4342,6 +4371,34 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
legacy.steps.find((step: WorkflowStep) => step.name === "Run legacy repo E2E suite")?.run,
).toBe("pnpm test:e2e");
for (const [jobName, job] of Object.entries({
prepare_repo_e2e_runtime: runtime,
validate_repo_e2e_gateway: gateway,
validate_repo_e2e_agent_plugin: agentPlugin,
validate_repo_e2e_ui: ui,
validate_repo_e2e_ui_real_gateway: realGateway,
validate_repo_e2e_legacy: legacy,
})) {
expect(
job.steps.find((step: WorkflowStep) => step.name === "Setup Node environment")?.with?.[
"cache-mode"
],
jobName,
).toBe("restore");
}
for (const job of [ui, realGateway]) {
expect(
job.steps.filter((step: WorkflowStep) => step.name === "Cache Playwright Chromium"),
).toEqual([
expect.objectContaining({
uses: "actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae",
}),
]);
expect(
job.steps.some((step: WorkflowStep) => step.uses?.startsWith("actions/cache/save@")),
).toBe(false);
}
const gate = jobs.validate_repo_e2e;
expect(gate.needs).toEqual([
"validate_selected_ref",
@@ -4359,6 +4416,13 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
expect(gate["runs-on"]).toBe("ubuntu-24.04");
expect(gate["timeout-minutes"]).toBe(5);
expect(gate["continue-on-error"]).toBe("${{ inputs.advisory }}");
expect(
gate.steps.some(
(step: WorkflowStep) =>
step.uses === "./.github/actions/setup-node-env" ||
step.uses?.startsWith("actions/cache/"),
),
).toBe(false);
const gateStep = gate.steps.find((step: WorkflowStep) => step.name === "Verify repo E2E lanes");
expect(gateStep.env).toMatchObject({
GATEWAY_RESULT: "${{ needs.validate_repo_e2e_gateway.result }}",
@@ -4371,6 +4435,98 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
expect(gateStep.run).toContain('require_result legacy "$LEGACY_RESULT" success');
});
it("uses the target-owned repo E2E capability across mixed target and tooling SHAs", () => {
const workflow = parse(
readFileSync(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml", "utf8"),
);
const capabilityStep = workflow.jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Resolve repo E2E capability",
);
const root = tempDirs.make("openclaw-repo-e2e-capability-");
runGit(root, ["init", "-q", "-b", "main"]);
runGit(root, ["config", "commit.gpgsign", "false"]);
runGit(root, ["config", "user.email", "ci-fixture@example.com"]);
runGit(root, ["config", "user.name", "CI Fixture"]);
writeFileSync(path.join(root, "fixture.txt"), "legacy\n", "utf8");
runGit(root, ["add", "fixture.txt"]);
runGit(root, ["commit", "-q", "-m", "legacy target"]);
const legacySha = runGit(root, ["rev-parse", "HEAD"]);
mkdirSync(path.join(root, ".github"), { recursive: true });
writeFileSync(
path.join(root, ".github", "repo-e2e-capabilities.json"),
readFileSync(".github/repo-e2e-capabilities.json", "utf8"),
"utf8",
);
runGit(root, ["add", ".github/repo-e2e-capabilities.json"]);
runGit(root, ["commit", "-q", "-m", "capable target"]);
const capableSha = runGit(root, ["rev-parse", "HEAD"]);
writeFileSync(path.join(root, ".github", "repo-e2e-capabilities.json"), "{}\n", "utf8");
runGit(root, ["add", ".github/repo-e2e-capabilities.json"]);
runGit(root, ["commit", "-q", "-m", "invalid target"]);
const invalidSha = runGit(root, ["rev-parse", "HEAD"]);
writeFileSync(path.join(root, "fixture.txt"), "newer tooling\n", "utf8");
runGit(root, ["add", "fixture.txt"]);
runGit(root, ["commit", "-q", "-m", "newer tooling"]);
const toolingSha = runGit(root, ["rev-parse", "HEAD"]);
expect(toolingSha).not.toBe(capableSha);
const harnessScripts = path.join(root, ".release-harness", "scripts");
mkdirSync(harnessScripts, { recursive: true });
writeFileSync(
path.join(harnessScripts, "validate-repo-e2e-capability.mjs"),
readFileSync("scripts/validate-repo-e2e-capability.mjs", "utf8"),
"utf8",
);
const runCapability = (selectedSha: string) => {
const githubOutput = path.join(root, `output-${selectedSha}`);
const githubSummary = path.join(root, `summary-${selectedSha}`);
writeFileSync(githubOutput, "", "utf8");
writeFileSync(githubSummary, "", "utf8");
const run = runWorkflowShellScript(capabilityStep.run, {
cwd: root,
env: {
...process.env,
CAPABILITY_PATH: ".github/repo-e2e-capabilities.json",
GITHUB_OUTPUT: githubOutput,
GITHUB_STEP_SUMMARY: githubSummary,
RUNNER_TEMP: root,
SELECTED_SHA: selectedSha,
WORKFLOW_SHA: toolingSha,
},
});
return {
...run,
outputs: readWorkflowOutputs(githubOutput),
};
};
const capable = runCapability(capableSha);
expect(capable.status, `${capable.stdout}\n${capable.stderr}`).toBe(0);
expect(capable.outputs).toMatchObject({
repo_e2e_capability_reason: "contract-v1",
repo_e2e_shards_available: "true",
});
const legacy = runCapability(legacySha);
expect(legacy.status, `${legacy.stdout}\n${legacy.stderr}`).toBe(0);
expect(legacy.outputs).toMatchObject({
repo_e2e_capability_reason: "missing",
repo_e2e_shards_available: "false",
});
const invalid = runCapability(invalidSha);
expect(invalid.status, `${invalid.stdout}\n${invalid.stderr}`).toBe(0);
expect(invalid.outputs).toMatchObject({
repo_e2e_capability_reason: "invalid",
repo_e2e_shards_available: "false",
});
});
it("persists Node 22 declarations through trusted bounded artifacts", () => {
const workflow = parse(readFileSync(".github/workflows/node22-compat.yml", "utf8"));
const steps = workflow.jobs.compat.steps as WorkflowStep[];
@@ -308,6 +308,25 @@ describe("shared Docker image artifacts", () => {
`api --method GET repos/openclaw/openclaw/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}`,
);
writeFileSync(fixture.ghLog, "");
const rawArchiveName = `repo-e2e-runtime-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}.tar.zst`;
const rawArchive = verifyUploadedArtifact(fixture, {
artifactName: rawArchiveName,
env: { FAKE_ARTIFACT_NAME: rawArchiveName },
});
expect(rawArchive.status, `${rawArchive.stdout}\n${rawArchive.stderr}`).toBe(0);
writeFileSync(fixture.ghLog, "");
const unsupportedSuffix = verifyUploadedArtifact(fixture, {
artifactName: `${ARTIFACT_NAME}.zip`,
env: { FAKE_ARTIFACT_NAME: `${ARTIFACT_NAME}.zip` },
});
expect(unsupportedSuffix.status).not.toBe(0);
expect(unsupportedSuffix.stderr).toContain(
"artifact name does not bind the producer run attempt",
);
expect(readFileSync(fixture.ghLog, "utf8")).toBe("");
writeFileSync(fixture.ghLog, "");
const digestMismatch = verifyUploadedArtifact(fixture, {
artifactDigest: "e".repeat(64),