fix(ci): validate older release targets (#103975)

* fix(ci): support older release targets

* fix(ci): guard optional channel contracts

* fix(ci): scope legacy release compatibility
This commit is contained in:
Peter Steinberger
2026-07-11 05:44:49 -07:00
committed by GitHub
parent 02560243f3
commit 7ae5996bb3
4 changed files with 369 additions and 44 deletions

View File

@@ -23,6 +23,11 @@ on:
required: false
default: ""
type: string
historical_target_tag:
description: Semver release tag authorizing compatibility fallbacks for its exact commit
required: false
default: ""
type: string
push:
branches: [main]
paths-ignore:
@@ -88,9 +93,12 @@ jobs:
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_checks_fast_core: ${{ steps.manifest.outputs.run_checks_fast_core }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
historical_target: ${{ steps.manifest.outputs.historical_target }}
run_qa_smoke_ci: ${{ steps.manifest.outputs.run_qa_smoke_ci }}
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
run_plugin_contracts_shards: ${{ steps.manifest.outputs.run_plugin_contracts_shards }}
plugin_contracts_matrix: ${{ steps.manifest.outputs.plugin_contracts_matrix }}
run_channel_contracts_shards: ${{ steps.manifest.outputs.run_channel_contracts_shards }}
channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_matrix }}
run_checks: ${{ steps.manifest.outputs.run_checks }}
run_checks_node_core_nondist: ${{ steps.manifest.outputs.run_checks_node_core_nondist }}
@@ -109,6 +117,7 @@ jobs:
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
run_ios_build: ${{ steps.manifest.outputs.run_ios_build }}
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
run_protocol_event_coverage: ${{ steps.manifest.outputs.run_protocol_event_coverage }}
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
steps:
- name: Validate release-gate dispatch
@@ -205,6 +214,30 @@ jobs:
id: checkout_ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate historical release target
id: historical_target
if: inputs.historical_target_tag != ''
env:
EXPECTED_SHA: ${{ steps.checkout_ref.outputs.sha }}
HISTORICAL_TARGET_TAG: ${{ inputs.historical_target_tag }}
run: |
set -euo pipefail
if [[ ! "$HISTORICAL_TARGET_TAG" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then
echo "historical_target_tag must be a canonical OpenClaw release tag." >&2
exit 1
fi
tag_ref="refs/tags/${HISTORICAL_TARGET_TAG}"
remote="$(git remote get-url origin)"
tag_sha="$(git ls-remote --tags "$remote" "${tag_ref}^{}" | awk 'NR == 1 { print $1 }')"
if [[ -z "$tag_sha" ]]; then
tag_sha="$(git ls-remote --tags "$remote" "$tag_ref" | awk 'NR == 1 { print $1 }')"
fi
if [[ "$tag_sha" != "$EXPECTED_SHA" ]]; then
echo "Historical release tag ${HISTORICAL_TARGET_TAG} does not resolve to ${EXPECTED_SHA}." >&2
exit 1
fi
echo "eligible=true" >> "$GITHUB_OUTPUT"
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
@@ -250,17 +283,50 @@ jobs:
OPENCLAW_CI_RUN_UI_TESTS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ui_tests || 'false' }}
OPENCLAW_CI_RUN_NATIVE_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_native_i18n || 'false' }}
OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }}
OPENCLAW_CI_HISTORICAL_TARGET: ${{ steps.historical_target.outputs.eligible || 'false' }}
OPENCLAW_CI_WORKFLOW_REVISION: ${{ github.sha }}
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }}
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import {
createNodeTestShardBundles,
} from "./scripts/lib/ci-node-test-plan.mjs";
import {
createChannelContractTestShards,
} from "./scripts/lib/channel-contract-test-plan.mjs";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
const eventName = process.env.OPENCLAW_CI_EVENT_NAME ?? "";
const checkoutRevision = process.env.OPENCLAW_CI_CHECKOUT_REVISION ?? "";
const workflowRevision = process.env.OPENCLAW_CI_WORKFLOW_REVISION ?? "";
const historicalTargetApproved = process.env.OPENCLAW_CI_HISTORICAL_TARGET === "true";
const historicalTarget =
eventName === "workflow_dispatch" &&
historicalTargetApproved &&
checkoutRevision !== workflowRevision;
const nodeTestPlan = await import("./scripts/lib/ci-node-test-plan.mjs");
const createNodeTestPlan =
typeof nodeTestPlan.createNodeTestShardBundles === "function"
? nodeTestPlan.createNodeTestShardBundles
: historicalTarget
? nodeTestPlan.createNodeTestShards
: undefined;
if (typeof createNodeTestPlan !== "function") {
throw new Error("CI target does not export a supported Node test shard planner");
}
const importTargetPlan = async (path) => {
if (existsSync(path)) {
return import(path);
}
if (!historicalTarget) {
throw new Error(`Current CI target does not provide ${path}`);
}
return {};
};
const channelContractPlan = await importTargetPlan(
"./scripts/lib/channel-contract-test-plan.mjs",
);
const createChannelContractTestShards =
typeof channelContractPlan.createChannelContractTestShards === "function"
? channelContractPlan.createChannelContractTestShards
: () => [];
const parseBoolean = (value, fallback = false) => {
if (value === undefined) return fallback;
@@ -270,29 +336,27 @@ jobs:
return fallback;
};
const { createPluginContractTestShards } = await import(
"./scripts/lib/plugin-contract-test-plan.mjs"
).catch((error) => {
if (error?.code !== "ERR_MODULE_NOT_FOUND") {
throw error;
}
return {
createPluginContractTestShards: () => [
{
checkName: "checks-fast-contracts-plugins-legacy",
includePatterns: ["src/plugins/contracts/**/*.test.ts"],
runtime: "node",
task: "contracts-plugins",
},
],
};
});
const pluginContractPlan = await importTargetPlan(
"./scripts/lib/plugin-contract-test-plan.mjs",
);
const createPluginContractTestShards =
typeof pluginContractPlan.createPluginContractTestShards === "function"
? pluginContractPlan.createPluginContractTestShards
: () => [
{
checkName: "checks-fast-contracts-plugins-legacy",
includePatterns: ["src/plugins/contracts/**/*.test.ts"],
runtime: "node",
task: "contracts-plugins",
},
];
const createMatrix = (include) => ({ include });
const outputPath = process.env.GITHUB_OUTPUT;
const packageScripts = JSON.parse(readFileSync("package.json", "utf8")).scripts ?? {};
const hasPackageScript = (name) => typeof packageScripts[name] === "string";
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
const eventName = process.env.OPENCLAW_CI_EVENT_NAME ?? "";
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
const runNodeFastOnly =
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_ONLY);
@@ -304,8 +368,16 @@ jobs:
const runPluginContractShards = runNodeFull || runNodeFastPluginContracts;
const runMacos =
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
const supportsCurrentIosCi =
hasPackageScript("ios:build") &&
existsSync("scripts/install-swift-tools.sh") &&
existsSync("scripts/lint-swift.sh") &&
existsSync("scripts/format-swift.sh");
const runIosBuild =
parseBoolean(process.env.OPENCLAW_CI_RUN_IOS_BUILD) && !docsOnly && isCanonicalRepository;
parseBoolean(process.env.OPENCLAW_CI_RUN_IOS_BUILD) &&
!docsOnly &&
isCanonicalRepository &&
(!historicalTarget || supportsCurrentIosCi);
const runAndroid =
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
const runWindows =
@@ -317,8 +389,14 @@ jobs:
const runControlUiI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
const runUiTests = parseBoolean(process.env.OPENCLAW_CI_RUN_UI_TESTS) && !docsOnly;
const supportsNativeI18n =
hasPackageScript("native:i18n:check") &&
hasPackageScript("android:i18n:check") &&
hasPackageScript("apple:i18n:check");
const runNativeI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_NATIVE_I18N) && !docsOnly;
parseBoolean(process.env.OPENCLAW_CI_RUN_NATIVE_I18N) &&
!docsOnly &&
(!historicalTarget || supportsNativeI18n);
const checksFastCoreTasks = [];
if (runNodeFull) {
checksFastCoreTasks.push(
@@ -336,8 +414,11 @@ jobs:
}
const compactPullRequest = isCanonicalRepository && eventName === "pull_request";
const runQaSmokeCi =
runNodeFull &&
(!historicalTarget || existsSync("extensions/qa-lab/src/ci-smoke-plan.ts"));
const nodeTestShards = runNodeFull
? createNodeTestShardBundles({
? createNodeTestPlan({
includeReleaseOnlyPluginShards: false,
compact: compactPullRequest,
}).map((shard) => ({
@@ -359,6 +440,11 @@ jobs:
: [];
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
const nodeTestDistShards = nodeTestShards.filter((shard) => shard.requires_dist);
const channelContractShards = runNodeFull ? createChannelContractTestShards() : [];
const protocolCoverageRequested = runNode || runIosBuild || runAndroid;
const runProtocolEventCoverage =
protocolCoverageRequested &&
(!historicalTarget || existsSync("scripts/check-protocol-event-coverage.mjs"));
const manifest = {
docs_only: docsOnly,
@@ -371,14 +457,15 @@ jobs:
run_build_artifacts: runNodeFull,
run_checks_fast_core: checksFastCoreTasks.length > 0,
run_checks_fast: runNodeFull,
historical_target: historicalTarget,
run_qa_smoke_ci: runQaSmokeCi,
checks_fast_core_matrix: createMatrix(checksFastCoreTasks),
run_plugin_contracts_shards: runPluginContractShards,
plugin_contracts_matrix: createMatrix(
runPluginContractShards ? createPluginContractTestShards() : [],
),
channel_contracts_matrix: createMatrix(
runNodeFull ? createChannelContractTestShards() : [],
),
run_channel_contracts_shards: channelContractShards.length > 0,
channel_contracts_matrix: createMatrix(channelContractShards),
run_checks: runNodeFull,
run_checks_node_core_nondist: nodeTestNonDistShards.length > 0,
checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards),
@@ -410,6 +497,7 @@ jobs:
run_macos_swift: runMacos,
run_ios_build: runIosBuild,
run_android_job: runAndroid,
run_protocol_event_coverage: runProtocolEventCoverage,
android_matrix: createMatrix(
runAndroid
? [
@@ -432,7 +520,7 @@ jobs:
EOF
- name: Check mobile protocol event coverage
if: ${{ steps.manifest.outputs.run_node == 'true' || steps.manifest.outputs.run_ios_build == 'true' || steps.manifest.outputs.run_android_job == 'true' }}
if: steps.manifest.outputs.run_protocol_event_coverage == 'true'
run: node scripts/check-protocol-event-coverage.mjs
# Run dependency-free security checks in parallel with scope detection so the
@@ -965,7 +1053,7 @@ jobs:
contents: read
name: QA Smoke CI (${{ matrix.name }})
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
if: needs.preflight.outputs.run_qa_smoke_ci == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 60
strategy:
@@ -1118,7 +1206,7 @@ jobs:
contents: read
name: ${{ matrix.checkName }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
if: needs.preflight.outputs.run_channel_contracts_shards == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 60
strategy:
@@ -1331,6 +1419,7 @@ jobs:
- name: Run check shard
env:
HISTORICAL_TARGET: ${{ needs.preflight.outputs.historical_target }}
OPENCLAW_LOCAL_CHECK: "0"
TASK: ${{ matrix.task }}
PR_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}
@@ -1377,7 +1466,12 @@ jobs:
;;
test-types)
pnpm check:test-types
pnpm tsgo:scripts
if pnpm run --silent 2>/dev/null | grep -q '^ tsgo:scripts$'; then
pnpm tsgo:scripts
elif [[ "$HISTORICAL_TARGET" != "true" ]]; then
echo "Current CI targets must provide the tsgo:scripts package script." >&2
exit 1
fi
;;
*)
echo "Unsupported check task: $TASK" >&2
@@ -1878,6 +1972,8 @@ jobs:
if: needs.preflight.outputs.run_macos_swift == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
timeout-minutes: 20
env:
HISTORICAL_TARGET: ${{ needs.preflight.outputs.historical_target }}
steps:
- *platform_checkout_step
@@ -1891,9 +1987,12 @@ jobs:
echo "$swift_tools_dir" >> "$GITHUB_PATH"
"$swift_tools_dir/swiftformat" --version
"$swift_tools_dir/swiftlint" version
else
elif [[ "$HISTORICAL_TARGET" == "true" ]]; then
# Frozen release targets before the pinned installer used Homebrew directly.
brew install xcodegen swiftlint swiftformat
else
echo "Current CI targets must provide scripts/install-swift-tools.sh." >&2
exit 1
fi
- name: Detect Swift toolchain cache key
@@ -1960,10 +2059,13 @@ jobs:
if [[ -x ./scripts/lint-swift.sh && -x ./scripts/format-swift.sh ]]; then
./scripts/lint-swift.sh macos
./scripts/format-swift.sh macos
else
elif [[ "$HISTORICAL_TARGET" == "true" ]]; then
# Frozen release targets before the shared wrappers used these commands directly.
swiftlint lint --config config/swiftlint.yml
swiftformat --lint apps/macos/Sources --config config/swiftformat --exclude '**/OpenClawProtocol,**/HostEnvSecurityPolicy.generated.swift'
else
echo "Current CI targets must provide the Swift lint and format wrappers." >&2
exit 1
fi
- name: Swift build (release)

View File

@@ -545,7 +545,11 @@ jobs:
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-ci"
dispatch_run_name="CI ${dispatch_id}"
dispatch_and_wait ci.yml "$dispatch_run_name" -f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id"
args=(-f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id")
if [[ "$TARGET_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then
args+=(-f historical_target_tag="$TARGET_REF")
fi
dispatch_and_wait ci.yml "$dispatch_run_name" "${args[@]}"
plugin_prerelease:
name: Run plugin prerelease validation

View File

@@ -45,6 +45,127 @@ function readCiWorkflow() {
return parse(readFileSync(".github/workflows/ci.yml", "utf8"));
}
function runCiManifestFixture(options: {
bundledPlanner: boolean;
eventName?: "pull_request" | "workflow_dispatch";
historicalCompatibility?: boolean;
iosCapabilities?: boolean;
}) {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-manifest-"));
try {
const scriptsDir = path.join(root, "scripts", "lib");
mkdirSync(scriptsDir, { recursive: true });
writeFileSync(
path.join(scriptsDir, "ci-node-test-plan.mjs"),
options.bundledPlanner
? `
export const createNodeTestShards = () => [{
checkName: "legacy-node-plan",
configs: ["test/vitest/legacy.config.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "legacy-node-plan",
}];
export const createNodeTestShardBundles = () => [{
checkName: "bundled-node-plan",
configs: ["test/vitest/bundled.config.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "bundled-node-plan",
}];
`
: `
export const createNodeTestShards = () => [{
checkName: "legacy-node-plan",
configs: ["test/vitest/legacy.config.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "legacy-node-plan",
}];
`,
"utf8",
);
const iosCapabilities = options.iosCapabilities ?? options.bundledPlanner;
const packageScripts = options.bundledPlanner
? {
"android:i18n:check": "true",
"apple:i18n:check": "true",
...(iosCapabilities ? { "ios:build": "true" } : {}),
"native:i18n:check": "true",
}
: {};
writeFileSync(
path.join(root, "package.json"),
`${JSON.stringify({ scripts: packageScripts })}\n`,
);
if (options.bundledPlanner) {
writeFileSync(
path.join(scriptsDir, "channel-contract-test-plan.mjs"),
`export const createChannelContractTestShards = () => [{ checkName: "channel-contracts" }];\n`,
);
writeFileSync(
path.join(scriptsDir, "plugin-contract-test-plan.mjs"),
`export const createPluginContractTestShards = () => [{ checkName: "plugin-contracts" }];\n`,
);
const smokePlan = path.join(root, "extensions", "qa-lab", "src", "ci-smoke-plan.ts");
mkdirSync(path.dirname(smokePlan), { recursive: true });
writeFileSync(smokePlan, "export {};\n");
}
if (iosCapabilities) {
for (const name of ["install-swift-tools.sh", "lint-swift.sh", "format-swift.sh"]) {
writeFileSync(path.join(root, "scripts", name), "#!/bin/sh\n");
}
}
const outputPath = path.join(root, "manifest.out");
writeFileSync(outputPath, "", "utf8");
const manifestStep = readCiWorkflow().jobs.preflight.steps.find(
(step: { name?: string }) => step.name === "Build CI manifest",
);
const run = spawnSync("bash", ["-c", manifestStep.run], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
GITHUB_OUTPUT: outputPath,
OPENCLAW_CI_CHECKOUT_REVISION: "a".repeat(40),
OPENCLAW_CI_DOCS_CHANGED: "true",
OPENCLAW_CI_DOCS_ONLY: "false",
OPENCLAW_CI_EVENT_NAME: options.eventName ?? "workflow_dispatch",
OPENCLAW_CI_HISTORICAL_TARGET:
(options.historicalCompatibility ?? true) &&
(options.eventName ?? "workflow_dispatch") === "workflow_dispatch"
? "true"
: "false",
OPENCLAW_CI_REPOSITORY: "openclaw/openclaw",
OPENCLAW_CI_RUN_ANDROID: "true",
OPENCLAW_CI_RUN_CONTROL_UI_I18N: "true",
OPENCLAW_CI_RUN_IOS_BUILD: "true",
OPENCLAW_CI_RUN_MACOS: "true",
OPENCLAW_CI_RUN_NATIVE_I18N: "true",
OPENCLAW_CI_RUN_NODE: "true",
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: "false",
OPENCLAW_CI_RUN_NODE_FAST_ONLY: "false",
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: "false",
OPENCLAW_CI_RUN_SKILLS_PYTHON: "true",
OPENCLAW_CI_RUN_WINDOWS: "true",
OPENCLAW_CI_WORKFLOW_REVISION: "b".repeat(40),
},
});
const outputs = Object.fromEntries(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
return { output: `${run.stdout}${run.stderr}`, outputs, status: run.status };
} finally {
rmSync(root, { force: true, recursive: true });
}
}
function readAndroidReleaseWorkflow() {
return parse(readFileSync(".github/workflows/android-release.yml", "utf8"));
}
@@ -1464,12 +1585,103 @@ describe("ci workflow guards", () => {
).run;
expect(coverageStep.run).toBe("node scripts/check-protocol-event-coverage.mjs");
expect(coverageStep.if).toContain("steps.manifest.outputs.run_node == 'true'");
expect(coverageStep.if).toContain("steps.manifest.outputs.run_ios_build == 'true'");
expect(coverageStep.if).toContain("steps.manifest.outputs.run_android_job == 'true'");
expect(coverageStep.if).toBe("steps.manifest.outputs.run_protocol_event_coverage == 'true'");
expect(checkShardRun).not.toContain("check:protocol-coverage");
});
it("uses target-owned CI plans and capabilities for older release checkouts", () => {
const legacy = runCiManifestFixture({ bundledPlanner: false });
expect(legacy.status, legacy.output).toBe(0);
expect(legacy.outputs.historical_target).toBe("true");
expect(legacy.outputs.run_ios_build).toBe("false");
expect(legacy.outputs.run_native_i18n).toBe("false");
expect(legacy.outputs.run_qa_smoke_ci).toBe("false");
expect(legacy.outputs.run_channel_contracts_shards).toBe("false");
expect(legacy.outputs.run_protocol_event_coverage).toBe("false");
expect(JSON.parse(legacy.outputs.checks_node_core_nondist_matrix).include).toContainEqual(
expect.objectContaining({
check_name: "legacy-node-plan",
shard_name: "legacy-node-plan",
}),
);
const current = runCiManifestFixture({ bundledPlanner: true });
expect(current.status, current.output).toBe(0);
expect(current.outputs.run_ios_build).toBe("true");
expect(current.outputs.run_native_i18n).toBe("true");
expect(current.outputs.run_qa_smoke_ci).toBe("true");
expect(current.outputs.run_channel_contracts_shards).toBe("true");
expect(JSON.parse(current.outputs.checks_node_core_nondist_matrix).include).toContainEqual(
expect.objectContaining({
check_name: "bundled-node-plan",
shard_name: "bundled-node-plan",
}),
);
const currentMissingIos = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
iosCapabilities: false,
});
expect(currentMissingIos.status, currentMissingIos.output).toBe(0);
expect(currentMissingIos.outputs.historical_target).toBe("false");
expect(currentMissingIos.outputs.run_ios_build).toBe("true");
const currentMissingPlanner = runCiManifestFixture({
bundledPlanner: false,
eventName: "pull_request",
});
expect(currentMissingPlanner.status).not.toBe(0);
expect(currentMissingPlanner.output).toContain(
"CI target does not export a supported Node test shard planner",
);
const alternateMissingPlanner = runCiManifestFixture({
bundledPlanner: false,
historicalCompatibility: false,
});
expect(alternateMissingPlanner.status).not.toBe(0);
expect(alternateMissingPlanner.output).toContain(
"CI target does not export a supported Node test shard planner",
);
const workflow = readCiWorkflow();
const historicalTargetStep = workflow.jobs.preflight.steps.find(
(step: { name?: string }) => step.name === "Validate historical release target",
);
expect(historicalTargetStep.if).toBe("inputs.historical_target_tag != ''");
expect(historicalTargetStep.run).toContain('git ls-remote --tags "$remote"');
expect(historicalTargetStep.run).toContain('[[ "$tag_sha" != "$EXPECTED_SHA" ]]');
expect(workflow.jobs["qa-smoke-ci-profile"].if).toBe(
"needs.preflight.outputs.run_qa_smoke_ci == 'true'",
);
expect(workflow.jobs["checks-fast-channel-contracts-shard"].if).toBe(
"needs.preflight.outputs.run_channel_contracts_shards == 'true'",
);
const swiftInstall = workflow.jobs["macos-swift"].steps.find(
(step: { name?: string }) => step.name === "Install XcodeGen / SwiftLint / SwiftFormat",
);
const swiftLint = workflow.jobs["macos-swift"].steps.find(
(step: { name?: string }) => step.name === "Swift lint",
);
expect(swiftInstall.run).toContain("brew install xcodegen swiftlint swiftformat");
expect(workflow.jobs["macos-swift"].env.HISTORICAL_TARGET).toBe(
"${{ needs.preflight.outputs.historical_target }}",
);
expect(swiftInstall.run).toContain('elif [[ "$HISTORICAL_TARGET" == "true" ]]');
expect(swiftLint.run).toContain("swiftlint lint --config config/swiftlint.yml");
expect(swiftLint.run).toContain('elif [[ "$HISTORICAL_TARGET" == "true" ]]');
const checkShard = workflow.jobs["check-shard"].steps.find(
(step: { name?: string }) => step.name === "Run check shard",
);
expect(checkShard.env.HISTORICAL_TARGET).toBe(
"${{ needs.preflight.outputs.historical_target }}",
);
expect(checkShard.run).toContain("pnpm tsgo:scripts");
expect(checkShard.run).toContain('elif [[ "$HISTORICAL_TARGET" != "true" ]]');
});
it("does not rebuild Control UI after build:ci-artifacts", () => {
const workflow = readCiWorkflow();
const buildArtifactSteps = workflow.jobs["build-artifacts"].steps;

View File

@@ -327,6 +327,12 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
required: false,
type: "boolean",
});
expect(workflow.on.workflow_dispatch.inputs.historical_target_tag).toEqual({
default: "",
description: "Semver release tag authorizing compatibility fallbacks for its exact commit",
required: false,
type: "string",
});
expect(manifestEnv).toEqual({
OPENCLAW_CI_CHECKOUT_REVISION: "${{ steps.checkout_ref.outputs.sha }}",
OPENCLAW_CI_DOCS_CHANGED:
@@ -334,6 +340,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
OPENCLAW_CI_DOCS_ONLY:
"${{ github.event_name == 'workflow_dispatch' && 'false' || steps.docs_scope.outputs.docs_only }}",
OPENCLAW_CI_EVENT_NAME: "${{ github.event_name }}",
OPENCLAW_CI_HISTORICAL_TARGET: "${{ steps.historical_target.outputs.eligible || 'false' }}",
OPENCLAW_CI_REPOSITORY: "${{ github.repository }}",
OPENCLAW_CI_RUN_ANDROID:
"${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
@@ -359,6 +366,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
"${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ui_tests || 'false' }}",
OPENCLAW_CI_RUN_WINDOWS:
"${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}",
OPENCLAW_CI_WORKFLOW_REVISION: "${{ github.sha }}",
});
expect(manifestEnv).not.toHaveProperty("OPENCLAW_CI_FULL_RELEASE_VALIDATION");
expect(manifestScript).toContain("includeReleaseOnlyPluginShards: false");
@@ -375,9 +383,8 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
expect(
workflow.jobs["check-shard"].steps.find((step) => step.name === "Run check shard").run,
).toContain("pnpm deadcode:ci");
expect(normalCiScript).toContain(
'dispatch_and_wait ci.yml "$dispatch_run_name" -f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id"',
);
expect(normalCiScript).toContain('args+=(-f historical_target_tag="$TARGET_REF")');
expect(normalCiScript).toContain('dispatch_and_wait ci.yml "$dispatch_run_name" "${args[@]}"');
expect(normalCiScript).not.toContain("full_release_validation=true");
expect(pluginPrereleaseScript).toContain(
'dispatch_and_wait plugin-prerelease.yml "$dispatch_run_name" -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id"',