From 57861e5e7097e29e9a6395b9831c50b83dfaef14 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 21 Jun 2026 15:45:54 +0000 Subject: [PATCH] fix: render gh merge result as table --- scripts/src/gh.ts | 83 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index 02005876..37dbad0f 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -5131,7 +5131,7 @@ async function prMerge(repo: string, token: string, number: number, options: Git if (isGitHubError(pr)) return commandError("pr merge", repo, pr, { number, phase: "fetch-pr" }); const summary = prSummary(pr); if (summary.merged === true) { - return { + return withPrMergeRendered({ ok: true, command: "pr merge", repo, @@ -5141,23 +5141,24 @@ async function prMerge(repo: string, token: string, number: number, options: Git pullRequest: summary, branchDeletion: { attempted: false, skippedReason: "already-merged" }, rest: true, - }; + }); } const metadata = await prGraphqlMetadata(repo, token, number); if (isGitHubError(metadata)) return commandError("pr merge", repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary }); const statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, false); const mergeability = prCloseoutSummary(summary, prMetadataSummary(metadata), statusChecks); if (mergeability.readyForCommanderMerge !== true) { - return validationError("pr merge", repo, "PR is not ready for merge; preflight blockers or pending states remain", { + const details = { number, pullRequest: preflightPullRequestSummary(summary), mergeability, statusChecks, closeoutMetadata: prCloseoutMetadata(metadata), - }); + }; + return withPrMergeRendered(validationError("pr merge", repo, "PR is not ready for merge; preflight blockers or pending states remain", details), details); } if (options.dryRun) { - return { + return withPrMergeRendered({ ok: true, command: "pr merge", repo, @@ -5169,7 +5170,7 @@ async function prMerge(repo: string, token: string, number: number, options: Git pullRequest: preflightPullRequestSummary(summary), mergeability, statusChecks, - }; + }); } const merged = await githubRequest>(token, "PUT", `/repos/${owner}/${name}/pulls/${number}/merge`, { merge_method: options.mergeMethod, @@ -5178,7 +5179,7 @@ async function prMerge(repo: string, token: string, number: number, options: Git const after = await githubRequest(token, "GET", `/repos/${owner}/${name}/pulls/${number}`); if (isGitHubError(after)) return commandError("pr merge", repo, after, { number, phase: "fetch-after-merge", mergeResult: merged }); const branchDeletion = options.deleteBranch ? await deleteHeadBranchAfterMerge(repo, token, after) : { attempted: false, skippedReason: "delete-branch-not-requested" }; - return { + return withPrMergeRendered({ ok: true, command: "pr merge", repo, @@ -5188,9 +5189,77 @@ async function prMerge(repo: string, token: string, number: number, options: Git pullRequest: prSummary(after), branchDeletion, rest: true, + }); +} + +function withPrMergeRendered(result: GitHubCommandResult, fallbackDetails?: Record): GitHubCommandResult { + return { + ...result, + contentType: "text/plain", + renderedText: renderPrMergeTable(result, fallbackDetails), }; } +function renderPrMergeTable(result: GitHubCommandResult, fallbackDetails?: Record): string { + const details = fallbackDetails ?? {}; + const pullRequest = isRecord(result.pullRequest) + ? result.pullRequest + : isRecord(details.pullRequest) + ? details.pullRequest + : {}; + const mergeability = isRecord(result.mergeability) + ? result.mergeability + : isRecord(details.mergeability) + ? details.mergeability + : {}; + const statusChecks = isRecord(result.statusChecks) + ? result.statusChecks + : isRecord(details.statusChecks) + ? details.statusChecks + : {}; + const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {}; + const branchDeletion = isRecord(result.branchDeletion) ? result.branchDeletion : {}; + const status = result.ok === true + ? result.alreadyMerged === true + ? "already-merged" + : result.dryRun === true + ? "dry-run" + : "merged" + : ghText(mergeability.conclusion) === "pending" + ? "pending" + : "blocked"; + const blockers = Array.isArray(mergeability.blockers) ? mergeability.blockers.map(String) : []; + const pending = Array.isArray(mergeability.pending) ? mergeability.pending.map(String) : []; + const rows = [[ + `#${ghText(result.number ?? details.number)}`, + status, + ghText(result.method ?? result.mergeMethod), + ghText(mergeability.mergeable), + ghText(mergeability.mergeStateStatus), + `${ghText(statusChecks.totalContexts)} total ${ghText(counts.success)} ok ${ghText(counts.failure)} fail ${ghText(counts.pending)} pending`, + ]]; + const lines = [ + `gh pr merge (${status})`, + "", + ghTable(["PR", "STATUS", "METHOD", "MERGEABLE", "MERGE_STATE", "CHECKS"], rows), + "", + "Summary:", + ` repo=${ghText(result.repo)} title=${ghShort(ghText(pullRequest.title), 96)}`, + ` blockers=${blockers.length === 0 ? "-" : blockers.join(",")} pending=${pending.length === 0 ? "-" : pending.join(",")}`, + ` branchDeletion=${ghText(branchDeletion.ok ?? branchDeletion.skippedReason ?? branchDeletion.attempted)}`, + "", + "Next:", + ]; + if (result.ok === true && result.alreadyMerged !== true && result.dryRun !== true) { + lines.push(` bun scripts/cli.ts gh pr view ${ghText(result.number ?? details.number)} --repo ${ghText(result.repo)}`); + } else { + lines.push(` bun scripts/cli.ts gh pr preflight ${ghText(result.number ?? details.number)} --repo ${ghText(result.repo)}`); + } + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use --full or --raw on read/preflight commands for structured metadata."); + return lines.join("\n"); +} + async function prPreflight(repo: string, number: number, commandName: "preflight" | "pr preflight" | "pr closeout", includeRaw: boolean): Promise { const auth = await authStatus(repo); const authCapability = compactAuthCapability(auth);