mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
dad42879c7
### Summary The include-mode filter in `check-changes.js` short-circuits to "skip" when no changed files are under `crates/`, so it never reaches the cargo tree dependency check for `Cargo.lock`-only dependency updates or `rust-toolchain.toml` bumps. Conformance, Minsize, Allocations, and Linter timings are all gated on this filter. This is how #24192 (`num-bigint` 0.4.6 → 0.5.1) landed with a stale parser allocation snapshot: the update changed BigInt-literal parsing allocation counts, the Allocations job never re-measured on main, and the mismatch surfaced on an unrelated PR (#24098). The snapshot itself is fixed in #24198. This PR treats `Cargo.lock` and `rust-toolchain.toml` changes as affecting every crate, so measurement jobs always re-run on dependency and toolchain updates. The cost is that renovate dependency PRs now run these four jobs; dependency updates are exactly the class of change that can shift measured behavior, so that is the intended behavior. Exclude-mode and paths-only jobs are unaffected (exclude mode already runs on such changes by construction). ### Trade-off For Conformance specifically this closes a correctness gap, not just a snapshot-staleness one: a dependency update that changes parser/transformer behavior currently skips conformance entirely. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
220 lines
6.7 KiB
JavaScript
220 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// oxlint-disable no-console
|
|
|
|
/**
|
|
* Generic change detection script for CI jobs.
|
|
*
|
|
* Include mode: checks if changes affect specified crates or their transitive dependencies.
|
|
* node check-changes.js --packages oxc_minifier --paths tasks/minsize/
|
|
*
|
|
* Exclude mode: skips only if ALL changed files belong to excluded crate directories.
|
|
* node check-changes.js --exclude oxc_linter,oxc_language_server --paths napi/,npm/
|
|
*
|
|
* Paths-only mode: run if any changed file matches the given paths (no cargo tree).
|
|
* node check-changes.js --paths apps/oxlint/,npm/oxlint/,napi/oxlint/
|
|
*/
|
|
|
|
const { getChangedFiles } = require("./get-changed-files.js");
|
|
const { getCrateDependencies, checkFilesAffectCrates } = require("./utils.js");
|
|
|
|
/**
|
|
* Parse CLI arguments into structured options.
|
|
* @returns {{ packages: string[], exclude: string[], paths: string[] }}
|
|
*/
|
|
function parseArgs() {
|
|
const args = process.argv.slice(2);
|
|
const result = { packages: [], exclude: [], paths: [] };
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === "--packages" && args[i + 1]) {
|
|
result.packages = args[++i].split(",").filter(Boolean);
|
|
} else if (arg === "--exclude" && args[i + 1]) {
|
|
result.exclude = args[++i].split(",").filter(Boolean);
|
|
} else if (arg === "--paths" && args[i + 1]) {
|
|
result.paths = args[++i].split(",").filter(Boolean);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Include mode: run if changes affect any of the specified crates (+ transitive deps) or paths.
|
|
* @param {string[] | null} changedFiles
|
|
* @param {string[]} packages - Root crate names
|
|
* @param {string[]} paths - Additional trigger paths
|
|
* @returns {boolean}
|
|
*/
|
|
function shouldRunInclude(changedFiles, packages, paths) {
|
|
if (changedFiles === null) {
|
|
console.error("No changed files list (manual trigger or error) - will run");
|
|
return true;
|
|
}
|
|
|
|
if (changedFiles.length === 0) {
|
|
console.error("No files changed - will skip");
|
|
return false;
|
|
}
|
|
|
|
// Check additional trigger paths first (no cargo tree needed)
|
|
for (const file of changedFiles) {
|
|
for (const p of paths) {
|
|
if (file.startsWith(p)) {
|
|
console.error(`File ${file} matches trigger path ${p} - will run`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dependency or toolchain updates can change any crate's behavior
|
|
// (e.g. num-bigint 0.5 changed parser allocation counts) — always run.
|
|
if (
|
|
changedFiles.some(
|
|
(file) => file === "Cargo.lock" || file === "rust-toolchain.toml" || file === "Cargo.toml",
|
|
)
|
|
) {
|
|
console.error("Cargo.lock or rust-toolchain.toml changed - will run");
|
|
return true;
|
|
}
|
|
|
|
// If no changed files are in crates/, cargo tree check cannot match — skip it
|
|
if (!changedFiles.some((file) => file.startsWith("crates/"))) {
|
|
console.error("No files changed in crates/ - will skip");
|
|
return false;
|
|
}
|
|
|
|
// Resolve transitive dependencies via cargo tree
|
|
let allCrates;
|
|
try {
|
|
const deps = getCrateDependencies(packages);
|
|
allCrates = [...packages, ...deps];
|
|
console.error(`Checking crates (${allCrates.length}): ${allCrates.join(", ")}`);
|
|
} catch (error) {
|
|
console.error(`cargo tree failed: ${error.message} - will run as fallback`);
|
|
return true;
|
|
}
|
|
|
|
if (allCrates.length === 0) {
|
|
console.error("Warning: No crates resolved - will run as fallback");
|
|
return true;
|
|
}
|
|
|
|
return checkFilesAffectCrates(changedFiles, allCrates);
|
|
}
|
|
|
|
/**
|
|
* Exclude mode: skip only if ALL changed files belong to excluded crate directories.
|
|
* Files matching `paths` always trigger the job. No cargo tree — exclusion is intentionally shallow.
|
|
* @param {string[] | null} changedFiles
|
|
* @param {string[]} excludeCrates - Crate names to exclude
|
|
* @param {string[]} paths - Additional paths that always trigger the job
|
|
* @returns {boolean}
|
|
*/
|
|
function shouldRunExclude(changedFiles, excludeCrates, paths) {
|
|
if (changedFiles === null) {
|
|
console.error("No changed files list (manual trigger or error) - will run");
|
|
return true;
|
|
}
|
|
|
|
if (changedFiles.length === 0) {
|
|
console.error("No files changed - will skip");
|
|
return false;
|
|
}
|
|
|
|
// Check always-trigger paths first
|
|
for (const file of changedFiles) {
|
|
for (const p of paths) {
|
|
if (file.startsWith(p)) {
|
|
console.error(`File ${file} matches always-trigger path ${p} - will run`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const excludePaths = excludeCrates.map((c) => `crates/${c}/`);
|
|
|
|
// Skip only if EVERY changed file is inside an excluded crate directory
|
|
const allExcluded = changedFiles.every((file) => excludePaths.some((ep) => file.startsWith(ep)));
|
|
|
|
if (allExcluded) {
|
|
console.error(
|
|
`All ${changedFiles.length} changed files are in excluded crates (${excludeCrates.join(", ")}) - will skip`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
console.error("Changes found outside excluded crates - will run");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Paths-only mode: run if any changed file matches any of the given paths.
|
|
* Used by jobs that don't need cargo tree dependency checking.
|
|
*/
|
|
function shouldRunPathsOnly(changedFiles, paths) {
|
|
if (changedFiles === null) {
|
|
console.error("No changed files list (manual trigger or error) - will run");
|
|
return true;
|
|
}
|
|
|
|
if (changedFiles.length === 0) {
|
|
console.error("No files changed - will skip");
|
|
return false;
|
|
}
|
|
|
|
for (const file of changedFiles) {
|
|
for (const p of paths) {
|
|
if (file.startsWith(p)) {
|
|
console.error(`File ${file} matches trigger path ${p} - will run`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.error("No files match trigger paths - will skip");
|
|
return false;
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
const opts = parseArgs();
|
|
|
|
if (opts.packages.length === 0 && opts.exclude.length === 0 && opts.paths.length === 0) {
|
|
console.error("Error: must specify --packages, --exclude, or --paths");
|
|
console.log("true");
|
|
process.exit(0);
|
|
}
|
|
|
|
const changedFiles = await getChangedFiles();
|
|
|
|
let shouldRun;
|
|
if (opts.exclude.length > 0) {
|
|
shouldRun = shouldRunExclude(changedFiles, opts.exclude, opts.paths);
|
|
} else if (opts.packages.length > 0) {
|
|
shouldRun = shouldRunInclude(changedFiles, opts.packages, opts.paths);
|
|
} else {
|
|
shouldRun = shouldRunPathsOnly(changedFiles, opts.paths);
|
|
}
|
|
|
|
console.log(shouldRun ? "true" : "false");
|
|
|
|
if (shouldRun) {
|
|
console.error("::notice title=Change detection::Will run this job");
|
|
} else {
|
|
console.error("::notice title=Change detection::Will skip this job");
|
|
}
|
|
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error("Error in change detection:", error);
|
|
// On error, run as a fallback
|
|
console.log("true");
|
|
console.error("::warning title=Change detection error::Error occurred, running as fallback");
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
void main();
|