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:
+33
-7
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user