229 lines
8.1 KiB
JavaScript
229 lines
8.1 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { appendFileSync } from "node:fs";
|
|
import { matchesGlob } from "node:path";
|
|
import { MANUAL_VERIFIERS, SCOPES } from "./scopes.mjs";
|
|
|
|
const flags = [
|
|
...new Set([
|
|
"code",
|
|
"runtime",
|
|
"package",
|
|
"apple",
|
|
"fixtures",
|
|
"readiness",
|
|
"docs",
|
|
"workflows",
|
|
...Object.keys(SCOPES),
|
|
]),
|
|
];
|
|
// Test files select the native-host lanes that run them explicitly. Provider
|
|
// scopes remain implementation-owned, so test-only edits stay off those lanes.
|
|
const NATIVE_TEST_OWNERS = ["apple", "package"];
|
|
// Actionlint validates workflow syntax only. The semantic suite asserts these
|
|
// workflows' release, publication and self-hosted runner safeguards.
|
|
const GUARDED_WORKFLOWS = new Set([
|
|
".github/workflows/release.yml",
|
|
".github/workflows/real-ghidra-windows.yml",
|
|
".github/workflows/sqlite-runtime.yml",
|
|
]);
|
|
// Sources compiled by build-conformance-fixtures.mjs. The readiness journey
|
|
// analyzes the built C fixture as well as its own applications.
|
|
const FIXTURE_SOURCES = [
|
|
"tests/conformance/c/**",
|
|
"tests/conformance/napi/**",
|
|
"tests/conformance/objc/**",
|
|
"tests/conformance/swift/**",
|
|
"tests/conformance/versions/**",
|
|
];
|
|
const READINESS_SOURCES = ["tests/conformance/readiness/**"];
|
|
const matchesAny = (file, patterns) =>
|
|
patterns.some((pattern) => matchesGlob(file, pattern));
|
|
const selected = Object.fromEntries(flags.map((flag) => [flag, false]));
|
|
const reasons = [];
|
|
const full = (reason) => {
|
|
for (const flag of flags) selected[flag] = true;
|
|
reasons.push(reason);
|
|
};
|
|
const git = (...args) =>
|
|
execFileSync("git", args, { encoding: "utf8" }).trimEnd();
|
|
function classifyPackage(base, head) {
|
|
const before = JSON.parse(git("show", `${base}:package.json`));
|
|
const after = JSON.parse(git("show", `${head}:package.json`));
|
|
const { scripts: beforeScripts, ...beforeRest } = before;
|
|
const { scripts: afterScripts, ...afterRest } = after;
|
|
if (JSON.stringify(beforeRest) !== JSON.stringify(afterRest)) {
|
|
full("Package metadata or dependencies changed");
|
|
return;
|
|
}
|
|
selected.code = true;
|
|
const changed = [
|
|
...new Set([
|
|
...Object.keys(beforeScripts ?? {}),
|
|
...Object.keys(afterScripts ?? {}),
|
|
]),
|
|
].filter((key) => beforeScripts?.[key] !== afterScripts?.[key]);
|
|
for (const script of changed) {
|
|
if (
|
|
/^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip$|jscpd$)/.test(
|
|
script,
|
|
)
|
|
)
|
|
continue;
|
|
if (
|
|
["compile:termux", "build:termux", "build:termux:run"].includes(script)
|
|
) {
|
|
selected.workflows = true;
|
|
reasons.push(
|
|
`Android build script changed; real Android verification remains manual: ${script}`,
|
|
);
|
|
continue;
|
|
}
|
|
// Verify scripts own provider setup as well as execution. Reuse their
|
|
// existing source-file ownership rather than maintaining a second map.
|
|
const entrypoint = (command) =>
|
|
/^(?:npm run build:cached && )?node (scripts\/[\w/-]+\.mjs)(?: [\w./=-]+)*$/.exec(
|
|
command ?? "",
|
|
)?.[1];
|
|
const beforeEntry = entrypoint(beforeScripts?.[script]);
|
|
const afterEntry = entrypoint(afterScripts?.[script]);
|
|
const entries = [beforeEntry, afterEntry].filter(Boolean);
|
|
const simple =
|
|
(!beforeScripts?.[script] || beforeEntry) &&
|
|
(!afterScripts?.[script] || afterEntry);
|
|
const scriptOwners =
|
|
simple &&
|
|
entries.every((entry) =>
|
|
Object.values(SCOPES).some((patterns) => matchesAny(entry, patterns)),
|
|
)
|
|
? Object.entries(SCOPES)
|
|
.filter(([, patterns]) =>
|
|
entries.some((entry) => matchesAny(entry, patterns)),
|
|
)
|
|
.map(([owner]) => owner)
|
|
: [];
|
|
if (scriptOwners.length)
|
|
for (const owner of scriptOwners) selected[owner] = true;
|
|
else full(`Shared or unknown package script: ${script}`);
|
|
}
|
|
}
|
|
|
|
const event = process.env.GITHUB_EVENT_NAME;
|
|
let files = [];
|
|
if (
|
|
event === "push" ||
|
|
event === "schedule" ||
|
|
event === "workflow_dispatch" ||
|
|
process.env.CI_FULL === "true"
|
|
) {
|
|
full("Full baseline requested by event or ci:full label");
|
|
} else if (event === "pull_request") {
|
|
const base = git("merge-base", process.env.BASE_SHA, process.env.HEAD_SHA);
|
|
const head = process.env.HEAD_SHA;
|
|
// Disable rename detection so both the removed and added paths participate.
|
|
files = git("diff", "--name-only", "--no-renames", "-z", base, head)
|
|
.split("\0")
|
|
.filter(Boolean);
|
|
for (const file of files) {
|
|
if (
|
|
/^(README(?:_[^/]+)?\.md|AGENTS\.md|CONTRIBUTING\.md|CHANGELOG\.md|SECURITY\.md|native\/windows\/README\.md|\.github\/pull_request_template\.md)$/.test(
|
|
file,
|
|
) ||
|
|
file.startsWith("docs/") ||
|
|
file.startsWith(".github/ISSUE_TEMPLATE/")
|
|
) {
|
|
selected.docs = true;
|
|
selected.website ||= matchesAny(file, SCOPES.website);
|
|
continue;
|
|
}
|
|
if (/^(src|tests)\/.*\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file)) {
|
|
selected.code = true;
|
|
for (const owner of NATIVE_TEST_OWNERS)
|
|
selected[owner] ||= matchesAny(file, SCOPES[owner]);
|
|
continue;
|
|
}
|
|
if (matchesAny(file, MANUAL_VERIFIERS)) {
|
|
selected.code = true;
|
|
continue;
|
|
}
|
|
const owners = Object.entries(SCOPES)
|
|
.filter(([, patterns]) =>
|
|
patterns.some((pattern) => matchesGlob(file, pattern)),
|
|
)
|
|
.map(([owner]) => owner);
|
|
for (const owner of owners) selected[owner] = true;
|
|
if (
|
|
file.startsWith(".github/workflows/") ||
|
|
file === ".github/actionlint.yaml"
|
|
) {
|
|
selected.workflows = true;
|
|
if (GUARDED_WORKFLOWS.has(file)) selected.code = true;
|
|
if (file !== ".github/workflows/ci.yml") continue;
|
|
}
|
|
if (owners.length === 1 && owners[0] === "website") continue;
|
|
if (/^src\/(contracts|domain)\//.test(file)) selected.docs = true;
|
|
if (file === "package.json") {
|
|
classifyPackage(base, head);
|
|
continue;
|
|
}
|
|
selected.code = true;
|
|
if (matchesAny(file, FIXTURE_SOURCES)) {
|
|
selected.fixtures = true;
|
|
selected.readiness = true;
|
|
}
|
|
if (matchesAny(file, READINESS_SOURCES)) selected.readiness = true;
|
|
if (file.startsWith("tests/")) continue;
|
|
if (
|
|
/^(src\/process\/|src\/filesystem\/|src\/contracts\/tool|src\/server\/createServer|src\/cli\.ts|scripts\/ci\/)/.test(
|
|
file,
|
|
)
|
|
) {
|
|
full(`Shared boundary: ${file}`);
|
|
} else if (owners.length) {
|
|
if (file.startsWith("src/") || file.startsWith("bridge/"))
|
|
selected.runtime = true;
|
|
} else if (/^(src|bridge)\//.test(file)) {
|
|
// Unowned runtime sources keep the Linux x64 installed-package baseline;
|
|
// ci:full, main pushes and manual dispatch retain every native host.
|
|
selected.runtime = true;
|
|
reasons.push(`Unowned runtime source: ${file}`);
|
|
} else if (file.startsWith(".agents/skills/")) {
|
|
selected.runtime = true;
|
|
selected.docs = true;
|
|
} else {
|
|
full(`Unclassified input: ${file}`);
|
|
}
|
|
}
|
|
} else throw new Error(`Unsupported CI event: ${event}`);
|
|
selected.runtime ||= selected.package;
|
|
const linux = { os: "ubuntu-latest", platform: "linux", architecture: "x64" };
|
|
// macOS x64 is the slowest package host. Pull requests run it only with ci:full;
|
|
// main pushes, schedules and manual dispatch always include it.
|
|
const intel =
|
|
event !== "pull_request" || process.env.CI_FULL === "true"
|
|
? [{ os: "macos-15-intel", platform: "darwin", architecture: "x64" }]
|
|
: [];
|
|
const matrix = {
|
|
include: selected.package
|
|
? [
|
|
linux,
|
|
{ os: "ubuntu-24.04-arm", platform: "linux", architecture: "arm64" },
|
|
{ os: "macos-15", platform: "darwin", architecture: "arm64" },
|
|
...intel,
|
|
]
|
|
: [linux],
|
|
};
|
|
const plan = { ...selected, matrix, files, reasons };
|
|
if (process.env.GITHUB_OUTPUT)
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
Object.entries({ ...selected, matrix: JSON.stringify(matrix) })
|
|
.map(([key, value]) => `${key}=${value}\n`)
|
|
.join(""),
|
|
);
|
|
if (process.env.GITHUB_STEP_SUMMARY)
|
|
appendFileSync(
|
|
process.env.GITHUB_STEP_SUMMARY,
|
|
`### Selected CI lanes\n\n${flags.filter((flag) => selected[flag]).join(", ") || "Formatting only"}\n\n${reasons.join("\n\n")}\n`,
|
|
);
|
|
console.log(JSON.stringify(plan));
|