scripts/ci-scope.mjs
Rama main · réplica de lectura del repositorio original.
import { execFileSync } from "node:child_process";
import { appendFileSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
// The ci.yml jobs a pull request runs only when its change can affect them;
// `static` and the `CI` gate always run. Besides these job ids a scope's checks
// name the vitest job's parts: "linux-tests", "host-tests" (Windows, and macOS
// where the event runs it), and the subsets in VITEST_SUBSETS.
export const SELECTABLE_JOBS = [
"build", "vitest", "behavior-evals", "packaged-server", "windows-cua", "electron-smokes", "cloudflared-linux-arm64",
"foss", "control-plane", "ui-smoke", "browser-identity", "package-linux", "browser-driver", "relay",
];
// The server imports Electron and renderer modules, and the macOS smokes run
// the packaged server inside Electron, so a server change and a desktop change
// can affect the same checks.
const SERVER_CHECKS = [
"build", "linux-tests", "host-tests", "behavior-evals", "packaged-server", "foss", "electron-smokes",
"ui-smoke", "browser-identity", "browser-driver", "package-linux",
];
const EVERY_CHECK = [...SERVER_CHECKS, "windows-cua", "cloudflared-linux-arm64", "control-plane", "relay"];
/** What a pull request's change in each area can affect. */
export const AREA_CHECKS = {
docs: ["docs-tests"],
locales: ["build", "locale-tests"],
web: ["build", "linux-tests", "ui-smoke"],
"script-tests": ["linux-tests", "host-tests", "ui-smoke", "browser-identity"],
server: SERVER_CHECKS,
desktop: SERVER_CHECKS,
// The Windows CUA job starts these the way the installed app does, then lends the PC's screen through them.
"windows-computer-control": [...SERVER_CHECKS, "windows-cua"],
"control-plane": ["control-plane"],
// The relay is its own package with its own lockfile; nothing outside relay/ reaches it except the shared vectors,
// which it keeps a pinned copy of.
relay: ["relay"],
"e2e-vectors": [...SERVER_CHECKS, "relay"],
everything: EVERY_CHECK,
};
/** The vitest files a pull request runs on Linux when it runs no full Linux
* shards, as vitest filters. Locale tests: every test that reads a translation
* other than English (the renderer's files take about nine minutes in one
* process), with the Electron node tests, which compare their own copy of some
* strings with every catalog, and the server's tests of its lines in other
* languages (they load server/server-line-packs.ts). Docs tests: every test that
* reads the docs. */
export const VITEST_SUBSETS = {
"locale-tests": {
part: "locale tests",
files: [
"src/",
"electron/cua-launch.test.mjs",
"electron/cua-watch-restart.test.mjs",
"electron/cua-windows-isolation.test.mjs",
"electron/menu.test.mjs",
"electron/updater.test.mjs",
"scripts/generate-locale.test.mjs",
"scripts/testing/bot-tools-ui.e2e.test.ts",
"scripts/testing/key-readback.e2e.test.ts",
"scripts/testing/one-workspace-ui.e2e.test.ts",
"scripts/testing/rail-profile-ui.e2e.test.ts",
"scripts/testing/secret-card-descriptions.test.ts",
"scripts/testing/team-own-chat.e2e.test.ts",
"scripts/testing/team-side-switch-ui.e2e.test.ts",
"server/body-thread-guards.e2e.test.ts",
"server/client-notifications.e2e.test.ts",
"server/cloud-connections.e2e.test.ts",
"server/cloud-lending-browser.e2e.test.ts",
"server/cloud-lending-chrome.e2e.test.ts",
"server/cloud-lending-memory.e2e.test.ts",
"server/cloud-lending.e2e.test.ts",
"server/cloud-lent-mcp.e2e.test.ts",
"server/cloud-send-kinds.e2e.test.ts",
"server/cloud-send-move.e2e.test.ts",
"server/cloud-send-rooms.e2e.test.ts",
"server/cloud-send-routine-merge.e2e.test.ts",
"server/cloud-send-store.e2e.test.ts",
"server/cloud-send.e2e.test.ts",
"server/cloud-sync.e2e.test.ts",
"server/cloud-team-asker-owns.e2e.test.ts",
"server/cloud-team-away.e2e.test.ts",
"server/cloud-team-browser.e2e.test.ts",
"server/cloud-team-connections.e2e.test.ts",
"server/cloud-team-engine-homes.e2e.test.ts",
"server/cloud-team-gateway-mcp.e2e.test.ts",
"server/cloud-team-inbox.e2e.test.ts",
"server/cloud-team-mausbot-reach.e2e.test.ts",
"server/cloud-team-new-chat-private.e2e.test.ts",
"server/cloud-team-open-reach.e2e.test.ts",
"server/cloud-team-own-key-apps.e2e.test.ts",
"server/cloud-team-owners.e2e.test.ts",
"server/cloud-team-people.e2e.test.ts",
"server/cloud-team-per-person.e2e.test.ts",
"server/cloud-team-phones.e2e.test.ts",
"server/cloud-team-pictures.e2e.test.ts",
"server/cloud-team-presence.e2e.test.ts",
"server/cloud-team-privacy-holes.e2e.test.ts",
"server/cloud-team-private.e2e.test.ts",
"server/cloud-team-profile.e2e.test.ts",
"server/cloud-team-requests.e2e.test.ts",
"server/cloud-team-routines.e2e.test.ts",
"server/cloud-team-setup-request.e2e.test.ts",
"server/cloud-team.e2e.test.ts",
"server/host/brief-words.test.ts",
"server/host/lending-lifts.test.ts",
"server/lent-folders.e2e.test.ts",
"server/move-chosen-bots.e2e.test.ts",
"server/one-home-keep.e2e.test.ts",
"server/one-home-outdated.e2e.test.ts",
"server/one-home-retry.e2e.test.ts",
"server/one-home-session.e2e.test.ts",
"server/one-home-trial-key.e2e.test.ts",
"server/one-home-two-desks.e2e.test.ts",
"server/one-home.e2e.test.ts",
"server/parked-cards.e2e.test.ts",
"server/question-cards.e2e.test.ts",
"server/shared-computers.e2e.test.ts",
"server/shared-computers.gate.test.ts",
"server/team-move.e2e.test.ts",
"server/team-welcome.e2e.test.ts",
"server/use-my-mac.e2e.test.ts",
"server/workspace-banner.e2e.test.ts",
"scripts/server-locale-plugin.test.ts",
"server/server-lines.test.ts",
"server/host/devs-reports.test.ts",
"server/automation-reports.test.ts",
"server/incidents.test.ts",
"server/routine-run-card.test.ts",
"server/host/chief-notices-retry.test.ts",
"server/connector-card-text.test.ts",
"server/host/secret-cards.test.ts",
"server/notify.test.ts",
"server/room-lines.test.ts",
"server/post-to-room.test.ts",
"server/teammate-lines.test.ts",
"server/chat-notices.test.ts",
"server/recall-disclosure.test.ts",
"server/routine-cron-text.test.ts",
"server/routine-requests.test.ts",
"server/cloud-send-arrived.test.ts",
"server/cloud-team-bot-access.e2e.test.ts",
"server/cloud-team-one-answer.e2e.test.ts",
"server/cloud-team-preference.e2e.test.ts",
"server/cloud-team-rules.e2e.test.ts",
"server/cloud-team-rules-running-turn.e2e.test.ts",
"server/outside-work-threads.e2e.test.ts",
"server/own-home.e2e.test.ts",
"server/places-elsewhere.e2e.test.ts",
"server/question-routing.e2e.test.ts",
"server/team-protocol.e2e.test.ts",
"server/team-small-fixes.e2e.test.ts",
"server/team-mausbot-followup.e2e.test.ts",
"server/team-no-ai.e2e.test.ts",
"server/team-refused-handoff.e2e.test.ts",
"server/two-places.e2e.test.ts",
"scripts/ci-workflow.test.ts",
"scripts/pre-push-check.test.ts",
"server/host/wait-words.test.ts",
"server/place-inbox.e2e.test.ts",
"server/bot-api-team.e2e.test.ts",
"server/cloud-team-first-look.e2e.test.ts",
"server/cloud-team-mausbot-own-chat.e2e.test.ts",
"server/connector-stayed.e2e.test.ts",
"server/email/host.test.ts",
"server/host/outside-lineage.test.ts",
"server/memory-move.test.ts",
"server/outside-lineage.e2e.test.ts",
"server/same-name-mentions.e2e.test.ts",
"server/skill-switch.test.ts",
],
shards: 2,
nodeTests: true,
},
"docs-tests": {
part: "docs tests",
files: ["scripts/testing/analytics-docs.test.ts", "server/model-providers-docs.test.ts", "src/locales/vocabulary.test.ts"],
shards: 1,
nodeTests: false,
},
};
/** Renderer files that also run outside the renderer: the server imports
* them, or the macOS app-frame smoke bundles them. ci-scope.test.ts holds this
* equal to the import graph. The language packs are left out, though the
* server reads their "server." lines: a change to English runs every Linux test,
* and one to another language runs every test that reads it (the locale tests). */
export const SRC_OUTSIDE_RENDERER = [
"src/components/app/app-frame-host.ts",
"src/lib/citations.ts",
"src/lib/composer-attachments.ts",
"src/lib/random-uuid.ts",
"src/lib/receipts.ts",
];
export const DOCS = /^(?:[^/]+\.md|docs\/.+\.md|\.github\/FUNDING\.yml)$/;
// The first matching rule names a path's area; a path no rule names can affect
// anything. English is the source every test reads, so en.json is web.
const AREAS = [
[DOCS, "docs"],
[/^(?:apps\/)?docs\//, "docs"],
[/^scripts\/.+\.test\.[cm]?[jt]sx?$/, "script-tests"],
[/^src\/locales\/(?:(?!en\.json$)[^/]+\.json|[^/]+\.test\.ts)$/, "locales"],
[/^(?:src|public)\/|^[^/]+-preview\.html$/, "web"],
[/^companion\/src\/e2e\/vectors\//, "e2e-vectors"],
[/^(?:server|shared|enterprise|companion|evals|skills|deploy|cloudflare\/composio-broker)\/|^(?:Dockerfile|\.dockerignore|compose\.yaml)$/, "server"],
[/^electron\/(?:cua|cua-failure|cua-connection|capabilities|screen-status|shared-cua-connection|shared-computer-access|lent-screen-tools|lent-screen-failures|paced-typing)\.[cm]js$/, "windows-computer-control"],
[/^(?:electron|build)\/|^(?:electron-builder\.yml|index\.html|rail\.html)$/, "desktop"],
[/^cloudflare\/control-plane\//, "control-plane"],
[/^relay\//, "relay"],
];
export function areaOf(file) {
if (typeof file !== "string" || /\p{Cc}/u.test(file) || file.split("/").some((part) => !part || part === "." || part === "..")) {
return "everything";
}
if (SRC_OUTSIDE_RENDERER.includes(file)) return "server";
return AREAS.find(([pattern]) => pattern.test(file))?.[1] ?? "everything";
}
export const FULL_SCOPE = { areas: ["everything"], checks: [...EVERY_CHECK].sort() };
/** A push to main and a merge group: the build, every test file on Linux and the Windows shards, the macOS smokes and
* the Linux checks no longer than a shard. The macOS shards, the Windows CUA smoke, the Linux package, the browser driver
* and the ARM installer run in the full lane only: nightly, manual runs and the release lane. */
export const MAIN_SCOPE = {
areas: ["main"],
checks: [
"behavior-evals", "browser-identity", "build", "control-plane", "electron-smokes", "foss", "host-tests", "linux-tests", "packaged-server",
"ui-smoke",
],
};
/** A push whose commit already passed the CI gate in the merge queue: only the control-plane checks the broker deploy needs. */
export const QUEUED_SCOPE = { areas: ["tested in the merge queue"], checks: ["control-plane"] };
/** A pull request's areas and the checks they select, from its changed files. */
export function selectCiScope(files) {
const areas = Array.isArray(files) && files.length > 0 ? [...new Set(files.map(areaOf))].sort() : ["everything"];
if (areas.includes("everything")) return FULL_SCOPE;
return { areas, checks: [...new Set(areas.flatMap((area) => AREA_CHECKS[area]))].sort() };
}
/** The pull request label that runs the Windows jobs whatever the change. */
export const WINDOWS_LABEL = "windows";
/** Paths whose change can break Windows first: Windows by name, the desktop app and its installer, the dependencies,
* the scripts that build, package or smoke the app, and what decides how the Windows shards run. */
export const WINDOWS_PATH = new RegExp([
String.raw`(?:^|[/._-])(?:win|win32|win64|windows|nsis|powershell|pwsh)(?:[/._-]|$)`,
String.raw`\.(?:ps1|psm1|psd1|cmd|bat|exe|msi|nsh|nsi)$`,
String.raw`^(?:electron|build|third_party)/`,
String.raw`^(?:electron-builder\.yml|package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|\.npmrc|\.gitattributes|vite\.config\.ts)$`,
String.raw`^\.github/workflows/ci\.yml$|^scripts/ci-scope\.mjs$`,
String.raw`^scripts/testing/(?:host-e2e\.ts|duration-sequencer\.ts|vitest-shard-weights\.json|ci-retry-[^/]+)$|^server/testing/host-timeout\.ts$`,
String.raw`^scripts/(?:build|bundle|prepare|package|smoke|after-pack|stage)[^/]*$`,
].join("|"), "i");
/** A changed line of code that names Windows or one of its APIs. */
export const WINDOWS_TEXT = new RegExp([
String.raw`\bwin32\b|\bWindows\b|\bwindows-latest\b|[Pp]ower[Ss]hell|\bpwsh\b|\bcmd\.exe\b|\.exe\b|\bpath\.win32\b`,
String.raw`\b(?:USERPROFILE|LOCALAPPDATA|APPDATA|PATHEXT|ComSpec)\b|\bwindows(?:Hide|VerbatimArguments)\b`,
].join("|"));
/** Why a pull request runs the Windows jobs, or null when nothing in it is Windows-sensitive: its label, a path above,
* an e2e file the Windows shards also run (`hostE2e`), or a changed line in `lines` (`git diff -U0` of its code). */
export function windowsReason({ files, labels = [], hostE2e = [], lines = () => [] }) {
if (labels.includes(WINDOWS_LABEL)) return `the "${WINDOWS_LABEL}" label`;
const file = files.find((path) => WINDOWS_PATH.test(path) || hostE2e.includes(path));
if (file) return file;
let changed;
try {
changed = lines();
} catch (error) {
return `a change whose lines could not be read (${error.message})`;
}
const line = changed.find((text) => /^[+-](?![+-]{2} )/.test(text) && WINDOWS_TEXT.test(text));
return line ? `the changed line ${JSON.stringify(line.slice(0, 80))}` : null;
}
/** True when two package.json texts differ only in the top-level "version"
* (the release bump, prepare-release.yml). Anything else that changed, a
* reordered key included, or text that is not a JSON object, is false. */
export function versionOnlyPackageJson(before, after) {
let a, b;
try {
a = JSON.parse(before);
b = JSON.parse(after);
} catch {
return false;
}
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
if (!isObject(a) || !isObject(b)) return false;
if (typeof a.version !== "string" || typeof b.version !== "string" || a.version === b.version) return false;
const { version: _before, ...restBefore } = a;
const { version: _after, ...restAfter } = b;
return JSON.stringify(restBefore) === JSON.stringify(restAfter);
}
/** A push's own diff (before..after) needs no runtime jobs when it is docs
* only or only bumps package.json's version. `show(rev, path)` reads a file. */
export function selectPushScope(files, show) {
if (!Array.isArray(files) || files.length === 0) return { runtime: true };
if (files.every((file) => areaOf(file) === "docs" && DOCS.test(file))) return { runtime: false };
if (files.length === 1 && files[0] === "package.json") {
try {
if (versionOnlyPackageJson(show("before", "package.json"), show("after", "package.json"))) return { runtime: false };
} catch { /* unreadable: run everything */ }
}
return { runtime: true };
}
// macOS runners are the scarce ones (five at a time for the whole account),
// and only a couple of test blocks are macOS-only, so a PR runs the suite on
// Linux, and on Windows only when its change can break Windows (windowsReason);
// a push to main and a merge group run Linux and Windows. The full lane adds
// macOS. A PR that can affect the macOS smokes still runs them.
export const ALL_OS = ["macos-latest", "ubuntu-latest", "windows-latest"];
export const LINUX_OS = ["ubuntu-latest"];
export const WINDOWS_OS = ["ubuntu-latest", "windows-latest"];
/** Events whose runs test every platform: the nightly run, manual runs and the release lane (a manual run). */
export const FULL_LANE_EVENTS = ["schedule", "workflow_dispatch"];
/** Which lane an event's run is: a pull request's own areas, main's, or every check on every platform. */
export function laneOf(event) {
if (event === "pull_request") return "pr";
if (event === "push" || event === "merge_group") return "main";
return "full";
}
// The slowest shard sets the run's length. Windows and macOS run only the e2e files that depend on their OS, so their
// shards stay as short as Linux's, and macOS keeps four: the account runs five macOS jobs at a time.
export const VITEST_SHARDS = { "macos-latest": 4, "ubuntu-latest": 8, "windows-latest": 8 };
// Test files at once on each platform's runner (OMB_VITEST_WORKERS, vite.config.ts). A Linux shard mostly waits on real
// servers and fake engines: its 4 vCPU sat 70% idle one file at a time, and four files take a third of the time. A
// Windows shard one file at a time took 9 to 14 minutes (Nov 10 2026) and set the length of every run that has one;
// its runner has 4 vCPU too, so it runs two files at once.
export const VITEST_WORKERS = { "macos-latest": 1, "ubuntu-latest": 4, "windows-latest": 2 };
/** ci.yml's vitest matrix: every shard of every selected platform, and which e2e files that platform runs. */
export function vitestMatrix(oses) {
return {
include: oses.flatMap((os) => Array.from({ length: VITEST_SHARDS[os] }, (_, i) => ({
os, part: `shard ${i + 1}/${VITEST_SHARDS[os]}`, shard: i + 1, shards: VITEST_SHARDS[os], files: "",
e2e: os === "ubuntu-latest" ? "all" : "host", node_tests: i === 0, workers: VITEST_WORKERS[os],
}))),
};
}
/** What ci.yml runs for a scope on an event whose suite covers `oses`: the
* selected job ids and the vitest matrix. */
export function ciOutputs({ checks }, oses) {
const platforms = oses.filter((os) => checks.includes(os === "ubuntu-latest" ? "linux-tests" : "host-tests"));
const { include } = vitestMatrix(platforms);
for (const [check, { part, files, shards, nodeTests }] of Object.entries(VITEST_SUBSETS)) {
if (platforms.includes("ubuntu-latest") || !checks.includes(check)) continue;
for (let shard = 1; shard <= shards; shard++) {
include.push({
os: "ubuntu-latest", part: shards > 1 ? `${part} ${shard}/${shards}` : part, shard, shards,
files: files.join(" "), e2e: "all", node_tests: nodeTests && shard === 1, workers: VITEST_WORKERS["ubuntu-latest"],
});
}
}
const jobs = SELECTABLE_JOBS.filter((job) => (job === "vitest" ? include.length > 0
: job === "windows-cua" ? checks.includes(job) && oses.includes("windows-latest") : checks.includes(job)));
return {
jobs,
vitest_os: oses,
// macOS's packaged server smoke runs in the macOS smokes job.
packaged_server_os: oses.filter((os) => os !== "macos-latest"),
// GitHub refuses an empty matrix, so a vitest job its `if` skips keeps the full one.
vitest_matrix: include.length > 0 ? { include } : vitestMatrix(oses),
};
}
/** The outputs for a static job that does the Linux work itself (on a Boat VM, `OMB_CI_LINUX_IN_STATIC`): the Linux
* vitest entries, behavior evals and the Linux packaged server smoke become `linux_lanes`
* (scripts/ci-linux-lanes.mjs), and the jobs left with nothing to run drop out of the selection. */
export function moveLinuxIntoStatic(outputs) {
const vitest = outputs.jobs.includes("vitest") ? outputs.vitest_matrix.include : [];
const others = vitest.filter((entry) => entry.os !== "ubuntu-latest");
const packagedOs = outputs.packaged_server_os.filter((os) => os !== "ubuntu-latest");
const packaged = outputs.jobs.includes("packaged-server");
return {
...outputs,
jobs: outputs.jobs.filter((job) => job !== "behavior-evals" && !(job === "vitest" && others.length === 0)
&& !(job === "packaged-server" && packagedOs.length === 0)),
// A job that drops out keeps its old matrix: GitHub refuses an empty one, even for a skipped job.
vitest_matrix: others.length > 0 ? { include: others } : outputs.vitest_matrix,
packaged_server_os: packaged && packagedOs.length > 0 ? packagedOs : outputs.packaged_server_os,
linux_lanes: {
vitest: vitest.filter((entry) => entry.os === "ubuntu-latest"),
evals: outputs.jobs.includes("behavior-evals"),
packaged_server: packaged && outputs.packaged_server_os.includes("ubuntu-latest"),
},
};
}
const SHA = /^[a-f\d]{40}(?:[a-f\d]{24})?$/i;
const git = (args) => execFileSync("git", args, { encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "pipe"] });
function changedFiles(range) {
// Disabling rename detection retains both old and new paths, including deletions.
const diff = git(["diff", "--name-only", "--no-renames", "-z", range, "--"]);
if (!diff || !diff.endsWith("\0")) throw new Error("Empty or invalid changed-path output");
return diff.slice(0, -1).split("\0");
}
/** The added and removed lines of a range's code, docs and translations left out. */
function changedCodeLines(range) {
return execFileSync("git", [
"diff", "-U0", "--no-renames", "--no-color", "--no-ext-diff", range, "--", ".", ":(exclude)*.md", ":(exclude)docs/**", ":(exclude)src/locales/**",
], { encoding: "utf8", timeout: 30_000, maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }).split("\n");
}
async function github(path) {
const { GH_TOKEN, GITHUB_REPOSITORY, GITHUB_API_URL = "https://api.github.com" } = process.env;
if (!GH_TOKEN || !GITHUB_REPOSITORY) throw new Error("GH_TOKEN and GITHUB_REPOSITORY are needed to read CI runs");
const response = await fetch(`${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}${path}`, {
headers: { authorization: `Bearer ${GH_TOKEN}`, accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28" },
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status} reading ${path.split("?")[0]}`);
return response.json();
}
const ciRuns = async (sha) => (await github(`/actions/workflows/ci.yml/runs?head_sha=${sha}&per_page=100`)).workflow_runs;
/** A pull request's labels as they are now, so a label added after the push counts when the run is re-run; the
* event's own list when GitHub cannot be read. */
async function pullRequestLabels(pr) {
const fromEvent = (Array.isArray(pr?.labels) ? pr.labels : []).map((label) => label?.name).filter((name) => typeof name === "string");
if (!Number.isSafeInteger(pr?.number) || !process.env.GH_TOKEN || !process.env.GITHUB_REPOSITORY) return fromEvent;
try {
return (await github(`/issues/${pr.number}/labels?per_page=100`)).map((label) => label.name);
} catch (error) {
console.warn(`CI scope: reading the pull request's labels from its event (${error.message})`);
return fromEvent;
}
}
/** Did the commit before this push get a green CI run of its own? Main keeps
* one run going and one waiting, and a push replaces the waiting run: if the
* previous commit's run was replaced, its changes were never tested, so this
* push must run main's checks even when its own diff is docs or a version. */
async function previousCommitGreen(sha) {
return (await ciRuns(sha)).some((run) => ["push", "merge_group", ...FULL_LANE_EVENTS].includes(run.event)
&& run.status === "completed" && run.conclusion === "success");
}
/** Did this exact commit pass the CI gate in the merge queue? The queue lands a merge group's own commit, so its run
* already tested what this push brings; the gate job is read because the push can come before the run has finished. */
async function passedMergeQueue(sha) {
for (const run of (await ciRuns(sha)).filter((candidate) => candidate.event === "merge_group")) {
const { jobs } = await github(`/actions/runs/${run.id}/jobs?per_page=100`);
if (jobs.some((job) => job.name === "CI" && job.conclusion === "success")) return true;
}
return false;
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const lane = laneOf(process.env.GITHUB_EVENT_NAME);
let scope = lane === "main" ? MAIN_SCOPE : FULL_SCOPE;
let vitestOs = lane === "main" ? WINDOWS_OS : ALL_OS;
if (lane === "pr") {
try {
const { pull_request: pr } = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
const base = pr?.base?.sha;
const head = pr?.head?.sha;
if (![base, head].every((sha) => typeof sha === "string" && SHA.test(sha))) {
throw new Error("Missing or invalid pull request commit SHAs");
}
const files = changedFiles(`${base}...${head}`);
scope = selectCiScope(files);
vitestOs = LINUX_OS;
if (scope.checks.includes("host-tests")) {
const why = windowsReason({
files,
labels: await pullRequestLabels(pr),
hostE2e: Object.keys((await import("./testing/host-e2e.ts")).HOST_E2E),
lines: () => changedCodeLines(`${base}...${head}`),
});
if (why) vitestOs = WINDOWS_OS;
console.log(why ? `CI scope: Windows runs for ${why}`
: `CI scope: nothing here can break Windows first, so its jobs wait for main (label the PR "${WINDOWS_LABEL}" and re-run to run them)`);
}
} catch (error) {
console.warn(`CI scope: using all checks because the pull request diff is unavailable: ${error.message}`);
}
} else if (process.env.GITHUB_EVENT_NAME === "push") {
// A push to main is scoped by its own commits; the release gate counts only
// a run of every platform's shards as tested.
try {
const { before, after } = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
if (![before, after].every((sha) => typeof sha === "string" && SHA.test(sha)) || /^0+$/.test(before)) {
throw new Error("Missing or invalid push commit SHAs");
}
const revs = { before, after };
const selected = selectPushScope(changedFiles(`${before}..${after}`), (rev, path) => git(["show", `${revs[rev]}:${path}`]));
if (await passedMergeQueue(after)) {
scope = QUEUED_SCOPE;
} else if (!selected.runtime && !(await previousCommitGreen(before))) {
console.warn(`CI scope: using main's checks because ${before} has no green CI run of its own`);
} else if (!selected.runtime) {
scope = { areas: ["docs or version only"], checks: ["build"] };
}
} catch (error) {
console.warn(`CI scope: using main's checks because the push diff or its CI is unavailable: ${error.message}`);
}
}
const linuxInStatic = process.env.OMB_CI_LINUX_IN_STATIC === "true";
const outputs = linuxInStatic ? moveLinuxIntoStatic(ciOutputs(scope, vitestOs)) : ciOutputs(scope, vitestOs);
console.log(`CI scope: ${scope.areas.join(", ")} -> ${outputs.jobs.join(", ") || "static checks only"}`);
if (linuxInStatic) {
const { vitest, evals, packaged_server: packaged } = outputs.linux_lanes;
console.log(`CI scope: this job runs ${vitest.length} Linux vitest lane(s)${evals ? ", behavior evals" : ""}${packaged ? ", the packaged server smoke" : ""}`);
}
appendFileSync(
process.env.GITHUB_OUTPUT,
Object.entries(outputs).map(([key, value]) => `${key}=${typeof value === "boolean" ? value : JSON.stringify(value)}\n`).join(""),
);
}