#!/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"); } }