mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 08:50:21 +08:00
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:
committed by
GitHub
parent
02560243f3
commit
7ae5996bb3
176
.github/workflows/ci.yml
vendored
176
.github/workflows/ci.yml
vendored
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"',
|
||||
|
||||
Reference in New Issue
Block a user