improve(ci): cut hybrid compact runner overhead (#124622)

* perf(ci): refit compact planner hints for Blacksmith

* test(ci): type default runner backend fixture
This commit is contained in:
Peter Steinberger
2026-08-16 07:12:32 -07:00
committed by GitHub
parent 1b34939f82
commit 3e2edc7f22
4 changed files with 166 additions and 88 deletions
+6 -2
View File
@@ -227,7 +227,11 @@ Hybrid is the normal degraded-capacity mode. If Blacksmith is down: rerun the fa
gh variable set OPENCLAW_CI_RUNNER_BACKEND --repo openclaw/openclaw --body github
```
Hosted paths use the same setup exercised by manual dispatches and fork pull requests. Blacksmith-only Docker and sticky-disk steps are skipped, dependency setup uses the ordinary Actions pnpm-store cache, and low-memory Android builds use separate Gradle processes. Hybrid attempt-1 Node and plateau lanes deliberately keep this backend-neutral Actions-cache profile when they run on Blacksmith because preflight remains hosted. The exact workspace dependency cache intentionally stays off when preflight is hosted: GitHub can roll the runner image and Node patch between preflight and fanout in one workflow, while the safe exact key then misses; the ordinary store archive is only slightly smaller and pnpm's measured relink is already single-digit seconds. Vitest transform and Node compile caches still use the upstream Actions cache API, which Blacksmith proxies; their Linux-only `runner.os != 'Windows'` conditions do not exclude Blacksmith labels, and the planner still elects one semantic transform-cache writer. Core oxlint splits into five deterministic hosted stripes, with extension/scripts lint and optional UI/format checks in the existing `check-lint` row. The 14 serial core test-type graphs likewise split into five `check-test-types-core-*` stripes (two overlapped graphs per stripe), leaving the extensions/root/scripts type tail in the `check-test-types` row; targets without stripe support keep the whole lane in that row. The compact Node planner keeps separate Blacksmith and standard 4-core hosted hint tables, targets 90/95 seconds of serial group work for the hosted large/small source runner classes, and applies a 1.6x median scaling fallback only to unmeasured groups. Failed and timeout-and-retry samples are excluded from refreshes. Groups above 150 predicted seconds split into file-weighted hosted stripes before packing; whole-config groups with registered file listers (agent support, gateway methods, runtime config, isolated unit fast) split the same way. The subprocess-heavy tooling family uses seven balanced stripes projecting to roughly 115 seconds per stripe. The resulting 70 push descriptors and 79 pull-request fallback descriptors stay within the 96-row hosted cap, and every nondist lane is at or below 149 predicted test seconds; the serial TUI PTY dist descriptor keeps its indivisible measured wall. In `github` mode compact jobs run hosted; hybrid attempt 1 uses each row's 4-vCPU or 8-vCPU Blacksmith label, while hybrid retries use hosted runners. The corresponding all-Blacksmith plans contain 25 and 34 total descriptors. Control UI E2E expands from the unchanged all-Blacksmith shape of three Vitest shards plus one browser-extension shard to eleven Vitest shards plus one browser-extension shard for the `github` and `hybrid` planner profiles. QA Smoke similarly expands from four parts to six. Hybrid attempt 1 runs those expanded matrices on Blacksmith; `github` mode and hybrid retries use hosted runners. QA's planner reserves the final part's observed roughly two-minute Matrix rider before greedily assigning primary scenarios, keeping that separate run from becoming the tail. Windows expands from the single Blacksmith job to three hosted jobs with disjoint, project-aligned explicit test lists only in `github` mode and on dispatches, whose runner expression forces hosted `windows-2025` in every mode. This partitions the complete Windows-specific inventory without applying Vitest `--shard` to project-local single-file selections, which Vitest rejects. Hybrid push and pull-request runs keep the single `blacksmith-8vcpu-windows-2025` lane because that runner class admits about two concurrent jobs, so a three-part split queues its tail behind a finished part; a hybrid retry reruns that single lane on hosted `windows-2025`, slower but bounded. Expect slower individual builds on standard 4-core hosted runners. Blacksmith's runner-registration budget is irrelevant for hosted jobs, but GitHub-hosted concurrency limits apply.
Hosted paths use the same setup exercised by manual dispatches and fork pull requests. Blacksmith-only Docker and sticky-disk steps are skipped, dependency setup uses the ordinary Actions pnpm-store cache, and low-memory Android builds use separate Gradle processes. Hybrid attempt-1 Node and plateau lanes deliberately keep this backend-neutral Actions-cache profile when they run on Blacksmith because preflight remains hosted. The exact workspace dependency cache intentionally stays off when preflight is hosted: GitHub can roll the runner image and Node patch between preflight and fanout in one workflow, while the safe exact key then misses; the ordinary store archive is only slightly smaller and pnpm's measured relink is already single-digit seconds. Vitest transform and Node compile caches still use the upstream Actions cache API, which Blacksmith proxies; their Linux-only `runner.os != 'Windows'` conditions do not exclude Blacksmith labels, and the planner still elects one semantic transform-cache writer. Core oxlint splits into five deterministic hosted stripes, with extension/scripts lint and optional UI and format checks in the existing `check-lint` row. The 14 serial core test-type graphs likewise split into five `check-test-types-core-*` stripes (two overlapped graphs per stripe), leaving the extensions/root/scripts type tail in the `check-test-types` row; targets without stripe support keep the whole lane in that row.
The compact Node planner keeps separate Blacksmith and standard 4-core hosted timing ownership. The `github` profile targets 90/95 seconds of serial group work for the hosted large/small source runner classes and applies a 1.6x median scaling fallback only to unmeasured groups. Hybrid keeps the expanded topology and hosted-derived splitting for groups above the unchanged 150-second ceiling, so hosted retries do not inherit an indivisible hosted wall pole. Its attempt-1 packing scales the Blacksmith hints by 0.87: the unscaled all-Blacksmith push plan totals 4,723 predicted seconds, while complete Blacksmith runs `31945998653` and `31949756966` measured 3,742.046 and 3,756.674 seconds of shard work (79.230% and 79.540%). The 0.87 factor is 9.379% above the higher observed ratio. With the direct outlier hints below, 0.85 yields 45 nondist jobs and 0.86 is the first two-decimal factor yielding 46; 0.87 stays one point above that rounding cliff while preserving the 46-job plan. Direct sampled hints cover the doctor and cron-service outliers, while a 140-second floor and singleton bin protect the observed `agentic-gateway-core-3` tail. During rebalance, native hybrid groups use scaled Blacksmith stripe hints; only synthesized hosted stripes retain their divided parent weight. Failed and timeout-and-retry samples are excluded from refreshes. Whole-config groups with registered file listers (agent support, gateway methods, runtime config, isolated unit fast) split into file-weighted hosted stripes. The subprocess-heavy tooling family uses seven balanced stripes projecting to roughly 115 hosted seconds per stripe.
The `github` profile contains 70 push descriptors and 79 pull-request fallback descriptors, with every nondist lane at or below 149 predicted test seconds; the serial TUI PTY dist descriptor keeps its indivisible measured wall. Hybrid contains 47 push descriptors (46 nondist jobs) and 55 pull-request fallback descriptors (53 nondist jobs), with a 140-second predicted maximum. In `github` mode compact jobs run hosted; hybrid attempt 1 uses each row's 4-vCPU or 8-vCPU Blacksmith label, while hybrid retries use hosted runners. The corresponding all-Blacksmith plans contain 25 and 34 total descriptors. Control UI E2E expands from the unchanged all-Blacksmith shape of three Vitest shards plus one browser-extension shard to eleven Vitest shards plus one browser-extension shard for the `github` and `hybrid` planner profiles. QA Smoke similarly expands from four parts to six. Hybrid attempt 1 runs those expanded matrices on Blacksmith; `github` mode and hybrid retries use hosted runners. QA's planner reserves the final part's observed roughly two-minute Matrix rider before greedily assigning primary scenarios, keeping that separate run from becoming the tail. Windows expands from the single Blacksmith job to three hosted jobs with disjoint, project-aligned explicit test lists only in `github` mode and on dispatches, whose runner expression forces hosted `windows-2025` in every mode. This partitions the complete Windows-specific inventory without applying Vitest `--shard` to project-local single-file selections, which Vitest rejects. Hybrid push and pull-request runs keep the single `blacksmith-8vcpu-windows-2025` lane because that runner class admits about two concurrent jobs, so a three-part split queues its tail behind a finished part; a hybrid retry reruns that single lane on hosted `windows-2025`, slower but bounded. Expect slower individual builds on standard 4-core hosted runners. Blacksmith's runner-registration budget is irrelevant for hosted jobs, but GitHub-hosted concurrency limits apply.
Restore all-Blacksmith routing after an outage by deleting the variable:
@@ -255,7 +259,7 @@ target below about 60% of the live bucket. With the current 10,000-registration
bucket, that means a 6,000-registration operating target, leaving headroom for
concurrent repositories, retries, and burst overlap.
The changed-target PR plan reduces the common Node test burst from 29 Blacksmith registrations to one. Broad-risk PRs keep the 32-registration metadata-complete compact fallback; canonical pushes use 24 nondist compact registrations. The `github` and `hybrid` planner profile can emit up to 96 compact rows. In `github` mode those rows are hosted; in hybrid they consume at most 96 Blacksmith registrations on attempt 1 and move to hosted capacity on retries. With roughly 25 other Blacksmith lanes, one full run registers about 121 runners; four admitted runs in a five-minute window register about 484, far below the 6,000-registration operating target.
The changed-target PR plan reduces the common Node test burst from 29 Blacksmith registrations to one. Broad-risk all-Blacksmith PRs keep the 32-registration metadata-complete compact fallback; canonical all-Blacksmith pushes use 24 nondist compact registrations. Hybrid currently uses 53 nondist fallback rows and 46 nondist push rows. The `github` and `hybrid` planner profiles remain capped at 96 compact rows; `github` rows are hosted, while hybrid rows consume Blacksmith registrations on attempt 1 and move to hosted capacity on retries. Even the 96-row cap plus roughly 25 other Blacksmith lanes is about 121 registrations per full run, or 484 for four admitted runs in a five-minute window, far below the 6,000-registration operating target.
Canonical-repo CI keeps Blacksmith as the default runner path for pushes and first-attempt same-repo pull-request runs when the backend is unset or `blacksmith`. Hybrid keeps the heavy set plus the named critical-path plateau lanes on Blacksmith for attempt 1; other light lanes and every rerun Blacksmith lane use GitHub-hosted capacity. Pull-request retries of both UI E2E jobs use GitHub-hosted Ubuntu in every mode; push retries remain on their normal backend unless hybrid fallback applies. All `workflow_dispatch` runs, including `release_gate`, and non-canonical repository runs use GitHub-hosted runners. The [`github` backend](#runner-backend-modes) provides a manual repository-wide fallback; canonical runs do not probe Blacksmith queue health or mutate the variable automatically.
+61 -29
View File
@@ -165,19 +165,18 @@ const COMPACT_EMBEDDED_GROUP_NAMES = [
const MAX_BUNDLED_NODE_TEST_PATTERNS = 64;
// Compact bundles trade a little serial work for fewer ephemeral runner registrations.
// Keep runner classes and subprocess isolation intact while bounding each combined job.
// The group hints below are loaded-fleet CI walls. Three-way striping plus a
// Blacksmith keeps the proven 200s/276s admission caps and 28-worker ceiling.
// Standard 4-core GitHub runners use direct hosted wall hints below. These
// budgets target roughly 160 seconds of predicted body work, leaving the
// measured ~70-second setup overhead inside a roughly 230-second lane.
// Default Blacksmith plans pack the Blacksmith base hints with 200s/276s
// admission caps. GitHub-hosted plans use direct hosted hints with 90s/95s
// packing caps. Hybrid keeps the expanded topology but packs its attempt-1
// Blacksmith rows with the refit Blacksmith estimates below.
const COMPACT_LARGE_NODE_TEST_JOB_SECONDS = 200;
const COMPACT_SMALL_NODE_TEST_JOB_SECONDS = 276;
const COMPACT_GITHUB_LARGE_NODE_TEST_JOB_SECONDS = 90;
const COMPACT_GITHUB_SMALL_NODE_TEST_JOB_SECONDS = 95;
const COMPACT_GITHUB_GROUP_SECONDS_SCALE = 1.6;
// Wall-time ceiling per hosted lane: with ~70s of fixed setup, a 150-second
// body keeps every Node lane inside a ~220-second job. The former 210-second
// ceiling let single fat groups own ~280-second critical paths.
const COMPACT_HYBRID_GROUP_SECONDS_SCALE = 0.87;
// Split groups above this hosted prediction before packing. Hybrid reuses the
// hosted-derived splits so retries cannot reunite an oversized hosted group.
const COMPACT_GITHUB_MAX_PREDICTED_SECONDS = 150;
const COMPACT_GITHUB_NODE_TEST_JOB_CAP = 96;
const COMPACT_NODE_TEST_JOB_GROUPS = 10;
@@ -530,6 +529,14 @@ const COMPACT_GITHUB_GROUP_SECONDS_HINTS = new Map<string, number>([
["core-unit-support", 32],
]);
// Hybrid-specific Blacksmith observations from 31949756966, plus the
// gateway-core-3 139.5s spike in 31938297538 that must stay singleton.
const COMPACT_HYBRID_GROUP_SECONDS_HINTS = new Map<string, number>([
["agentic-commands-doctor", 64],
["agentic-gateway-core-3", 140],
["core-runtime-cron-service", 80],
]);
// Advisory per-file wall-clock hints (seconds) for stripe balancing, measured
// from single-file local runs (M4 Max) and static import-graph size. Packing
// only: a stale entry skews stripe balance but never correctness. Unlisted
@@ -631,16 +638,33 @@ function estimateDefaultCompactGroupSeconds(group: NodeTestShardGroup): number {
return DEFAULT_WHOLE_GROUP_SECONDS;
}
function usesGithubRunnerProfile(runnerBackend: string | undefined): boolean {
function usesExpandedRunnerProfile(runnerBackend: string | undefined): boolean {
return runnerBackend === "github" || runnerBackend === "hybrid";
}
function estimateHybridCompactGroupSeconds(group: NodeTestShardGroup, seconds: number): number {
// The 4,723s Blacksmith push hint sum measured 3,742.046s/3,756.674s
// (79.230%/79.540%) in runs 31945998653/31949756966. A 0.87 scale keeps
// 9.379% headroom above the higher ratio. With direct outlier hints, it sits
// one point above the 0.86 packing cliff.
return (
COMPACT_HYBRID_GROUP_SECONDS_HINTS.get(group.shard_name) ??
Math.round(seconds * COMPACT_HYBRID_GROUP_SECONDS_SCALE)
);
}
function estimateCompactGroupSeconds(
group: NodeTestShardGroup,
runnerBackend: string | undefined,
): number {
const defaultSeconds = estimateDefaultCompactGroupSeconds(group);
if (!usesGithubRunnerProfile(runnerBackend)) {
// Hybrid attempt 1 runs on Blacksmith. It keeps the expanded topology for
// hosted retries, but its packing weights must describe the runner that
// normally executes the plan.
if (runnerBackend === "hybrid") {
return estimateHybridCompactGroupSeconds(group, defaultSeconds);
}
if (runnerBackend !== "github") {
return defaultSeconds;
}
return (
@@ -653,13 +677,15 @@ function estimateCompactStripeSeconds(
group: NodeTestShardGroup,
runnerBackend: string | undefined,
): number {
if (usesGithubRunnerProfile(runnerBackend)) {
if (runnerBackend === "github") {
return estimateCompactGroupSeconds(group, runnerBackend);
}
return (
const blacksmithSeconds =
COMPACT_LARGE_GROUP_STRIPE_SECONDS_HINTS.get(group.shard_name) ??
estimateDefaultCompactGroupSeconds(group)
);
estimateDefaultCompactGroupSeconds(group);
return runnerBackend === "hybrid"
? estimateHybridCompactGroupSeconds(group, blacksmithSeconds)
: blacksmithSeconds;
}
// Equal-weight sibling stripes can otherwise land in one bin and recreate the
@@ -2111,20 +2137,24 @@ const WHOLE_CONFIG_SPLIT_FILE_LISTERS = new Map<string, () => string[]>([
function splitOversizedGithubCompactGroup(
group: NodeTestShardGroup,
runnerBackend: string | undefined,
): Array<{ group: NodeTestShardGroup; seconds: number }> {
const seconds = estimateCompactGroupSeconds(group, "github");
if (seconds <= COMPACT_GITHUB_MAX_PREDICTED_SECONDS) {
return [{ group, seconds }];
// Hybrid retries run hosted, so retain hosted-derived striping even though
// Blacksmith timings own its attempt-1 packing weights.
const githubSeconds = estimateCompactGroupSeconds(group, "github");
const profileSeconds = estimateCompactGroupSeconds(group, runnerBackend);
if (githubSeconds <= COMPACT_GITHUB_MAX_PREDICTED_SECONDS) {
return [{ group, seconds: profileSeconds }];
}
const includePatterns =
group.includePatterns ?? WHOLE_CONFIG_SPLIT_FILE_LISTERS.get(group.shard_name)?.();
if (!includePatterns || includePatterns.length === 0) {
return [{ group, seconds }];
return [{ group, seconds: profileSeconds }];
}
const stripeCount = Math.ceil(seconds / COMPACT_GITHUB_MAX_PREDICTED_SECONDS);
const splitSeconds = Math.ceil(seconds / stripeCount);
const stripeCount = Math.ceil(githubSeconds / COMPACT_GITHUB_MAX_PREDICTED_SECONDS);
const splitSeconds = Math.ceil(profileSeconds / stripeCount);
return createStripedBatches(includePatterns, stripeCount, stripeFileWeight).map(
(patterns, index) => ({
group: {
@@ -2145,7 +2175,7 @@ function createCompactNodeTestShardBundles(
(shard) => compactMode !== "push" || !COMPACT_PUSH_EXCLUDED_SHARDS.has(shard.shardName),
);
const groupsByRunner = new Map<string, NodeTestShardGroup[]>();
const hostedSplitSeconds = new Map<string, number>();
const synthesizedSplitSeconds = new Map<string, number>();
for (const shard of shards) {
const runner = resolveCiNodeTestRunner(shard);
@@ -2159,13 +2189,15 @@ function createCompactNodeTestShardBundles(
runner,
shard_name: shard.shardName,
});
const plannedGroups = usesGithubRunnerProfile(options.runnerBackend)
? splitOversizedGithubCompactGroup(group)
const plannedGroups = usesExpandedRunnerProfile(options.runnerBackend)
? splitOversizedGithubCompactGroup(group, options.runnerBackend)
: [{ group, seconds: estimateCompactGroupSeconds(group, options.runnerBackend) }];
for (const planned of plannedGroups) {
groups.push(planned.group);
if (usesGithubRunnerProfile(options.runnerBackend)) {
hostedSplitSeconds.set(planned.group.shard_name, planned.seconds);
// Synthesized hosted stripes need their divided parent weight. Native
// groups must reach the runner-specific stripe estimator during rebalance.
if (planned.group.shard_name !== group.shard_name) {
synthesizedSplitSeconds.set(planned.group.shard_name, planned.seconds);
}
}
groupsByRunner.set(key, groups);
@@ -2173,10 +2205,10 @@ function createCompactNodeTestShardBundles(
const compactJobs: CompactNodeTestShard[] = [];
const estimateGroupSeconds = (group: NodeTestShardGroup) =>
hostedSplitSeconds.get(group.shard_name) ??
synthesizedSplitSeconds.get(group.shard_name) ??
estimateCompactGroupSeconds(group, options.runnerBackend);
const estimateStripeSeconds = (group: NodeTestShardGroup) =>
hostedSplitSeconds.get(group.shard_name) ??
synthesizedSplitSeconds.get(group.shard_name) ??
estimateCompactStripeSeconds(group, options.runnerBackend);
for (const groups of groupsByRunner.values()) {
// First-fit decreasing sets the existing registration count from the
@@ -2192,7 +2224,7 @@ function createCompactNodeTestShardBundles(
const exclusive = isExclusiveCompactGroup(group);
const secondsCap = exclusive
? COMPACT_EXCLUSIVE_JOB_SECONDS
: usesGithubRunnerProfile(options.runnerBackend)
: usesExpandedRunnerProfile(options.runnerBackend)
? group.runner.includes("-8vcpu-")
? COMPACT_GITHUB_LARGE_NODE_TEST_JOB_SECONDS
: COMPACT_GITHUB_SMALL_NODE_TEST_JOB_SECONDS
@@ -2280,7 +2312,7 @@ function createCompactNodeTestShardBundles(
}
if (
usesGithubRunnerProfile(options.runnerBackend) &&
usesExpandedRunnerProfile(options.runnerBackend) &&
compactJobs.length > COMPACT_GITHUB_NODE_TEST_JOB_CAP
) {
throw new Error(
+75 -13
View File
@@ -313,20 +313,82 @@ describe("scripts/lib/ci-node-test-plan.mts", () => {
"core-tooling-isolated",
]);
// Pushes retain three lanes of headroom under the workflow's 28-worker cap.
expect(compact).toHaveLength(25);
expect(pullRequestCompact).toHaveLength(34);
expect(githubCompact).toHaveLength(70);
expect(githubPullRequestCompact).toHaveLength(79);
expect(hybridCompact).toEqual(githubCompact);
expect(hybridPullRequestCompact).toEqual(githubPullRequestCompact);
for (const profile of [
{
name: "Blacksmith",
pullRequest: pullRequestCompact,
pullRequestJobs: 34,
pullRequestMax: 204,
push: compact,
pushJobs: 25,
pushMax: 204,
},
{
name: "GitHub-hosted",
pullRequest: githubPullRequestCompact,
pullRequestJobs: 79,
pullRequestMax: 186,
push: githubCompact,
pushJobs: 70,
pushMax: 149,
},
{
name: "hybrid",
pullRequest: hybridPullRequestCompact,
pullRequestJobs: 55,
pullRequestMax: 140,
push: hybridCompact,
pushJobs: 47,
pushMax: 140,
},
]) {
expect(profile.push, `${profile.name} push jobs`).toHaveLength(profile.pushJobs);
expect(profile.pullRequest, `${profile.name} pull-request jobs`).toHaveLength(
profile.pullRequestJobs,
);
expect(
Math.max(...profile.push.map((shard) => shard.predictedSeconds ?? Infinity)),
`${profile.name} push max`,
).toBe(profile.pushMax);
expect(
Math.max(...profile.pullRequest.map((shard) => shard.predictedSeconds ?? Infinity)),
`${profile.name} pull-request max`,
).toBe(profile.pullRequestMax);
}
expect(hybridCompact.filter((shard) => !shard.requiresDist)).toHaveLength(46);
expect(githubCompact.length - hybridCompact.length).toBeGreaterThanOrEqual(20);
expect(githubPullRequestCompact.length).toBeLessThanOrEqual(96);
// Nondist hosted lanes stay under the 150-second body ceiling; the serial
// TUI PTY dist descriptor keeps its indivisible measured wall.
expect(Math.max(...githubCompact.map((shard) => shard.predictedSeconds ?? Infinity))).toBe(149);
expect(
Math.max(...githubPullRequestCompact.map((shard) => shard.predictedSeconds ?? Infinity)),
).toBe(186);
// Nondist expanded-profile lanes stay under the 150-second body ceiling;
// the hosted PR's serial TUI PTY descriptor remains indivisible.
for (const plan of [
githubCompact,
githubPullRequestCompact,
hybridCompact,
hybridPullRequestCompact,
]) {
expect(
plan
.filter((shard) => !shard.requiresDist)
.every((shard) => (shard.predictedSeconds ?? Infinity) <= 150),
).toBe(true);
}
// Historical checks-node-compact-large-2 was this gateway-core group. Its
// 139.5s Blacksmith spike keeps a dedicated floor and singleton bin even
// though compact check numbers change when the matrix shrinks.
const hybridLargeTail = hybridCompact.find((shard) =>
shard.groups.some((group) => group.shard_name === "agentic-gateway-core-3"),
);
expect(hybridLargeTail?.groups.map((group) => group.shard_name)).toEqual([
"agentic-gateway-core-3",
]);
expect(hybridLargeTail?.predictedSeconds).toBe(140);
const hybridJobOf = (name: string) =>
hybridCompact.findIndex((shard) => shard.groups.some((group) => group.shard_name === name));
// Synthesized hosted stripes retain divided admission weights, while
// native hybrid groups use Blacksmith stripe hints during rebalance.
expect(hybridJobOf("agentic-agents-core-runtime-hosted-1")).not.toBe(
hybridJobOf("agentic-agents-core-tools"),
);
expect(compact.every((shard) => Array.isArray(shard.groups))).toBe(true);
expect(compact.every((shard) => shard.groups.length <= 10)).toBe(true);
expect(compact.some((shard) => shard.requiresDist)).toBe(true);
+24 -44
View File
@@ -5920,50 +5920,30 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
}),
);
const push = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
});
expect(push.status, push.output).toBe(0);
expect(
JSON.parse(
expectDefined(
push.outputs.checks_node_core_nondist_matrix,
"push node core nondist matrix output",
),
).include,
).toContainEqual(
expect.objectContaining({
check_name: "bundled-node-plan",
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: "push",
OPENCLAW_CI_TEST_RUNNER_BACKEND: "",
},
}),
);
const githubPush = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
runnerBackend: "github",
});
expect(githubPush.status, githubPush.output).toBe(0);
expect(
JSON.parse(
expectDefined(
githubPush.outputs.checks_node_core_nondist_matrix,
"GitHub-hosted push node core nondist matrix output",
),
).include,
).toContainEqual(
expect.objectContaining({
check_name: "bundled-node-plan",
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: "push",
OPENCLAW_CI_TEST_RUNNER_BACKEND: "github",
},
}),
);
for (const runnerBackend of [undefined, "github", "hybrid"] as const) {
const push = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
runnerBackend,
});
expect(push.status, push.output).toBe(0);
expect(
JSON.parse(
expectDefined(
push.outputs.checks_node_core_nondist_matrix,
`${runnerBackend ?? "default"} push node core nondist matrix output`,
),
).include,
).toContainEqual(
expect.objectContaining({
check_name: "bundled-node-plan",
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: "push",
OPENCLAW_CI_TEST_RUNNER_BACKEND: runnerBackend ?? "",
},
}),
);
}
const changedPullRequest = runCiManifestFixture({
bundledPlanner: true,