fix: support labels for gh issue create
This commit is contained in:
+66
-10
@@ -200,6 +200,7 @@ interface GitHubOptions {
|
||||
draft: boolean;
|
||||
notifyClaudeQqBriefDiff: boolean;
|
||||
allowShortBody: boolean;
|
||||
labels: string[];
|
||||
title?: string;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
@@ -302,6 +303,18 @@ function optionValue(args: string[], name: string): string | undefined {
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionValues(args: string[], name: string): string[] {
|
||||
const values: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
if (args[index] !== name) continue;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
values.push(value);
|
||||
index += 1;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function hasFlag(args: string[], name: string): boolean {
|
||||
return args.includes(name);
|
||||
}
|
||||
@@ -318,6 +331,22 @@ function commaListOption(args: string[], name: string): string[] | undefined {
|
||||
return values;
|
||||
}
|
||||
|
||||
function labelsOption(args: string[]): string[] {
|
||||
const rawValues = optionValues(args, "--label");
|
||||
const labels: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const raw of rawValues) {
|
||||
const parts = raw.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
||||
if (parts.length === 0) throw new Error("--label requires at least one non-empty label");
|
||||
for (const label of parts) {
|
||||
if (seen.has(label)) continue;
|
||||
labels.push(label);
|
||||
seen.add(label);
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
|
||||
const raw = optionValue(args, name);
|
||||
if (raw === undefined) return defaultValue;
|
||||
@@ -367,7 +396,7 @@ function parseIssueBodyProfile(args: string[]): IssueBodyProfileOption {
|
||||
}
|
||||
|
||||
function validateKnownOptions(args: string[]): void {
|
||||
const valueOptions = new Set(["--repo", "--limit", "--title", "--body-file", "--body", "--base", "--head", "--json", "--state", "--mode", "--expect-updated-at", "--expect-body-sha", "--body-profile"]);
|
||||
const valueOptions = new Set(["--repo", "--limit", "--title", "--body-file", "--body", "--base", "--head", "--json", "--state", "--mode", "--expect-updated-at", "--expect-body-sha", "--body-profile", "--label"]);
|
||||
const flagOptions = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body"]);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
@@ -392,6 +421,7 @@ function parseOptions(args: string[]): GitHubOptions {
|
||||
draft: hasFlag(args, "--draft"),
|
||||
notifyClaudeQqBriefDiff: hasFlag(args, "--notify-claudeqq-brief-diff"),
|
||||
allowShortBody: hasFlag(args, "--allow-short-body"),
|
||||
labels: labelsOption(args),
|
||||
title: optionValue(args, "--title"),
|
||||
body: optionValue(args, "--body"),
|
||||
bodyFile: optionValue(args, "--body-file"),
|
||||
@@ -859,6 +889,12 @@ function commanderBriefNotificationPlan(issueNumber: number, body: string, diff:
|
||||
|
||||
function writeBodyPlan(command: "issue create" | "issue comment create" | "pr create" | "pr comment", repo: string, body: string, bodySource: Record<string, unknown>, extra: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
const isIssueWrite = command === "issue create" || command === "issue comment create";
|
||||
const requestBody: Record<string, unknown> = {
|
||||
bodyChars: body.length,
|
||||
bodySource,
|
||||
};
|
||||
if (command === "issue create" && typeof extra.title === "string") requestBody.title = extra.title;
|
||||
if (command === "issue create" && Array.isArray(extra.labels)) requestBody.labels = extra.labels;
|
||||
return {
|
||||
repo,
|
||||
bodySource,
|
||||
@@ -870,10 +906,7 @@ function writeBodyPlan(command: "issue create" | "issue comment create" | "pr cr
|
||||
path: isIssueWrite
|
||||
? (command === "issue create" ? "/repos/{owner}/{repo}/issues" : "/repos/{owner}/{repo}/issues/{issue_number}/comments")
|
||||
: (command === "pr create" ? "/repos/{owner}/{repo}/pulls" : "/repos/{owner}/{repo}/issues/{issue_number}/comments"),
|
||||
body: {
|
||||
bodyChars: body.length,
|
||||
bodySource,
|
||||
},
|
||||
body: requestBody,
|
||||
},
|
||||
validation: {
|
||||
source: String(bodySource.kind ?? "unknown"),
|
||||
@@ -1047,6 +1080,10 @@ function labelSummary(label: string | { name?: string; color?: string; descripti
|
||||
};
|
||||
}
|
||||
|
||||
function issueLabelNames(issue: GitHubIssue): string[] {
|
||||
return (issue.labels ?? []).map((label) => typeof label === "string" ? label : label.name ?? "").filter((label) => label.length > 0);
|
||||
}
|
||||
|
||||
function issueListSummary(issue: GitHubIssue, fields: IssueListJsonField[]): Record<string, unknown> {
|
||||
const summary: Record<IssueListJsonField, unknown> = {
|
||||
number: issue.number,
|
||||
@@ -1619,6 +1656,7 @@ async function issueCreate(repo: string, token: string, options: GitHubOptions):
|
||||
if (options.title === undefined) throw new Error("issue create requires --title <title>");
|
||||
const body = readBodyFile(options.bodyFile, "issue create");
|
||||
const bodySource = { kind: "body-file", path: options.bodyFile ?? null };
|
||||
const labels = options.labels;
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
@@ -1627,13 +1665,26 @@ async function issueCreate(repo: string, token: string, options: GitHubOptions):
|
||||
dryRun: true,
|
||||
planned: true,
|
||||
title: options.title,
|
||||
...writeBodyPlan("issue create", repo, body, bodySource, { title: options.title }),
|
||||
labels,
|
||||
...writeBodyPlan("issue create", repo, body, bodySource, { title: options.title, labels }),
|
||||
};
|
||||
}
|
||||
const { owner, name } = repoParts(repo);
|
||||
const issue = await githubRequest<GitHubIssue>(token, "POST", `/repos/${owner}/${name}/issues`, { title: options.title, body });
|
||||
if (isGitHubError(issue)) return commandError("issue create", repo, issue);
|
||||
return { ok: true, command: "issue create", repo, issue: issueSummary(issue), bodySource, rest: true };
|
||||
const payload: Record<string, unknown> = { title: options.title, body };
|
||||
if (labels.length > 0) payload.labels = labels;
|
||||
const issue = await githubRequest<GitHubIssue>(token, "POST", `/repos/${owner}/${name}/issues`, payload);
|
||||
if (isGitHubError(issue)) return commandError("issue create", repo, issue, { title: options.title, labels });
|
||||
const appliedLabels = issueLabelNames(issue);
|
||||
const missingLabels = labels.filter((label) => !appliedLabels.includes(label));
|
||||
if (missingLabels.length > 0) {
|
||||
return validationError("issue create", repo, "GitHub created the issue but did not return all requested labels; refusing to report silent label success", {
|
||||
issue: issueSummary(issue),
|
||||
requestedLabels: labels,
|
||||
appliedLabels,
|
||||
missingLabels,
|
||||
});
|
||||
}
|
||||
return { ok: true, command: "issue create", repo, issue: issueSummary(issue), labels, bodySource, rest: true };
|
||||
}
|
||||
|
||||
async function issueEdit(repo: string, token: string, issueNumber: number, options: GitHubOptions, commandName = "issue edit"): Promise<GitHubCommandResult> {
|
||||
@@ -2307,7 +2358,7 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh auth status [--repo owner/name]",
|
||||
"bun scripts/cli.ts gh issue list [--state open|closed|all] [--limit N] [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels]",
|
||||
"bun scripts/cli.ts gh issue view <number> [--repo owner/name] [--json body,title,state,comments]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> --body-file <file> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> --body-file <file> [--label label[,label...]]... [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue update <number> --mode replace|append --body-file <file> [--title title] [--repo owner/name] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256] [--body-profile auto|code-queue-board|commander-brief] [--allow-short-body]",
|
||||
"bun scripts/cli.ts gh issue edit <number> --body-file <file> [compat alias for issue update --mode replace]",
|
||||
"bun scripts/cli.ts gh issue edit 24 --body-file <file> --notify-claudeqq-brief-diff [--dry-run]",
|
||||
@@ -2332,6 +2383,7 @@ export function ghHelp(): unknown {
|
||||
"Token values are never printed; auth status reports only token source and presence.",
|
||||
"issue list defaults to --state open and bounded --limit 30; supported --json fields are number,title,state,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
|
||||
"issue view supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"issue create accepts repeatable --label values and comma-separated labels; dry-run prints the parsed labels and non-dry-run sends them in the GitHub REST create-issue payload.",
|
||||
"--body-file is the recommended source for Markdown bodies so real newlines, backticks, and tables are read as file bytes instead of shell arguments.",
|
||||
"update defaults to --mode replace; --mode append reads the current body and appends file bytes so real newlines, backticks, and Markdown tables are preserved.",
|
||||
"issue edit is a compatibility alias for issue update --mode replace.",
|
||||
@@ -2385,6 +2437,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, "--allow-short-body, --expect-updated-at, --expect-body-sha, and --body-profile are only supported by gh issue update/edit");
|
||||
}
|
||||
if (optionWasProvided(args, "--label") && !(top === "issue" && sub === "create")) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--label is only supported by gh issue create");
|
||||
}
|
||||
|
||||
if (top === "auth" && sub === "status") return authStatus(options.repo);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user