Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/scripts/ci/platform-lane-triggers.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// The macOS and Windows lanes have exactly one live trigger left: the nightly
// schedule. Their pull-request trigger is off until the standing Windows
// path/symlink failures are fixed, and the merge queue is not enabled on this
// repository — no `merge_group` run since 2026-07-02 — so `on.schedule` is the
// only thing between this repository and the state #9220 shipped from: a
// macOS-only failure sitting in `main` with nothing left to report it.
//
// These guards are about that single point of failure. The schedule exists,
// both lanes accept it, and a nightly is those two jobs and nothing else — a
// third job joining them would burn a runner every night and, worse, could
// fail the nightly for reasons that have nothing to do with either platform,
// which is how a real regression gets read as noise.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import { parse } from 'yaml';

const ciDoc = parse(
readFileSync(
join(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'workflows',
'ci.yml',
),
'utf8',
),
);

const LANES = ['test_macos', 'test_windows'];

// Evaluates a job-level `if` for one event name. Only the `github.event_name`
// atoms are resolved against the event; every other atom — `cancelled()`, a
// `needs` output, a pull_request payload field — collapses to the value that
// most favours the job RUNNING. A job this reports as skipped is therefore
// skipped by its event wiring alone, never by an accident of some upstream
// output, which is the only thing these guards claim to check.
function runsOnEvent(expression, eventName) {
const js = String(expression)
.trim()
.replace(/^\$\{\{/, '')
.replace(/\}\}$/, '')
.replace(/github\.event_name\s*==\s*'([^']*)'/g, (_, n) =>
String(n === eventName),
)
.replace(/github\.event_name\s*!=\s*'([^']*)'/g, (_, n) =>
String(n !== eventName),
)
.replace(/!\s*[a-z_]+\(\)/gi, 'true')
.replace(/[A-Za-z_][\w.]*\s*[=!]=\s*'[^']*'/g, 'true')
.replace(/\b(?:github|needs|vars|inputs)\.[\w.]+/g, 'true')
.replace(/\btrue\s*[=!]=\s*true\b/g, 'true');

// Anything the substitutions did not reduce to the boolean skeleton is a
// shape this evaluator was never taught to read: fail loudly rather than
// quietly report a job as skipped because a new atom parsed as nothing.
assert.match(
js,
/^[\s()!&|truefalse]*$/,
`unreduced atoms in \`if\`: ${js.trim()}`,
);
return Boolean(new Function(`return (${js});`)());
}

describe('ci.yml macOS/Windows lane triggers', () => {
it('keeps the nightly schedule that is now their only trigger', () => {
const schedule = ciDoc.on?.schedule;
assert.ok(
Array.isArray(schedule) && schedule.length > 0 && schedule[0].cron,
'ci.yml must keep an `on.schedule` cron: with the pull-request trigger ' +
'off and no merge queue, deleting it silences macOS and Windows ' +
'entirely instead of merely reducing their coverage',
);
});

it('runs both lanes on that schedule', () => {
for (const lane of LANES) {
assert.ok(
runsOnEvent(ciDoc.jobs[lane].if, 'schedule'),
`${lane} must run on the nightly schedule`,
);
}
});

it('runs the two lanes and nothing else on a nightly', () => {
const scheduled = Object.entries(ciDoc.jobs)
.filter(([, job]) => runsOnEvent(job.if, 'schedule'))
.map(([name]) => name)
.sort();
assert.deepEqual(scheduled, [...LANES].sort());
});
});
152 changes: 51 additions & 101 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ on:
- 'main'
- 'release/**'
merge_group:
# A daily heartbeat for the macOS and Windows lanes ONLY. Those two are
# gated on `merge_group`, and the merge queue is not enabled on this
# repository — no queue run since 2026-07-02 — so they had stopped running
# entirely: skipped on every pull request, and never reached afterwards. The
# pull-request gate below catches the diffs a path list can recognise; this
# catches everything it cannot, one day later, on `main`. Every other job
# here excludes `schedule` explicitly, so a nightly run is exactly two jobs.
# The macOS and Windows lanes' ONLY remaining trigger, and therefore this
# repository's only signal about a host that is not Linux with a GNU
# userland. Those two are otherwise gated on `merge_group`, and the merge
# queue is not enabled here — no queue run since 2026-07-02 — while their
# pull-request trigger is off until the standing Windows failures are fixed
# (see test_macos). A regression therefore surfaces here, one day later, on
# `main`, and nowhere else: treat a red nightly as a blocker, not as noise.
# Every other job here excludes `schedule` explicitly, so a nightly run is
# exactly two jobs.
schedule:
- cron: '17 19 * * *'
workflow_dispatch:
Expand Down Expand Up @@ -59,7 +61,7 @@ env:
# BOTH the github_ci_only helper step and the full-profile Test step, so a
# new helper test can't be added to one path and silently dropped from the
# other.
HELPER_TESTS: '.github/scripts/pr-safety-precheck.test.mjs .github/scripts/cap-release-notes.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/ci/classify-platform-sensitivity.test.mjs .github/scripts/ci/classify-pr-profile.test.mjs .github/scripts/upsert-bot-comment.test.mjs .github/scripts/ci/main-failure-signature.test.mjs .github/scripts/classify-release-notes.test.mjs .github/scripts/dsw-swe-verified/make-manifest.test.mjs .github/scripts/dsw-swe-verified/make-terminal-bench-manifest.test.mjs .github/scripts/resolve-sandbox-image.test.mjs .github/scripts/web-shell-visuals-publish.test.mjs .github/scripts/web-shell-visuals-compose.test.mjs .github/scripts/serve-ab-diff.test.mjs .github/scripts/serve-ab-drive.test.mjs .github/scripts/qwen-triage-workflow.test.mjs .github/scripts/assign-issue-owner.test.mjs .github/scripts/auto-minimize-spam.test.mjs .github/scripts/ci-runner-routing.test.mjs'
HELPER_TESTS: '.github/scripts/pr-safety-precheck.test.mjs .github/scripts/cap-release-notes.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/ci/classify-platform-sensitivity.test.mjs .github/scripts/ci/classify-pr-profile.test.mjs .github/scripts/ci/platform-lane-triggers.test.mjs .github/scripts/upsert-bot-comment.test.mjs .github/scripts/ci/main-failure-signature.test.mjs .github/scripts/classify-release-notes.test.mjs .github/scripts/dsw-swe-verified/make-manifest.test.mjs .github/scripts/dsw-swe-verified/make-terminal-bench-manifest.test.mjs .github/scripts/resolve-sandbox-image.test.mjs .github/scripts/web-shell-visuals-publish.test.mjs .github/scripts/web-shell-visuals-compose.test.mjs .github/scripts/serve-ab-diff.test.mjs .github/scripts/serve-ab-drive.test.mjs .github/scripts/qwen-triage-workflow.test.mjs .github/scripts/assign-issue-owner.test.mjs .github/scripts/auto-minimize-spam.test.mjs .github/scripts/ci-runner-routing.test.mjs'
# The growth ratchet and its vitest mirror compare each workflow against
# the PR's base commit to tell "this PR grew the file" apart from "the
# baseline went stale on main" (#9904). Wired once here so every lane
Expand Down Expand Up @@ -152,71 +154,6 @@ jobs:
echo "ubuntu_runner=${ubuntu_runner}" >> "${GITHUB_OUTPUT}"
echo "Selected Linux runner: ${ubuntu_runner}"

# Does this pull request need the macOS and Windows lanes? They are the only
# signal this repository has about a host that is not Linux with a GNU
# userland, and they are expensive, so they run on the diffs whose behaviour
# the HOST decides — shell, CI definitions, the script layer, the runner
# config, the platform-coupled subtrees. The classifier is a net, not a
# proof; the scheduled run on `main` is what covers everything a path list
# cannot see.
#
# Its own job, not a step in classify_pr: that job's outputs pick the Linux
# runner for everything else, and a new failure mode there (this one needs a
# checkout, on a pool whose workspace other jobs can poison) would take the
# whole run's routing with it. Here a failure costs one skipped
# classification, which the gate reads as "run the lanes".
#
# Hosted, and the checkout is of the pull request's BASE commit: this runs
# before any review, so checking out the contributor's head would run their
# classifier with this job's token, and staying off the ECS pool keeps it
# away from the poisoned-workspace class entirely.
classify_platform:
name: 'Classify platform sensitivity'
if: "${{ github.event_name == 'pull_request' }}"
runs-on: 'ubuntu-latest'
timeout-minutes: 5
continue-on-error: true
permissions:
contents: 'read'
pull-requests: 'read'
outputs:
platform_sensitive: '${{ steps.platform.outputs.platform_sensitive }}'
steps:
- name: 'Check out the classifier from the base branch'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ github.event.pull_request.base.sha }}'
persist-credentials: false
sparse-checkout: '.github/scripts/ci'

- name: 'Classify platform sensitivity'
id: 'platform'
env:
GH_TOKEN: '${{ github.token }}'
PR_NUMBER: '${{ github.event.pull_request.number }}'
IS_SAME_REPO_PR: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
run: |-
set -uo pipefail
# Fail-safe in every direction: only a confident `false` from the
# classifier skips the lanes. A classifier error, an unexpected
# word, a fork PR, or this whole job failing all end as "run" — a
# gate that fails open stops testing without ever saying so.
sensitive=true
if [ "${IS_SAME_REPO_PR}" = 'true' ]; then
set +e
classified="$(.github/scripts/ci/classify-pr-profile.sh "${GITHUB_REPOSITORY}" "${PR_NUMBER}" platform)"
rc=$?
set -e
case "${rc}:${classified}" in
0:true|0:false) sensitive="${classified}" ;;
*) echo "::warning::Platform-sensitivity classifier returned rc=${rc} '${classified}'; running the macOS and Windows lanes." ;;
esac
else
echo "Fork PR detected; running the macOS and Windows lanes."
fi
echo "platform_sensitive=${sensitive}" >> "${GITHUB_OUTPUT}"
echo "Platform-sensitive: ${sensitive}"

#
# Test: Node
#
Expand Down Expand Up @@ -916,13 +853,34 @@ jobs:
# `realpath -m` in a workflow guard, with the suite that pinned it red on
# every Mac).
#
# Three triggers now, in cost order: a pull request whose diff the
# platform-sensitivity classifier recognises (shell, CI definitions, the
# script layer, the runner config, the platform-coupled subtrees), the merge
# queue if it is ever enabled again, and a nightly run on `main` for
# everything a path list cannot see. The pull-request gate is fail-safe —
# only a confident `false` skips, so a broken classifier costs runner minutes
# rather than coverage.
# The pull-request trigger is OFF, and this is the whole reason the nightly
# above exists. Over the 18 hours measured before it was pulled, the Windows
# lane reported 13 failures and 0 successes on pull requests — one standing
# set of Windows-only path and symlink failures (resolved paths into
# read_many_files, releaseWorktree through an ancestor symlink, the SHA-256
# review worktree) repeating across unrelated PRs — and macOS queued for a
# p90 of 42 minutes, 159 at worst, on a hosted pool this repository does not
# saturate by itself. Neither lane gates a merge: the `main` ruleset carries
# no required status check. So the cost landed entirely on contributors, who
# read a red X no diff of theirs caused and waited on checks that never had
# to pass.
#
# Two triggers remain, in cost order: the merge queue if it is ever enabled
# again, and a nightly run on `main` — which is now the only thing keeping
# macOS and Windows visible at all, so it is load-bearing rather than a
# backstop. Read a red nightly as a blocker; nothing else will report it.
#
# Restoring the pull-request trigger is the last step of fixing those
# failures, not a separate decision. It takes three things, and the pieces
# are all still here: revert the commit that removed the `classify_platform`
# job (its classifier, `classify-platform-sensitivity.mjs`, the `platform`
# mode of `classify-pr-profile.sh` and both their test files were left
# untouched precisely so this stays a revert), put its
# `github.event_name == 'pull_request' && needs.classify_platform.outputs
# .platform_sensitive != 'false'` arm back on both lanes, and re-add it to
# each lane's `needs`. Worth revisiting at the same time: that classifier
# calls every fork pull request platform-sensitive, which is most of the
# traffic here and most of what the macOS queue was spent on.
#
# Two named jobs, not a matrix: a skipped matrix job reports one collapsed
# check name, never the per-OS contexts, so a required-check configuration
Expand All @@ -932,17 +890,13 @@ jobs:
# currently insurance, not a live constraint.)
test_macos:
name: 'Test (macos-latest, Node 22.x)'
needs:
- 'classify_pr'
- 'classify_platform'
needs: 'classify_pr'
if: |-
${{
!cancelled() && (
github.event_name == 'merge_group' ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
needs.classify_platform.outputs.platform_sensitive != 'false')
github.event_name == 'workflow_dispatch'
)
}}
runs-on: 'macos-latest'
Expand Down Expand Up @@ -1022,28 +976,24 @@ jobs:

# Windows counterpart of test_macos (see that job's note). ECS is the default
# with a windows-2022 kill-switch fallback; the check name stays unchanged so
# it matches the required-status-check context. The lane now runs on pull
# requests too, but every pull request runs on hosted windows-2022: a
# pull_request run executes the workflow YAML from the PR's own merge commit,
# so any PR this lane admits could rewrite `runs-on` in the same diff that
# reaches it. A gate the gated tree controls is no gate; the pool is reached
# only by triggers an unreviewed PR cannot open — the post-approval merge
# queue, schedule and dispatch. ECS-only tuning is gated on runner.environment;
# the hosted fallback is the pre-ECS job plus the checkout guard and a
# job-level timeout-minutes.
# it matches the required-status-check context. The `!= 'pull_request'` arm
# of `runs-on` is inert while the pull-request trigger is off, and stays for
# when it returns: a pull_request run executes the workflow YAML from the
# PR's own merge commit, so any PR this lane admits could rewrite `runs-on`
# in the same diff that reaches it. A gate the gated tree controls is no
# gate; the pool is reached only by triggers an unreviewed PR cannot open —
# the post-approval merge queue, schedule and dispatch. ECS-only tuning is
# gated on runner.environment; the hosted fallback is the pre-ECS job plus
# the checkout guard and a job-level timeout-minutes.
test_windows:
name: 'Test (windows-latest, Node 22.x)'
needs:
- 'classify_pr'
- 'classify_platform'
needs: 'classify_pr'
if: |-
${{
!cancelled() && (
github.event_name == 'merge_group' ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
needs.classify_platform.outputs.platform_sensitive != 'false')
github.event_name == 'workflow_dispatch'
)
}}
runs-on: '${{ vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && github.event_name != ''pull_request'' && fromJSON(''["self-hosted", "Windows", "X64", "ecs-win"]'') || fromJSON(''["windows-2022"]'') }}'
Expand Down
Loading