ci: finalize pac status and gh heredoc guard
This commit is contained in:
@@ -9,16 +9,19 @@ import type { GitHubOptions, GitHubTokenProbe } from "./types";
|
||||
|
||||
export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | 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/--comment-stdin are mutually exclusive`);
|
||||
if (options.comment !== undefined) {
|
||||
throw new Error(`${command} inline --comment is unsupported; use --comment-stdin with a quoted heredoc`);
|
||||
}
|
||||
if (options.body !== undefined || options.bodyFile !== undefined) {
|
||||
throw new Error(`${command} --comment/--comment-file/--comment-stdin cannot be combined with --body/--body-file/--body-stdin`);
|
||||
throw new Error(`${command} --comment-stdin cannot be combined with --body/--body-file/--body-stdin`);
|
||||
}
|
||||
if (options.commentFile !== undefined) {
|
||||
if (options.commentFile !== "-") {
|
||||
throw new Error(`${command} --comment-file is unsupported for lifecycle comments; use --comment-stdin with a quoted heredoc`);
|
||||
}
|
||||
return readIssueCommentBody({ ...options, body: undefined, bodyFile: options.commentFile });
|
||||
}
|
||||
return readIssueCommentBody({ ...options, body: options.comment, bodyFile: undefined });
|
||||
return null;
|
||||
}
|
||||
|
||||
export function tokenFromEnvironment(): GitHubTokenProbe {
|
||||
|
||||
@@ -537,13 +537,14 @@ function renderScanEscape(result: GitHubCommandResult): string {
|
||||
function renderIssueLifecycle(result: GitHubCommandResult): string {
|
||||
const issue = record(result.issue);
|
||||
const comment = record(result.comment);
|
||||
const commentStatus = comment.id ?? (comment.planned === true ? "planned" : "-");
|
||||
return [
|
||||
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
|
||||
"",
|
||||
ghTable(["ISSUE", "STATE", "COMMENT", "UPDATED", "TITLE"], [[
|
||||
`#${ghText(result.issueNumber ?? issue.number)}`,
|
||||
ghText(issue.state ?? record(result.wouldPatch).state),
|
||||
ghText(comment.id ?? "-"),
|
||||
ghText(commentStatus),
|
||||
shortDate(issue.updatedAt),
|
||||
ghShort(ghText(issue.title), 96),
|
||||
]]),
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue comment patch <commentId> --body-patch-stdin [--repo owner/name] [--number N compat] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue comment edit <commentId> (--body-stdin|--body-file <file|->|--body <short-text>) [--repo owner/name] [--number N compat] [--dry-run] [compatibility alias for issue comment update]",
|
||||
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment <short-text>|--comment-stdin|--comment-file <file|->] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment-stdin] [--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]",
|
||||
@@ -86,7 +86,7 @@ export function ghHelp(): unknown {
|
||||
"issue patch reads the current GitHub issue body, applies a Codex apply_patch envelope against virtual file issue.md from --body-patch-stdin or --body-patch-file <file|->, then runs the same issue body guard before PATCH. It returns old/new bodySha, updatedAt, patch summary, and bounded previews; context mismatch fails with redacted diagnostics and no GitHub write.",
|
||||
"issue comment view/read reads one comment by commentId. Default output is compact metadata plus bodyChars/bodySha/preview; --full includes that one full comment body. issue comment create/update/edit accept --body-stdin or --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. Use comment update/edit to correct existing wording in place; delete remains for intentional removal.",
|
||||
"issue comment patch reads the current issue comment by commentId, applies a Codex apply_patch envelope against virtual file comment.md, then PATCHes only that comment. It returns comment id, old/new bodySha, updatedAt, patch summary, and redacted mismatch diagnostics without echoing the full comment body.",
|
||||
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text>, --comment-stdin, or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin is the first-class heredoc path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
|
||||
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment-stdin posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin with a quoted heredoc is the only lifecycle closeout comment path; inline --comment and --comment-file are unsupported. Use gh issue view <number> --json body or --full/--raw on view 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, prefer quoted heredoc stdin: bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin <<'EOF' ... EOF or gh issue comment create <number> --body-stdin <<'EOF' ... EOF. --body-file <file|-> remains available for reusable files and pipes.",
|
||||
"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-stdin for heredoc/stdin or --body-file <file|-> for reusable files.",
|
||||
@@ -164,8 +164,8 @@ export function ghScopedHelpNotes(tokens: string[]): string[] {
|
||||
notes.push("Use `issue comments <number>` as the low-friction recent-progress path; default output is bounded and structured output is at `.data.comments`.");
|
||||
notes.push("`--full` or `--raw` includes full bodies for the bounded recent list without changing the stable top-level `comments` path.");
|
||||
} else if (key === "issue close" || key === "issue reopen") {
|
||||
notes.push("Issue close/reopen can post a lifecycle comment with `--comment`, `--comment-stdin`, or `--comment-file <file|->` before changing state.");
|
||||
notes.push("For long closeout evidence, prefer `--comment-stdin` with a quoted heredoc.");
|
||||
notes.push("Issue close/reopen can post a lifecycle comment with `--comment-stdin` before changing state.");
|
||||
notes.push("Use `--comment-stdin` with a quoted heredoc for closeout evidence; inline `--comment` and `--comment-file` are unsupported.");
|
||||
} else if (key === "pr comment" || key.startsWith("pr comment ")) {
|
||||
notes.push("PR comments are GitHub issue comments under the hood; use comment id targets for update/edit/delete.");
|
||||
notes.push("Use `pr comment view <commentId> --full` to read one full comment body by id.");
|
||||
|
||||
+16
-2
@@ -194,9 +194,23 @@ 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"))) {
|
||||
if (optionWasProvided(args, "--comment")) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--comment/--comment-file is only supported by gh issue close/reopen; use gh issue comment create for standalone comments");
|
||||
return validationError(command, options.repo, "inline --comment is unsupported because shell quoting corrupts Markdown; use --comment-stdin with a quoted heredoc", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh issue close <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
|
||||
"bun scripts/cli.ts gh issue reopen <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
|
||||
],
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--comment-file")) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--comment-file is unsupported for issue lifecycle comments; use --comment-stdin with a quoted heredoc", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh issue close <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
|
||||
"bun scripts/cli.ts gh issue reopen <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
|
||||
],
|
||||
});
|
||||
}
|
||||
if ((optionWasProvided(args, "--description") || optionWasProvided(args, "--private") || optionWasProvided(args, "--public") || optionWasProvided(args, "--auto-init")) && !(top === "repo" && sub === "create")) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
|
||||
@@ -10,6 +10,8 @@ import { ghShort, ghTable, ghText } from "./render";
|
||||
import { GITHUB_REST_PAGE_SIZE, ISSUE_LIST_JSON_FIELDS, MAX_ISSUE_LIST_LIMIT } from "./types";
|
||||
import type { GitHubCommandResult, IssueListJsonField, IssueListState } from "./types";
|
||||
|
||||
const COMPACT_JSON_ISSUE_LIMIT = 40;
|
||||
|
||||
export function shellWord(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
@@ -82,6 +84,21 @@ export async function issueList(repo: string, token: string, state: IssueListSta
|
||||
? listedIssues.filter((issue) => issue.title.startsWith(normalizedTitlePrefix))
|
||||
: listedIssues;
|
||||
const fields = jsonFields ?? ISSUE_LIST_JSON_FIELDS.slice();
|
||||
const selectedIssues = issues.map((issue) => issueListSummary(issue, fields));
|
||||
const compactJson = jsonFields !== undefined && !noDump;
|
||||
const returnedIssues = compactJson ? selectedIssues.slice(0, COMPACT_JSON_ISSUE_LIMIT) : selectedIssues;
|
||||
const omittedIssues = Math.max(0, selectedIssues.length - returnedIssues.length);
|
||||
const fullCommand = [
|
||||
"bun scripts/cli.ts gh issue list",
|
||||
"--repo", shellWord(repo),
|
||||
"--state", state,
|
||||
"--limit", String(limit),
|
||||
...(normalizedSearch.length > 0 ? ["--search", shellWord(normalizedSearch)] : []),
|
||||
...(normalizedTitlePrefix.length > 0 ? ["--title-prefix", shellWord(normalizedTitlePrefix)] : []),
|
||||
...labels.flatMap((label) => ["--label", shellWord(label)]),
|
||||
"--json", shellWord(fields.join(",")),
|
||||
"--full",
|
||||
].join(" ");
|
||||
const payload: GitHubCommandResult = {
|
||||
ok: true,
|
||||
command: "issue list",
|
||||
@@ -109,10 +126,29 @@ export async function issueList(repo: string, token: string, state: IssueListSta
|
||||
rawCount: result.rawCount,
|
||||
searchTotalCount: result.searchTotalCount,
|
||||
searchIncomplete: result.searchIncomplete,
|
||||
pagination: issueListPaginationSummary(result),
|
||||
...(compactJson ? {} : { pagination: issueListPaginationSummary(result) }),
|
||||
hasMore: result.hasMore,
|
||||
jsonFields: fields,
|
||||
issues: issues.map((issue) => issueListSummary(issue, fields)),
|
||||
issues: returnedIssues,
|
||||
...(compactJson
|
||||
? {
|
||||
issueListDisclosure: {
|
||||
mode: "bounded-json",
|
||||
returned: returnedIssues.length,
|
||||
omitted: omittedIssues,
|
||||
totalMatchedInBoundedScan: issues.length,
|
||||
inlineLimit: COMPACT_JSON_ISSUE_LIMIT,
|
||||
fullCommand,
|
||||
note: "Default --json issue list output is bounded for interactive use; add --full for complete selected fields and pagination/request metadata.",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(omittedIssues > 0
|
||||
? {
|
||||
omittedIssues,
|
||||
fullCommand,
|
||||
}
|
||||
: {}),
|
||||
...(result.hasMore && limit < MAX_ISSUE_LIST_LIMIT
|
||||
? {
|
||||
next: {
|
||||
@@ -129,14 +165,18 @@ export async function issueList(repo: string, token: string, state: IssueListSta
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
request: {
|
||||
method: "GET",
|
||||
path: normalizedSearch ? "/search/issues" : "/repos/{owner}/{repo}/issues",
|
||||
query: normalizedSearch
|
||||
? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}${labels.map((label) => ` ${githubSearchLabelQualifier(label)}`).join("")}`, per_page: GITHUB_REST_PAGE_SIZE }
|
||||
: { state, labels, per_page: GITHUB_REST_PAGE_SIZE },
|
||||
localTitlePrefixFilter: normalizedTitlePrefix || null,
|
||||
},
|
||||
...(compactJson
|
||||
? {}
|
||||
: {
|
||||
request: {
|
||||
method: "GET",
|
||||
path: normalizedSearch ? "/search/issues" : "/repos/{owner}/{repo}/issues",
|
||||
query: normalizedSearch
|
||||
? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}${labels.map((label) => ` ${githubSearchLabelQualifier(label)}`).join("")}`, per_page: GITHUB_REST_PAGE_SIZE }
|
||||
: { state, labels, per_page: GITHUB_REST_PAGE_SIZE },
|
||||
localTitlePrefixFilter: normalizedTitlePrefix || null,
|
||||
},
|
||||
}),
|
||||
note: "GitHub's issues endpoint may include pull requests; this command filters pull requests from .issues.",
|
||||
};
|
||||
return jsonFields === undefined && !noDump ? withIssueListRendered(payload) : payload;
|
||||
|
||||
Reference in New Issue
Block a user