fix: support comments on issue close
This commit is contained in:
+50
-9
@@ -40,7 +40,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",
|
||||
"--search", "--inactive-hours", "--comment",
|
||||
]);
|
||||
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;
|
||||
@@ -335,6 +335,7 @@ interface GitHubOptions {
|
||||
title?: string;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
comment?: string;
|
||||
base?: string;
|
||||
head?: string;
|
||||
jsonFields?: IssueViewJsonField[];
|
||||
@@ -814,6 +815,7 @@ function parseOptions(args: string[]): GitHubOptions {
|
||||
title: optionValue(args, "--title"),
|
||||
body: optionValue(args, "--body"),
|
||||
bodyFile: optionValue(args, "--body-file"),
|
||||
comment: optionValue(args, "--comment"),
|
||||
base: optionValue(args, "--base"),
|
||||
head: optionValue(args, "--head"),
|
||||
jsonFields: top === "issue" && isIssueReadCommand(sub) ? parseIssueViewJsonFields(requestedJsonFields) : undefined,
|
||||
@@ -1123,6 +1125,14 @@ 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.body !== undefined || options.bodyFile !== undefined) {
|
||||
throw new Error(`${command} --comment cannot be combined with --body or --body-file`);
|
||||
}
|
||||
return readIssueCommentBody({ ...options, body: options.comment, bodyFile: undefined });
|
||||
}
|
||||
|
||||
function tokenFromEnvironment(): GitHubTokenProbe {
|
||||
if (process.env.GH_TOKEN && process.env.GH_TOKEN.length > 0) {
|
||||
return { present: true, source: "GH_TOKEN", ghFallbackAttempted: false };
|
||||
@@ -5578,8 +5588,14 @@ async function commentDelete(repo: string, token: string, ownerKind: "issue" | "
|
||||
};
|
||||
}
|
||||
|
||||
async function issueState(repo: string, token: string, issueNumber: number, state: "open" | "closed", dryRun: boolean): Promise<GitHubCommandResult> {
|
||||
async function issueState(repo: string, token: string, issueNumber: number, state: "open" | "closed", dryRun: boolean, options?: GitHubOptions): Promise<GitHubCommandResult> {
|
||||
const command = state === "closed" ? "issue close" : "issue reopen";
|
||||
let lifecycleComment: { body: string; bodySource: Record<string, unknown> } | null = null;
|
||||
try {
|
||||
lifecycleComment = options === undefined ? null : readIssueLifecycleCommentBody(options, command);
|
||||
} catch (error) {
|
||||
return validationError(command, repo, error instanceof Error ? error.message : String(error), { issueNumber });
|
||||
}
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
@@ -5589,16 +5605,37 @@ async function issueState(repo: string, token: string, issueNumber: number, stat
|
||||
issueNumber,
|
||||
disclosure: issueLifecycleDisclosure(repo, issueNumber, true),
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
comment: lifecycleComment === null
|
||||
? null
|
||||
: {
|
||||
planned: true,
|
||||
...writeBodyPlan("issue comment create", repo, lifecycleComment.body, lifecycleComment.bodySource, { issueNumber }),
|
||||
},
|
||||
wouldPatch: { state },
|
||||
};
|
||||
}
|
||||
const { owner, name } = repoParts(repo);
|
||||
let commentSummary: Record<string, unknown> | null = null;
|
||||
if (lifecycleComment !== null) {
|
||||
const comment = await githubRequest<GitHubComment>(token, "POST", `/repos/${owner}/${name}/issues/${issueNumber}/comments`, { body: lifecycleComment.body });
|
||||
if (isGitHubError(comment)) {
|
||||
return commandError(command, repo, comment, {
|
||||
issueNumber,
|
||||
phase: "comment",
|
||||
commentBodySource: lifecycleComment.bodySource,
|
||||
commentBodyChars: lifecycleComment.body.length,
|
||||
commentBodySha: bodySha(lifecycleComment.body),
|
||||
});
|
||||
}
|
||||
commentSummary = compactCommentSummary(comment);
|
||||
}
|
||||
const issue = await githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, { state });
|
||||
if (isGitHubError(issue)) return commandError(command, repo, issue, { issueNumber });
|
||||
if (isGitHubError(issue)) return commandError(command, repo, issue, { issueNumber, phase: "state", comment: commentSummary });
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
repo,
|
||||
comment: commentSummary,
|
||||
issue: issueLifecycleSummary(issue),
|
||||
disclosure: issueLifecycleDisclosure(repo, issueNumber, false),
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
@@ -6296,7 +6333,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] [--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 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]",
|
||||
@@ -6343,7 +6380,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. 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> 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 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.",
|
||||
@@ -6480,6 +6517,10 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--inactive-hours is only supported by gh issue stale-close");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
if (top === "auth" && sub === "status") return authStatus(options.repo);
|
||||
|
||||
@@ -6554,8 +6595,8 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
return issueEdit(options.repo, token ?? "", issueNumber, options, "issue update");
|
||||
}
|
||||
if (sub === "comment") return issueComment(options.repo, "", parseNumber(third, "issue comment"), options);
|
||||
if (sub === "close") return issueState(options.repo, "", parseNumber(third, "issue close"), "closed", true);
|
||||
if (sub === "reopen") return issueState(options.repo, "", parseNumber(third, "issue reopen"), "open", true);
|
||||
if (sub === "close") return issueState(options.repo, "", parseNumber(third, "issue close"), "closed", true, options);
|
||||
if (sub === "reopen") return issueState(options.repo, "", parseNumber(third, "issue reopen"), "open", true, options);
|
||||
}
|
||||
if (sub === "read" || sub === "view") {
|
||||
const resolved = resolveReadViewNumberReference("issue", sub, third, options, args);
|
||||
@@ -6577,8 +6618,8 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (sub === "edit") return issueEdit(options.repo, token, parseNumber(third, "issue edit"), options);
|
||||
if (sub === "update") return issueEdit(options.repo, token, parseNumber(third, "issue update"), options, "issue update");
|
||||
if (sub === "comment") return issueComment(options.repo, token, parseNumber(third, "issue comment"), options);
|
||||
if (sub === "close") return issueState(options.repo, token, parseNumber(third, "issue close"), "closed", options.dryRun);
|
||||
if (sub === "reopen") return issueState(options.repo, token, parseNumber(third, "issue reopen"), "open", options.dryRun);
|
||||
if (sub === "close") return issueState(options.repo, token, parseNumber(third, "issue close"), "closed", options.dryRun, options);
|
||||
if (sub === "reopen") return issueState(options.repo, token, parseNumber(third, "issue reopen"), "open", options.dryRun, options);
|
||||
}
|
||||
|
||||
if (top === "pr") {
|
||||
|
||||
Reference in New Issue
Block a user