* ci: narrow native-host lane selection and reuse build caches Unowned src/ and bridge/ files no longer fall back to the full baseline. They run the Linux suite plus the Linux x64 installed-package/Inspector baseline. New apple and package scopes own the Apple artifact lane, the package matrix and the Windows curated lane, including the test files those lanes invoke. Manual-only verifier entrypoints run static checks and the Linux suite. CHANGELOG, SECURITY, GitHub templates and the native Windows README are documentation-only. Pull requests omit the macOS x64 package host unless labelled ci:full. The Windows and Apple lanes restore the shared Turbo build cache, the Windows lane drops its duplicate typecheck, and the Linux suite uses six shards. Main pushes keep the full baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: match package script entrypoints against scope globs Script-only package.json edits resolved their entrypoint with literal equality, so glob-owned verifiers such as scripts/verify/apple/** and scripts/verify/inspector/** fell back to the full baseline instead of selecting their owning lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
8.1 KiB
JavaScript
228 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",
|
|
]);
|
|
// 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));
|