From e30c8cb1c362a34fc14fa2adb242808d82953931 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 15:13:17 +0000 Subject: [PATCH] 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 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 --- scripts/src/gh.ts | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index 422891bf..f9bca4f9 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -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 } | 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 --notify-claudeqq-brief-diff [--dry-run]", "bun scripts/cli.ts gh issue comment create --body-file |--body [--repo owner/name] [--dry-run]", "bun scripts/cli.ts gh issue comment delete [--repo owner/name] [--dry-run]", - "bun scripts/cli.ts gh issue close|reopen [--repo owner/name] [--comment ] [--dry-run]", + "bun scripts/cli.ts gh issue close|reopen [--repo owner/name] [--comment |--comment-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 [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 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 posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. Use gh issue read --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 or --comment-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 --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 --repo owner/name --body-file - or gh issue comment create --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 for long or multiline content.", @@ -6545,7 +6571,7 @@ export async function runGhCommand(args: string[]): Promise 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);