mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
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:
committed by
GitHub
parent
1b34939f82
commit
3e2edc7f22
+6
-2
@@ -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.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user