Files
playbook/antigravity-awesome-skills/tools/scripts/pr_preflight.cjs
T
2026-07-29 07:59:17 +00:00

362 lines
12 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const sanitizeFilename = require("sanitize-filename");
const { resolveBlobSizes } = require("../lib/git-blob-sizes");
const { findProjectRoot } = require("../lib/project-root");
const { parseRawDiff } = require("../lib/git-raw-diff");
const {
classifyChangeRecords,
classifyChangedFiles,
classifyShadowImpact,
getDirectDerivedChanges,
hasIssueLink,
hasQualityChecklist,
loadWorkflowContract,
normalizeRepoPath,
requiresReferencesValidation,
} = require("../lib/workflow-contract");
function parseArgs(argv) {
const args = {
repo: null,
base: null,
head: "HEAD",
eventPath: null,
checkPolicy: false,
checkForkSafety: false,
noRun: false,
writeGithubOutput: false,
writeStepSummary: false,
json: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--repo") {
args.repo = argv[index + 1] || null;
index += 1;
} else if (arg === "--base") {
args.base = argv[index + 1];
index += 1;
} else if (arg === "--head") {
args.head = argv[index + 1];
index += 1;
} else if (arg === "--event-path") {
args.eventPath = argv[index + 1];
index += 1;
} else if (arg === "--check-policy") {
args.checkPolicy = true;
} else if (arg === "--check-fork-safety") {
args.checkForkSafety = true;
} else if (arg === "--no-run") {
args.noRun = true;
} else if (arg === "--write-github-output") {
args.writeGithubOutput = true;
} else if (arg === "--write-step-summary") {
args.writeStepSummary = true;
} else if (arg === "--json") {
args.json = true;
}
}
return args;
}
function safeUserPath(pathValue, baseDir = process.cwd()) {
if (path.isAbsolute(String(pathValue || ""))) {
return path.resolve(pathValue);
}
const root = path.resolve(baseDir);
const segments = String(pathValue || "").split(/[\\/]+/).filter(Boolean).map((segment) => {
const sanitized = sanitizeFilename(segment);
if (sanitized !== segment || !sanitized) {
throw new Error(`Unsafe path segment: ${segment}`);
}
return sanitized;
});
const target = path.resolve(root, ...segments);
const rel = path.relative(root, target);
if (rel.startsWith("..") || path.isAbsolute(rel)) {
throw new Error(`Path escapes allowed directory: ${pathValue}`);
}
return target;
}
function runGit(args, options = {}) {
const result = spawnSync("git", args, {
cwd: options.cwd,
encoding: "utf8",
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
});
if (result.error) {
throw result.error;
}
if (typeof result.status !== "number" || result.status !== 0) {
const stderr = options.capture ? result.stderr.trim() : "";
throw new Error(stderr || `git ${args.join(" ")} failed with status ${result.status}`);
}
return options.capture ? result.stdout.trim() : "";
}
function runCommand(command, args, cwd) {
console.log(`[pr:preflight] ${command} ${args.join(" ")}`);
const result = spawnSync(command, args, {
cwd,
stdio: "inherit",
shell: process.platform === "win32",
});
if (result.error) {
throw result.error;
}
if (typeof result.status !== "number" || result.status !== 0) {
process.exit(result.status || 1);
}
}
function resolveBaseRef(projectRoot) {
for (const candidate of ["origin/main", "main"]) {
const result = spawnSync("git", ["rev-parse", "--verify", candidate], {
cwd: projectRoot,
stdio: "ignore",
});
if (result.status === 0) {
return candidate;
}
}
return "HEAD";
}
function getChangeRecords(projectRoot, baseRef, headRef) {
if (baseRef === headRef) {
return [];
}
const result = spawnSync(
"git",
["diff", "--raw", "--no-abbrev", "-z", "-M", "--find-copies-harder", `${baseRef}...${headRef}`, "--"],
{
cwd: projectRoot,
encoding: null,
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 64 * 1024 * 1024,
},
);
if (result.error) throw result.error;
if (typeof result.status !== "number" || result.status !== 0) {
const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8").trim() : "";
throw new Error(stderr || `git diff failed with status ${result.status}`);
}
return parseRawDiff(result.stdout, { allowEmpty: true });
}
function getChangedFiles(projectRoot, baseRef, headRef) {
const records = getChangeRecords(projectRoot, baseRef, headRef);
return [...new Set(records.flatMap((record) => [record.old_path, record.new_path])
.filter(Boolean)
.map(normalizeRepoPath))];
}
function changedFilesFromRecords(records) {
return [...new Set(records.flatMap((record) => [record.old_path, record.new_path])
.filter(Boolean)
.map(normalizeRepoPath))];
}
function loadPullRequestEvent(eventPath) {
if (!eventPath) {
return null;
}
const rawEvent = fs.readFileSync(safeUserPath(eventPath), "utf8");
return JSON.parse(rawEvent).pull_request || null;
}
function evaluateForkSafety(projectRoot, changeRecords, pullRequest) {
if (!pullRequest) {
return { applicable: false, approvalSafe: true, reasons: [], requiresHumanReview: false };
}
const headRepository = String(pullRequest?.head?.repo?.full_name || "").toLowerCase();
const baseRepository = String(pullRequest?.base?.repo?.full_name || "").toLowerCase();
if (!headRepository || !baseRepository) {
return {
applicable: true,
approvalSafe: false,
reasons: ["pull_request_repository_identity_unavailable"],
requiresHumanReview: false,
};
}
if (headRepository === baseRepository) {
return { applicable: false, approvalSafe: true, reasons: [], requiresHumanReview: false };
}
const preliminary = classifyChangeRecords(changeRecords, { requireBlobSizes: false });
if (!preliminary.approvalSafe) {
return { applicable: true, ...preliminary };
}
const blobSizes = resolveBlobSizes(projectRoot, changeRecords);
return { applicable: true, ...classifyChangeRecords(changeRecords, { blobSizes }) };
}
function appendGithubOutput(result) {
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) {
return;
}
const lines = [
`primary_category=${result.primaryCategory}`,
`categories=${result.categories.join(",")}`,
`requires_references=${String(result.requiresReferencesValidation)}`,
`direct_derived_changes_count=${String(result.directDerivedChanges.length)}`,
`direct_derived_changes=${JSON.stringify(result.directDerivedChanges)}`,
`changed_files_count=${String(result.changedFiles.length)}`,
`has_quality_checklist=${String(result.prBody.hasQualityChecklist)}`,
`has_issue_link=${String(result.prBody.hasIssueLink)}`,
`fork_safety_applicable=${String(result.forkSafety.applicable)}`,
`fork_approval_safe=${String(result.forkSafety.approvalSafe)}`,
`fork_safety_reasons=${JSON.stringify(result.forkSafety.reasons)}`,
`impact_profile=${result.shadowImpact.profile}`,
`impact_reasons=${JSON.stringify(result.shadowImpact.reasons)}`,
];
fs.appendFileSync(outputPath, `${lines.join("\n")}\n`, "utf8");
}
function appendStepSummary(result) {
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
if (!summaryPath) {
return;
}
const derivedSummary =
result.directDerivedChanges.length === 0
? "none"
: result.directDerivedChanges.map((filePath) => `\`${filePath}\``).join(", ");
const lines = [
"## PR Workflow Intake",
"",
`- Primary change: \`${result.primaryCategory}\``,
`- Categories: ${result.categories.length > 0 ? result.categories.map((category) => `\`${category}\``).join(", ") : "\`none\`"}`,
`- Changed files: ${result.changedFiles.length}`,
`- Direct derived-file edits: ${derivedSummary}`,
`- \`validate:references\` required: ${result.requiresReferencesValidation ? "yes" : "no"}`,
`- PR template checklist: ${result.prBody.hasQualityChecklist ? "present" : "missing"}`,
`- Issue auto-close link: ${result.prBody.hasIssueLink ? "detected" : "not detected"}`,
`- Fork approval safety: ${result.forkSafety.applicable ? (result.forkSafety.approvalSafe ? "safe" : "blocked") : "not applicable"}`,
`- Shadow impact profile: \`${result.shadowImpact.profile}\` (observational only; no test is skipped)`,
"",
"> Generated drift is reported separately in the artifact preview job and remains informational on pull requests.",
];
fs.appendFileSync(summaryPath, `${lines.join("\n")}\n`, "utf8");
}
function main() {
const args = parseArgs(process.argv.slice(2));
const projectRoot = args.repo ? path.resolve(args.repo) : findProjectRoot(__dirname);
const contract = loadWorkflowContract(__dirname);
const baseRef = args.base || resolveBaseRef(projectRoot);
const changeRecords = getChangeRecords(projectRoot, baseRef, args.head);
const changedFiles = changedFilesFromRecords(changeRecords);
const classification = classifyChangedFiles(changedFiles, contract);
const directDerivedChanges = getDirectDerivedChanges(changedFiles, contract);
const pullRequest = loadPullRequestEvent(args.eventPath);
const pullRequestBody = pullRequest?.body ?? null;
const forkSafety = evaluateForkSafety(projectRoot, changeRecords, pullRequest);
const shadowImpact = classifyShadowImpact(changeRecords, contract);
const result = {
baseRef,
headRef: args.head,
changeRecords,
changedFiles,
categories: classification.categories,
primaryCategory: classification.primaryCategory,
directDerivedChanges,
requiresReferencesValidation: requiresReferencesValidation(changedFiles, contract),
forkSafety,
shadowImpact,
prBody: {
available: pullRequestBody !== null,
hasQualityChecklist: hasQualityChecklist(pullRequestBody),
hasIssueLink: hasIssueLink(pullRequestBody),
},
};
if (args.writeGithubOutput) {
appendGithubOutput(result);
}
if (args.writeStepSummary) {
appendStepSummary(result);
}
if (args.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} else {
console.log(`[pr:preflight] Base ref: ${baseRef}`);
console.log(`[pr:preflight] Changed files: ${changedFiles.length}`);
console.log(
`[pr:preflight] Classification: ${result.categories.length > 0 ? result.categories.join(", ") : "none"}`,
);
}
if (args.checkPolicy) {
if (directDerivedChanges.length > 0) {
console.error(
[
"Pull requests are source-only.",
"Remove derived files from the PR and let main regenerate them after merge.",
`Derived files detected: ${directDerivedChanges.join(", ")}`,
].join(" "),
);
process.exit(1);
}
if (pullRequestBody !== null && !result.prBody.hasQualityChecklist) {
console.error("PR body must include the Quality Bar Checklist section from the template.");
process.exit(1);
}
}
if (args.checkForkSafety && forkSafety.applicable && !forkSafety.approvalSafe) {
console.error(`Fork PR is not approval-safe: ${forkSafety.reasons.slice(0, 12).join(", ") || "unclassified diff"}.`);
process.exit(1);
}
if (!args.noRun) {
runCommand("npm", ["run", "validate"], projectRoot);
if (result.requiresReferencesValidation) {
runCommand("npm", ["run", "validate:references"], projectRoot);
}
runCommand("npm", ["run", "test"], projectRoot);
}
}
if (require.main === module) {
main();
}
module.exports = {
changedFilesFromRecords,
evaluateForkSafety,
getChangeRecords,
getChangedFiles,
loadPullRequestEvent,
parseArgs,
};