Select SQLite compatibility by explicit source, fixture, lifecycle, and script ownership while retaining every version in full qualification. Share the canonical scope inventory between planning and the required gate. Reuse recent exact-SHA full qualification only for hourly schedules, with trusted successful-run metadata and receipts. Force a daily refresh, reject stale original runs even after retries, and never renew proof from reused validation. Report attempt-specific queue, execution, and matched step timing in a separate completion workflow using trusted default-branch code and read-only API access. Preserve raw metadata and unknown timings without delaying validation or publication. Validation: 177 focused tests; typecheck/lint; knip; actionlint 1.7.7; all 1046 tests discovered; live 37-job API report smoke. Remote workflow scheduling and real-provider matrices remain CI verification.
255 lines
9.1 KiB
JavaScript
255 lines
9.1 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { appendFileSync } from "node:fs";
|
|
import { matchesGlob } from "node:path";
|
|
import { CI_FLAGS as flags, MANUAL_VERIFIERS, SCOPES } from "./scopes.mjs";
|
|
|
|
// Test files select the host/runtime lanes that execute their focused suites.
|
|
const HOST_TEST_OWNERS = ["apple", "package", "sqlite"];
|
|
// 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",
|
|
".github/workflows/ci-timings.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 (script === "test:sqlite" || script === "test:sqlite:run") {
|
|
selected.sqlite = true;
|
|
continue;
|
|
}
|
|
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;
|
|
if (!["pull_request", "push", "schedule", "workflow_dispatch"].includes(event))
|
|
throw new Error(`Unsupported CI event: ${event}`);
|
|
const requestedFull =
|
|
event === "schedule" ||
|
|
event === "workflow_dispatch" ||
|
|
process.env.CI_FULL === "true";
|
|
const tier =
|
|
event === "schedule" &&
|
|
process.env.CI_SKIP_FULL === "true" &&
|
|
process.env.CI_FULL !== "true"
|
|
? "reused"
|
|
: requestedFull
|
|
? "full"
|
|
: "fast";
|
|
let files = [];
|
|
if (tier === "reused") {
|
|
reasons.push(
|
|
"Recent exact-SHA full qualification reused; no fresh provider observation",
|
|
);
|
|
} else if (tier === "full") {
|
|
full("Full baseline requested by event or ci:full label");
|
|
} else if (event === "pull_request" || event === "push") {
|
|
const head = process.env.HEAD_SHA;
|
|
let base;
|
|
if (event === "pull_request") {
|
|
base = git("merge-base", process.env.BASE_SHA, head);
|
|
} else {
|
|
const candidate = process.env.BASE_SHA;
|
|
const baselineReason = process.env.BASELINE_REASON;
|
|
try {
|
|
if (!candidate || !/^[0-9a-f]{40}$/i.test(candidate))
|
|
throw new Error(
|
|
baselineReason || "No valid successful push baseline was found",
|
|
);
|
|
execFileSync("git", ["merge-base", "--is-ancestor", candidate, head], {
|
|
stdio: "ignore",
|
|
});
|
|
base = candidate;
|
|
} catch {
|
|
full(
|
|
baselineReason ||
|
|
"No valid successful push baseline ancestor; selecting all lanes",
|
|
);
|
|
}
|
|
}
|
|
if (base) {
|
|
// 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 HOST_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;
|
|
// full qualification retains 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}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
selected.runtime ||= selected.package;
|
|
const linux = { os: "ubuntu-latest", platform: "linux", architecture: "x64" };
|
|
// macOS x64 is the slowest package host. Fast qualification omits it.
|
|
const intel =
|
|
tier === "full"
|
|
? [{ 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, tier, matrix, files, reasons };
|
|
if (process.env.GITHUB_OUTPUT)
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
Object.entries({ ...selected, tier, 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 (${tier})\n\n${flags.filter((flag) => selected[flag]).join(", ") || "Formatting only"}\n\n${reasons.join("\n\n")}\n`,
|
|
);
|
|
console.log(JSON.stringify(plan));
|