ai-chatbot/scripts/check-branch-readiness.mjs

134 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const args = process.argv.slice(2);
const currentBranch = git(["branch", "--show-current"]).trim() || "HEAD";
const configuredDeploymentBranch = getOptionalGitConfig("deployment.branch");
const requiredBranch =
String(process.env.REQUIRED_BRANCH || configuredDeploymentBranch || "").trim();
const positionalBaseRef = args.find((arg) => !arg.startsWith("--"));
const upstreamRef = getOptionalUpstreamRef();
const baseRef =
process.env.PR_BASE_REF || positionalBaseRef || upstreamRef || "origin/main";
const requireUpToDate =
process.env.REQUIRE_UP_TO_DATE === "true" || args.includes("--require-up-to-date");
const allowDirty = process.env.ALLOW_DIRTY === "true" || args.includes("--allow-dirty");
const failures = [];
const warnings = [];
const status = git(["status", "--porcelain"]);
if (status.trim() && !allowDirty) {
failures.push("working tree has uncommitted changes; commit, stash, or set ALLOW_DIRTY=true");
}
if (requiredBranch && currentBranch !== requiredBranch) {
failures.push(
`current branch is ${currentBranch}, but deployment requires ${requiredBranch}`
);
}
try {
git(["rev-parse", "--verify", baseRef]);
} catch {
failures.push(`base ref not found: ${baseRef}. Run \`git fetch\` or set PR_BASE_REF.`);
}
if (failures.length === 0) {
const mergeBase = git(["merge-base", "HEAD", baseRef]).trim();
const [behindText, aheadText] = git(["rev-list", "--left-right", "--count", `${baseRef}...HEAD`])
.trim()
.split(/\s+/);
const behind = Number.parseInt(behindText || "0", 10);
const ahead = Number.parseInt(aheadText || "0", 10);
const changedFiles = git(["diff", "--name-only", `${baseRef}...HEAD`])
.split(/\r?\n/)
.filter(Boolean).length;
const mergeTree = runMergeTree(mergeBase, baseRef);
const conflictLines = mergeTree
.split(/\r?\n/)
.filter((line) => /^(<<<<<<<|=======|>>>>>>>|CONFLICT\b)/.test(line));
if (conflictLines.length > 0) {
const conflictReportPath = join(tmpdir(), "yedir-branch-readiness-conflicts.txt");
writeFileSync(conflictReportPath, conflictLines.join("\n"));
failures.push(
`merge conflict markers detected against ${baseRef}; details: ${conflictReportPath}`
);
}
if (behind > 0) {
const message = `${currentBranch} is behind ${baseRef} by ${behind} commit(s)`;
if (requireUpToDate) {
failures.push(message);
} else {
warnings.push(message);
}
}
console.log(
JSON.stringify(
{
baseRef,
branch: currentBranch,
requiredBranch: requiredBranch || null,
upstreamRef: upstreamRef || null,
mergeBase,
ahead,
behind,
changedFiles,
mergeConflictMarkers: conflictLines.length,
requireUpToDate,
},
null,
2
)
);
}
if (warnings.length > 0) {
console.warn(`Branch readiness warnings:\n- ${warnings.join("\n- ")}`);
}
if (failures.length > 0) {
console.error(`Branch readiness failed:\n- ${failures.join("\n- ")}`);
process.exit(1);
}
console.log("Branch readiness check passed.");
function git(args) {
return execFileSync("git", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
}
function getOptionalGitConfig(key) {
try {
return git(["config", "--get", key]).trim();
} catch {
return "";
}
}
function getOptionalUpstreamRef() {
try {
return git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]).trim();
} catch {
return "";
}
}
function runMergeTree(mergeBase, baseRef) {
try {
return git(["merge-tree", mergeBase, "HEAD", baseRef]);
} catch (error) {
return [error.stdout, error.stderr].filter(Boolean).join("\n");
}
}