mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ci): declare repo e2e shard capability
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
@@ -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" },
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user