* feat(process): add owned debugger shutdown lifecycle * feat(reverse): add local inspection commands * feat(gdb): add persistent debugger integration * feat(cutter): add Cutter bridge integration * test(reverse): enforce complete integration coverage * fix(process): preserve debugger shutdown ownership * fix(reverse): retain evidence across interrupted commands * fix(gdb): honor captured provider environment * test(process): cover debugger shutdown retries * fix(reverse): retain evidence for interrupted providers * test(process): cover debugger shutdown retries * fix(reverse): retain evidence for interrupted providers * fix(server): snapshot provider environment at creation * fix(reverse): close review gaps with complete coverage * fix(reverse): address inspection review findings * fix(gdb): bound sessions and retain cleanup ownership * fix(ci): require reverse-tools quality lane * fix(process): unref inherited debugger output pipes * fix(reverse): address remaining PR 1960 findings * fix(gdb): address remaining PR 1961 findings * fix(reverse): account for unrestricted provider execution * fix(process): retain debugger ownership until safe shutdown * fix(process): preserve replacement debugger sessions * fix(reverse): preserve admission and output provenance * fix(process): retain exited debugger until output settles * fix(mcp): bound provider admission and debugger cleanup * fix(reverse): preserve failure provenance and classify E2BIG * fix(gdb): preserve startup cleanup and detach safety * fix(process): retain debugger ownership after detach failure * fix(reverse): preserve mutation and failure provenance * fix(gdb): address debugger review findings * fix(reverse): rebind sessions after external mutation * fix(process): cap debugger output after exit * fix(gdb): protect one-shot detach verification * feat(debugger): add reverse tool coverage gate * test(debugger): cover integrated failure paths * fix(gdb): bound and preserve session observations * fix(gdb): address review regressions * style: format merged integration files * fix(reverse): preserve session and output evidence * refactor(gdb): share repeated test and partial result setup * fix(gdb): invalidate TTY proof for Python aliases * fix(reverse): retain failure output byte counts * test(process): synchronize late debugger output * fix(gdb): invalidate TTY proof for wrapped commands * fix(gdb): recognize wit command abbreviation * test(reverse): wait for cancelled descendant readiness * test(reverse): bound descendant cancellation cleanup * style(reverse): format integration tests and adapters * style: format reverse engineering integration files * test(reverse): remove duplicate environment harness * test(gdb): share session startup assertions * fix(gdb): invalidate output proof for command abbreviations * test(reverse): share composition edge case scenarios * fix(ci): recognize verification and mutation tool entries * test(rizin): cover successful fake inferior detach * test(cli): align full output failure envelope * test(cli): isolate reverse command lifecycle service * test(process): share debugger output fixture setup * fix(binary): preserve high-bit Windows signature bytes * style(binary): format Windows signature parser * test(mcp): preserve call coverage in unchanged comparison * fix(ci): include reverse tools Vitest configs in lint cache * fix(mcp): preserve target content on delivery errors * ci: align duplication ceiling with expanded suite * test(reverse): allow signal admission mutants * fix(reverse): bound retained cleanup failures to process owners * fix(test): replace drifting mutation ranges with owned selectors * fix(reverse): admit persistent commands and retain cleanup ownership * fix(cutter): recover timed-out commands from exact receipts * fix(cutter): reject unsupported bridge request kinds * fix(ci): repair client format and shared verification fixtures * fix(native): preserve debugger cleanup ownership and bridge authentication * test(native): retain diagnostics without perfect coverage requirements * chore(licenses): refresh locked dependency inventory and notices * fix(test): exclude transient command locks from mutation sandboxes * fix(gdb): admit only platforms with safe detach verification * fix(cutter): retain bounded completion receipts until acknowledgement * fix(cutter): acknowledge commands rejected before UI dispatch * test(reverse): fail cancellation checks before fixture teardown * test(gdb): clean up the retained broken-stdin fixture owner * docs(test): clarify focused provider verification scope * fix(rizin): retain close cleanup failures * fix(gdb): wait for inferior stop and bound MI buffering * fix(cutter): retain failed receipt acknowledgements * fix(rizin): return retained session identity on rebind failure * fix(gdb): preserve startup cancellation and lossy status * fix(rizin): retain launch provenance and assemble frames once * fix(cutter): recover receipts across callers and bound discovery * docs: describe debugger evidence and bridge recovery * fix(process): keep cleanup incomplete while writers remain Stopping a debugger no longer reports success when inherited output streams stay open, so BinarySession cannot reopen a binary a child is still changing. Concurrent Cutter cleanup retries share one receipt acknowledgement instead of treating a consumed receipt as failure. * fix(debuggers): recover cleanup with captured process identities * fix(analysis): retire sessions after unrestricted commands * fix(cli): preserve completed evidence after cleanup recovery * fix(cutter): bound frame collection and coalesce cleanup * fix(windows): resolve selected provider paths absolutely * fix(gdb): report decoding loss in failed startup evidence * fix(rizin): scope decoding loss to collected command frames * test(mcp): cover snapshot retirement in reverse tools CI --------- Co-authored-by: Daniel Gonçalves Araujo <47118053+dandgabr@users.noreply.github.com> Co-authored-by: REA Maintainers <noreply@github.com> Co-authored-by: OpenAI Codex <codex@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
279 lines
9.9 KiB
JavaScript
279 lines
9.9 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";
|
|
import { planTestSelection } from "./test-selection.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/pr-title.yml",
|
|
".github/workflows/ci-full-pr.yml",
|
|
".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/**"];
|
|
// These reporters affect CI output only; they do not select a host or provider
|
|
// verification lane when changed on their own.
|
|
const PORTABLE_CI_REPORTERS = new Set([
|
|
"scripts/ci/job-timings.mjs",
|
|
"scripts/ci/job-timings-batch.mjs",
|
|
"scripts/ci/test-report.mjs",
|
|
"scripts/ci/test-timing-reporter.mjs",
|
|
"scripts/ci/test-history.mjs",
|
|
]);
|
|
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:(coverage|mutation):reverse-tools$/.test(script)) {
|
|
selected.reverse_tools = true;
|
|
continue;
|
|
}
|
|
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 (matchesAny(file, SCOPES.reverse_tools)) selected.reverse_tools = true;
|
|
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 (PORTABLE_CI_REPORTERS.has(file)) 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 testSelection = planTestSelection(files, event, tier);
|
|
const plan = { ...selected, tier, matrix, files, reasons, testSelection };
|
|
if (process.env.GITHUB_OUTPUT)
|
|
appendFileSync(
|
|
process.env.GITHUB_OUTPUT,
|
|
Object.entries({
|
|
...selected,
|
|
tier,
|
|
matrix: JSON.stringify(matrix),
|
|
test_selection: JSON.stringify(testSelection),
|
|
})
|
|
.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));
|