fix(gh): relax pr list --json whitelist and add issue close --comment-file (#780 1+2)

Issue 1 of #780: gh pr list --json whitelist is unintuitive.
merged / closedAt / mergedAt / mergeCommit are basic per-PR
fields already returned by GitHub list API and already
projected by prSummary(); they were previously rejected by
the prListJsonFields validator. Add them to PR_LIST_JSON_FIELDS
so callers no longer have to fall back to a pr view per-PR.
mergeable / mergeStateStatus / statusCheckRollup remain
closeout fields that still require a per-PR pr view.

Issue 2 of #780: gh issue close --comment only accepts short
inline text; long Markdown closeout bodies had to escape
backticks, newlines and \\ in shell, then bash heredoc.
Add --comment-file <file|-> to issue close/reopen, which
mirrors the existing --body-file plumbing through
readIssueLifecycleCommentBody. --comment and --comment-file
are mutually exclusive; both remain mutually exclusive with
--body and --body-file.

Verified live:
- gh pr list --repo pikasTech/HWLAB --state closed --json
  number,merged,mergedAt,closedAt,mergeCommit returns real data
  (number=781 merged=true mergedAt=2026-06-03T14:22:32Z
   closedAt=2026-06-03T14:22:32Z
   mergeCommit={oid:3ac4cf8d2e4dfadb251cad53bf35d08b86d73840})
- gh pr list --json mergeable,statusCheckRollup still rejected
  with 'unsupported closeout field(s)' pointing to pr view
- gh issue close 780 --comment-file /tmp/test-close-comment.md
  --dry-run reports comment.planned=true bodyChars=155
  bodySource.kind=body-file
- gh issue close 780 --comment 'x' --comment-file /tmp/x.md
  --dry-run fails with --comment and --comment-file are
  mutually exclusive
This commit is contained in:
Codex
2026-06-03 15:13:17 +00:00
parent b86bcb3d4d
commit e30c8cb1c3
+33 -7
View File
@@ -26,7 +26,25 @@ const BOARD_ROW_FIELDS = ["progress", "status", "validation", "branch", "tasks",
const BOARD_ROW_UPSERT_TEXT_FIELDS = ["category", "branch", "tasks", "summary", "focus", "validation", "progress"] as const;
const ISSUE_VIEW_JSON_FIELDS = ["body", "title", "state", "comments", "number", "url", "author", "createdAt", "updatedAt"] as const;
const ISSUE_LIST_JSON_FIELDS = ["number", "title", "state", "url", "updatedAt", "createdAt", "author", "labels"] as const;
const PR_LIST_JSON_FIELDS = ["body", "title", "state", "number", "url", "author", "head", "base", "draft", "createdAt", "updatedAt", "headRefName", "baseRefName"] as const;
const PR_LIST_JSON_FIELDS = [
"body",
"title",
"state",
"number",
"url",
"author",
"head",
"base",
"draft",
"createdAt",
"updatedAt",
"closedAt",
"merged",
"mergedAt",
"mergeCommit",
"headRefName",
"baseRefName"
] as const;
const PR_READ_JSON_FIELDS = ["body", "title", "state", "stateDetail", "number", "url", "author", "head", "base", "draft", "createdAt", "updatedAt", "closed", "closedAt", "merged", "mergedAt", "mergeCommit", "headRefName", "baseRefName", "mergeable", "mergeStateStatus", "statusCheckRollup"] as const;
const PR_CLOSEOUT_JSON_FIELDS = ["mergeable", "mergeStateStatus", "statusCheckRollup"] as const;
const PR_CLOSEOUT_VIEW_JSON = "headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup";
@@ -40,7 +58,7 @@ const GH_VALUE_OPTIONS = new Set([
"--expect-updated-at", "--expect-body-sha", "--body-profile", "--label", "--field",
"--value", "--section", "--to", "--status", "--row-file", "--category", "--branch",
"--tasks", "--summary", "--focus", "--validation", "--progress", "--number", "--pr",
"--search", "--inactive-hours", "--comment",
"--search", "--inactive-hours", "--comment", "--comment-file",
]);
const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--raw", "--full", "--stat", "--merge", "--squash", "--rebase", "--delete-branch"]);
const MIN_SAFE_BODY_SCAN_CHARS = MIN_SAFE_ISSUE_BODY_CHARS;
@@ -336,6 +354,7 @@ interface GitHubOptions {
body?: string;
bodyFile?: string;
comment?: string;
commentFile?: string;
base?: string;
head?: string;
jsonFields?: IssueViewJsonField[];
@@ -816,6 +835,7 @@ function parseOptions(args: string[]): GitHubOptions {
body: optionValue(args, "--body"),
bodyFile: optionValue(args, "--body-file"),
comment: optionValue(args, "--comment"),
commentFile: optionValue(args, "--comment-file"),
base: optionValue(args, "--base"),
head: optionValue(args, "--head"),
jsonFields: top === "issue" && isIssueReadCommand(sub) ? parseIssueViewJsonFields(requestedJsonFields) : undefined,
@@ -1152,9 +1172,15 @@ function readIssueCommentBody(options: GitHubOptions): { body: string; bodySourc
}
function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
if (options.comment === undefined) return null;
if (options.comment === undefined && options.commentFile === undefined) return null;
if (options.comment !== undefined && options.commentFile !== undefined) {
throw new Error(`${command} --comment and --comment-file are mutually exclusive`);
}
if (options.body !== undefined || options.bodyFile !== undefined) {
throw new Error(`${command} --comment cannot be combined with --body or --body-file`);
throw new Error(`${command} --comment or --comment-file cannot be combined with --body or --body-file`);
}
if (options.commentFile !== undefined) {
return readIssueCommentBody({ ...options, body: undefined, bodyFile: options.commentFile });
}
return readIssueCommentBody({ ...options, body: options.comment, bodyFile: undefined });
}
@@ -6359,7 +6385,7 @@ export function ghHelp(): unknown {
"bun scripts/cli.ts gh issue edit 24 --body-file <file> --notify-claudeqq-brief-diff [--dry-run]",
"bun scripts/cli.ts gh issue comment create <number> --body-file <file|->|--body <short-text> [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--dry-run]",
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--comment <short-text>] [--dry-run]",
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--comment <short-text>|--comment-file <file|->] [--dry-run]",
"bun scripts/cli.ts gh issue stale-close [--repo owner/name] [--inactive-hours N] [--limit N] [--label label[,label...]]... [--dry-run]",
"bun scripts/cli.ts gh issue delete <number> [unsupported: use close]",
"bun scripts/cli.ts gh issue scan-escape [--repo owner/name] [--limit N] [--dry-run]",
@@ -6406,7 +6432,7 @@ export function ghHelp(): unknown {
"issue update --body-file accepts files or - for stdin, refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading, warns when HWLAB product/user issue routing appears in favor of pikasTech/HWLAB, and still rejects commander brief update sections; commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).",
"issue update dry-run reports bounded bodyPreview/bodyPreviewLines, old/new body length slots, body SHA, required heading checks, literal \\n detection, shell-pollution signals, guard/concurrency summary, wouldPatch, and readCommands without printing an unbounded full body. Non-dry-run automatically reads current issue metadata before PATCH and returns oldBodySha/updatedAt; --expect-updated-at or --expect-body-sha remain available for explicit stale-cache protection.",
"issue comment create accepts --body-file <file|-> for Markdown/generated content and --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally.",
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. Use gh issue read <number> --json body or --full/--raw on read when full text is needed.",
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text> or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-file is the recommended path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue read <number> --json body or --full/--raw on read when full text is needed.",
"issue stale-close is the reusable lifecycle cleanup path for policies such as closing open issues inactive for more than 48 hours. It selects open issues by GitHub updatedAt older than observedAt - --inactive-hours, treats comments and state changes as activity, filters pull requests, supports --dry-run, and returns bounded candidate/closed/failure summaries without echoing full bodies.",
"For one-shot issue writes, pipe reviewed Markdown through stdin: cat body.md | bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file - or gh issue comment create <number> --body-file -. When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments. GitHub issue/PR Markdown writes use --body-file <file|-> for long or multiline content.",
@@ -6545,7 +6571,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
}
if (optionWasProvided(args, "--comment") && !(top === "issue" && (sub === "close" || sub === "reopen"))) {
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
return validationError(command, options.repo, "--comment is only supported by gh issue close/reopen; use gh issue comment create for standalone comments");
return validationError(command, options.repo, "--comment/--comment-file is only supported by gh issue close/reopen; use gh issue comment create for standalone comments");
}
if (top === "auth" && sub === "status") return authStatus(options.repo);