fix(ci): route web shell smoke to ARM64 independently (#15364)

* fix(ci): route web shell smoke to ARM64 independently

* test(ci): pin the smoke lane's ARM64 routing in the meta-tests

The runs-on change left two scripts/tests assertions pinning the old
routing, so the Linux Test check and the nightly macOS/Windows lanes
would go red once dependency installation succeeded (AGENTS.md
guideline #3).

- ci-platform-lanes: evaluate the real runs-on expression and pin
  self-hosted -> ARM64 plus the hosted and missing-output fallbacks,
  instead of the old equality with the shared test lane.
- no-ak-integration-ci: replace the raw-YAML literal pin with a check
  that the lane still derives its runner from the classifier; the
  architecture override is pinned in ci-platform-lanes.
- ci.yml: restore the #11250 pool history the routing change deleted,
  marked as x64-specific rather than a verdict on ARM64.

* test: tolerate slow durable I/O in CI synchronization checks
This commit is contained in:
易良 authored and GitHub committed 2026-10-23 16:02:17 +00:00
1 parent dcb32c28c6
commit 2b05c8bd99
6 files changed
+140 -20

No files matched your search

+77 -2
View File
@@ -4,7 +4,7 @@
//
// classify_pr carries the routing logic TWICE — the `runs-on` expression
// (which selects the classify job's own runner) and the `pick_runner` shell
// step (which publishes `ubuntu_runner` for every downstream Linux job). If
// step (which publishes `ubuntu_runner` for downstream Linux jobs). If
// they drift, classify and the Test job land on different pools. These tests
// evaluate BOTH against the same event matrix — including the negative
// associations that must stay hosted — and assert they agree.
@@ -118,7 +118,14 @@ function evalRunsOn(expression, { ecsDisabled, eventName, sameRepo, assoc }) {
// Executes the real pick_runner shell with the same inputs and returns the
// selected runner exactly as CI would publish it.
function runPickRunner({ ecsDisabled, sameRepo, assoc, eventName, dispatch }) {
function runPickRunner({
ecsDisabled,
sameRepo,
assoc,
eventName,
dispatch,
fleet,
}) {
const tmp = mkdtempSync(join(tmpdir(), 'pick-runner-'));
const outputFile = join(tmp, 'github_output');
const result = spawnSync('bash', ['-c', pickRunner.run], {
@@ -127,6 +134,7 @@ function runPickRunner({ ecsDisabled, sameRepo, assoc, eventName, dispatch }) {
AUTHOR_ASSOCIATION: assoc,
ECS_DISABLED: ecsDisabled ? 'true' : '',
EVENT_NAME: eventName,
LINUX_RUNNER_FLEET: fleet ?? '',
DISPATCH_LINUX_RUNNER: dispatch ?? '',
GITHUB_OUTPUT: outputFile,
},
@@ -266,6 +274,73 @@ describe('ci.yml classify_pr runner routing', () => {
});
});
describe('ci.yml web_shell_e2e_smoke runner routing', () => {
const expression = String(ciDoc.jobs.web_shell_e2e_smoke['runs-on'])
.replace(/^\$\{\{\s*/, '')
.replace(/\s*\}\}$/, '');
const selectSmokeRunner = new Function(
'needs',
'fromJSON',
'contains',
`return (${expression});`,
);
it('uses ARM64 for self-hosted runs while preserving every hosted fallback', () => {
for (const ecsDisabled of [false, true]) {
for (const fleet of ['', 'x64', 'arm64', 'hosted']) {
for (const sameRepo of [false, true]) {
for (const assoc of ASSOCIATIONS) {
for (const dispatch of [
undefined,
'self-hosted',
'self-hosted-arm64',
'hosted',
]) {
const runner = runPickRunner({
ecsDisabled,
fleet,
sameRepo,
assoc,
dispatch,
eventName: dispatch ? 'workflow_dispatch' : 'pull_request',
});
assert.deepEqual(
selectSmokeRunner(
{ classify_pr: { outputs: { ubuntu_runner: runner } } },
JSON.parse,
(labels, label) => labels.includes(label),
),
JSON.parse(runner).includes('self-hosted')
? ['self-hosted', 'linux', 'arm64']
: JSON.parse(HOSTED),
JSON.stringify({
ecsDisabled,
fleet,
sameRepo,
assoc,
dispatch,
runner,
}),
);
}
}
}
}
}
});
it('stays hosted when the classifier output is missing', () => {
assert.deepEqual(
selectSmokeRunner(
{ classify_pr: { outputs: {} } },
JSON.parse,
(labels, label) => labels.includes(label),
),
JSON.parse(HOSTED),
);
});
});
describe('ci.yml test_windows runner routing', () => {
it('keeps every pull request hosted, whoever opens it', () => {
// A pull_request run executes the workflow YAML from the PR's own merge
+7 -5
View File
@@ -1442,14 +1442,16 @@ jobs:
needs.classify_pr.outputs.skip_ci != 'true' &&
needs.test.outputs.ci_profile == 'full'
}}
# Routed like test's: trusted runs use the ECS pool, untrusted fork PRs
# stay hosted. #11250 moved it hosted after four pool attempts failed
# (run 34081409590). Three ran the smoke unpinned at 8 workers, half the
# host's 16 cores, against hosted's 2, and timed out broadly on
# Keep the classifier's trust and hosted overrides, but use ARM64 for
# this lane independently of the ordinary Linux CI fleet. The history
# below is x64-specific, not a verdict on ARM64, but it is why this lane
# is fragile: #11250 moved it back to hosted after four pool attempts
# failed (run 34081409590). Three ran the smoke unpinned at 8 workers,
# half the host's 16 cores, against hosted's 2, and timed out broadly on
# visibility and clock waits. The fourth's transcript gate took 73 s
# against its 60 s budget while npm install alone took 28 min on that
# host. The smoke step pins the hosted worker count.
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
runs-on: '${{ contains(fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]''), ''self-hosted'') && fromJSON(''["self-hosted", "linux", "arm64"]'') || fromJSON(''["ubuntu-latest"]'') }}'
# Slow hosted mirrors kept downloading past 23 min; reserve time for
# the 208 smoke tests and artifact cleanup after installation.
timeout-minutes: 60
@@ -859,6 +859,7 @@ describe('hosted-session-command-session funnel (H4e-b3)', () => {
expect(record.run.delivery?.state).toBe('rejected');
});
// The 256 real durable commands require over 1,000 filesystem syncs.
it('enforces the per-run command budget with budget_exhausted', ({
signal,
}) => {
@@ -926,7 +927,7 @@ describe('hosted-session-command-session funnel (H4e-b3)', () => {
})();
pendingBudgetTests.push(work);
return work;
}, 30_000);
}, 90_000);
it("commits the leader's receipt against an attached run and refetches its reply", async () => {
const lead = await leadSideSetup();
@@ -1690,20 +1690,28 @@ describe('extension tests', () => {
it('signals the durable commit before deferred settings finish', async () => {
const { manager, prepared } = await readyPrepared('settings-deferred');
const settings = deferred();
const settingsStarted = deferred();
const commitSettings = stubCommitSettings(
prepared,
vi.fn(async () => await settings.promise),
vi.fn(async () => {
settingsStarted.resolve();
await settings.promise;
}),
);
const onCommitted = vi.fn();
const committing = manager.commitPreparedExtension(prepared, onCommitted);
await vi.waitFor(() => expect(commitSettings).toHaveBeenCalledOnce());
expect(onCommitted).toHaveBeenCalledOnce();
expect(onCommitted.mock.invocationCallOrder[0]).toBeLessThan(
commitSettings.mock.invocationCallOrder[0]!,
);
settings.resolve();
try {
await Promise.race([settingsStarted.promise, committing]);
expect(commitSettings).toHaveBeenCalledOnce();
expect(onCommitted).toHaveBeenCalledOnce();
expect(onCommitted.mock.invocationCallOrder[0]).toBeLessThan(
commitSettings.mock.invocationCallOrder[0]!,
);
} finally {
settings.resolve();
await committing;
}
await expect(committing).resolves.toMatchObject({
identity: { name: 'settings-deferred' },
});
+33 -2
View File
@@ -71,6 +71,26 @@ const timeoutMinutesOn = (job, ubuntuRunner) => {
return Number(new Function(`return (${expr});`)());
};
// The smoke lane keeps the classifier's trust decision and hosted overrides
// but picks its own architecture, so its `runs-on` cannot be compared with
// another lane's. Evaluate the real expression the way
// `.github/scripts/ci-runner-routing.test.mjs` does.
const runnerOn = (job, ubuntuRunner) => {
const expression = String(ci.jobs[job]['runs-on'])
.replace(/^\$\{\{\s*/, '')
.replace(/\s*\}\}$/, '');
return new Function(
'needs',
'fromJSON',
'contains',
`return (${expression});`,
)(
{ classify_pr: { outputs: { ubuntu_runner: ubuntuRunner } } },
JSON.parse,
(labels, label) => labels.includes(label),
);
};
it('gives the shared Linux test lane one ceiling on both pools', () => {
// The full suite outgrew 60 minutes on hosted runners too: on 2026-10-17,
// 17 of the 30 hosted runs that ended on their own were cancelled at 60.
@@ -101,8 +121,19 @@ it('keeps lint_and_static sized for cold-cache pool runs', () => {
expect(timeoutMinutesOn('lint_and_static', '')).toBe(45);
});
it('routes browser gates like the shared Linux test lane, at the hosted worker count', () => {
expect(ci.jobs.web_shell_e2e_smoke['runs-on']).toBe(ci.jobs.test['runs-on']);
it('routes the browser smoke to ARM64 independently of the shared Linux test lane', () => {
// The lane keeps the classifier's trust decision and hosted overrides, but
// it no longer inherits the ordinary Linux fleet's architecture.
expect(runnerOn('web_shell_e2e_smoke', ECS_RUNNER)).toEqual([
'self-hosted',
'linux',
'arm64',
]);
expect(runnerOn('web_shell_e2e_smoke', HOSTED_RUNNER)).toEqual([
'ubuntu-latest',
]);
// A missing classifier output (classify_pr skipped) is the hosted routing.
expect(runnerOn('web_shell_e2e_smoke', '')).toEqual(['ubuntu-latest']);
const smoke = ci.jobs.web_shell_e2e_smoke.steps.find(
(step) => step.name === 'Run web-shell browser smoke',
);
+5 -2
View File
@@ -1042,8 +1042,11 @@ describe('no-AK integration CI wiring', () => {
);
const webShellJob = getWorkflowJob(workflow, 'web_shell_e2e_smoke');
expect(webShellJob).toContain(
"runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''[\"ubuntu-latest\"]'') }}'",
// The lane still takes its runner from the classifier, so an untrusted
// fork PR cannot reach a self-hosted host; the architecture override it
// applies on top is pinned in scripts/tests/ci-platform-lanes.test.js.
expect(webShellJob).toMatch(
/runs-on: .*needs\.classify_pr\.outputs\.ubuntu_runner/,
);
const hostedInstall = getWorkflowStep(
webShellJob,