Ctrl K
ComunidadGuías…

scripts/ci-scope.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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(""),
  );
}