fix: support gh pr list state filter
This commit is contained in:
@@ -607,6 +607,12 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(acceptanceListData.limit === 5, "acceptance issue list command should preserve limit=5", acceptanceListData);
|
||||
assertCondition(acceptanceListData.count === 2, "acceptance issue list command should filter PRs and keep issues", acceptanceListData);
|
||||
|
||||
const listDefaultState = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--limit", "2", "--json", "number,title,state,url"], env);
|
||||
assertCondition(listDefaultState.status === 0, "issue list default state should still succeed", listDefaultState.json ?? { stdout: listDefaultState.stdout });
|
||||
const listDefaultStateData = dataOf(listDefaultState.json ?? {});
|
||||
assertCondition(listDefaultStateData.state === "open", "issue list should keep default state=open", listDefaultStateData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=2"), "issue list default should query state=open", mock.requests);
|
||||
|
||||
const listDefaultFields = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "all", "--limit", "3"], env);
|
||||
assertCondition(listDefaultFields.status === 0, "issue list should support default fields", listDefaultFields.json ?? { stdout: listDefaultFields.stdout });
|
||||
const listDefaultData = dataOf(listDefaultFields.json ?? {});
|
||||
|
||||
@@ -99,6 +99,14 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, [pullRequest]);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/pulls?state=open&per_page=4") {
|
||||
sendJson(res, 200, [pullRequest]);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/pulls?state=closed&per_page=4") {
|
||||
sendJson(res, 200, [{ ...pullRequest, number: 43, state: "closed", html_url: "https://github.com/pikasTech/unidesk/pull/43" }]);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/pulls/42") {
|
||||
sendJson(res, 200, pullRequest);
|
||||
return;
|
||||
@@ -162,6 +170,12 @@ function dataOf(response: JsonRecord): JsonRecord {
|
||||
return response.data as JsonRecord;
|
||||
}
|
||||
|
||||
function failedDataOf(response: JsonRecord): JsonRecord {
|
||||
assertCondition(response.ok === false, "CLI command should fail", response);
|
||||
assertCondition(typeof response.data === "object" && response.data !== null && !Array.isArray(response.data), "failure data should be object", response);
|
||||
return response.data as JsonRecord;
|
||||
}
|
||||
|
||||
export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const help = await runCli(["gh", "help"]);
|
||||
assertCondition(help.status === 0, "gh help should succeed", help.json ?? { stdout: help.stdout });
|
||||
@@ -173,8 +187,10 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(usage.some((line) => line.includes("gh pr view")), "gh help should list pr view", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr create")), "gh help should list pr create", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr comment")), "gh help should list pr comment", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr list") && line.includes("--state open|closed|all")), "gh help should document pr list state filtering", { usage });
|
||||
assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state pr read is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state pr view is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR list defaults to --state all")), "gh help should document pr list default state", { notes });
|
||||
|
||||
const mock = await startMockGitHub();
|
||||
const env = {
|
||||
@@ -185,9 +201,34 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const list = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--limit", "4"], env);
|
||||
assertCondition(list.status === 0, "pr list should succeed through REST", list.json ?? { stdout: list.stdout });
|
||||
const listData = dataOf(list.json ?? {});
|
||||
assertCondition(listData.state === "all", "pr list should keep default state=all compatibility", listData);
|
||||
const pullRequests = listData.pullRequests as JsonRecord[];
|
||||
assertCondition(Array.isArray(pullRequests) && pullRequests.length === 1, "pr list should return pullRequests", listData);
|
||||
assertCondition(pullRequests[0]?.number === 42 && pullRequests[0]?.base && pullRequests[0]?.head, "pr list should expose PR summary", pullRequests[0]);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls?state=all&per_page=4"), "default pr list should query state=all", mock.requests);
|
||||
|
||||
const listOpen = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--state", "open", "--limit", "4", "--json", "number,title,state,url"], env);
|
||||
assertCondition(listOpen.status === 0, "pr list should support --state open", listOpen.json ?? { stdout: listOpen.stdout });
|
||||
const listOpenData = dataOf(listOpen.json ?? {});
|
||||
assertCondition(listOpenData.state === "open", "pr list should preserve requested state", listOpenData);
|
||||
const listOpenPrs = listOpenData.pullRequests as JsonRecord[];
|
||||
assertCondition(Array.isArray(listOpenPrs) && listOpenPrs[0]?.state === "open", "pr list --state open should return selected PR fields", listOpenData);
|
||||
assertCondition(!("body" in listOpenPrs[0]), "pr list --json should keep progressive disclosure", listOpenPrs[0]);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls?state=open&per_page=4"), "pr list --state open should query REST state=open", mock.requests);
|
||||
|
||||
const listClosed = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--state", "closed", "--limit", "4", "--json", "number,state,url"], env);
|
||||
assertCondition(listClosed.status === 0, "pr list should support --state closed", listClosed.json ?? { stdout: listClosed.stdout });
|
||||
const listClosedData = dataOf(listClosed.json ?? {});
|
||||
assertCondition(listClosedData.state === "closed", "pr list should preserve requested closed state", listClosedData);
|
||||
const listClosedPrs = listClosedData.pullRequests as JsonRecord[];
|
||||
assertCondition(Array.isArray(listClosedPrs) && listClosedPrs[0]?.number === 43 && listClosedPrs[0]?.state === "closed", "pr list --state closed should return closed PR summaries", listClosedData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/pulls?state=closed&per_page=4"), "pr list --state closed should query REST state=closed", mock.requests);
|
||||
|
||||
const badState = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--state", "merged"], env);
|
||||
assertCondition(badState.status !== 0, "pr list unsupported state should fail", badState.json ?? { stdout: badState.stdout });
|
||||
const badStateData = failedDataOf(badState.json ?? {});
|
||||
assertCondition(badStateData.degradedReason === "validation-failed", "pr list unsupported state should be validation-failed", badStateData);
|
||||
assertCondition(badStateData.runnerDisposition === "business-failed", "pr list unsupported state should be business-failed", badStateData);
|
||||
|
||||
const read = await runCli(["gh", "pr", "read", "42", "--repo", "pikasTech/unidesk", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(read.status === 0, "pr read should succeed through REST", read.json ?? { stdout: read.stdout });
|
||||
|
||||
+19
-8
@@ -53,6 +53,7 @@ type IssueListJsonField = typeof ISSUE_LIST_JSON_FIELDS[number];
|
||||
type PrListJsonField = typeof PR_LIST_JSON_FIELDS[number];
|
||||
type PrReadJsonField = typeof PR_READ_JSON_FIELDS[number];
|
||||
type IssueListState = typeof ISSUE_LIST_STATES[number];
|
||||
type PrListState = typeof ISSUE_LIST_STATES[number];
|
||||
type BodyUpdateMode = typeof BODY_UPDATE_MODES[number];
|
||||
type BoardMutationSection = typeof BOARD_MUTATION_SECTIONS[number];
|
||||
type BoardGithubStatus = typeof BOARD_GITHUB_STATUSES[number];
|
||||
@@ -317,6 +318,7 @@ interface GitHubOptions {
|
||||
prListJsonFields?: PrListJsonField[];
|
||||
prJsonFields?: PrReadJsonField[];
|
||||
listState: IssueListState;
|
||||
prListState: PrListState;
|
||||
mode: BodyUpdateMode;
|
||||
expectUpdatedAt?: string;
|
||||
expectBodySha?: string;
|
||||
@@ -573,6 +575,12 @@ function parseIssueListState(args: string[]): IssueListState {
|
||||
throw new Error(`unsupported gh issue list --state ${raw}; supported states: ${ISSUE_LIST_STATES.join(",")}`);
|
||||
}
|
||||
|
||||
function parsePrListState(args: string[]): PrListState {
|
||||
const raw = optionValue(args, "--state") ?? "all";
|
||||
if ((ISSUE_LIST_STATES as readonly string[]).includes(raw)) return raw as PrListState;
|
||||
throw new Error(`unsupported gh pr list --state ${raw}; supported states: ${ISSUE_LIST_STATES.join(",")}`);
|
||||
}
|
||||
|
||||
function parseBodyUpdateMode(args: string[]): BodyUpdateMode {
|
||||
const raw = optionValue(args, "--mode") ?? "replace";
|
||||
if ((BODY_UPDATE_MODES as readonly string[]).includes(raw)) return raw as BodyUpdateMode;
|
||||
@@ -655,7 +663,8 @@ function parseOptions(args: string[]): GitHubOptions {
|
||||
issueListJsonFields: top === "issue" && sub === "list" ? parseIssueListJsonFields(requestedJsonFields) : undefined,
|
||||
prListJsonFields: top === "pr" && sub === "list" ? parsePrListJsonFields(requestedJsonFields) : undefined,
|
||||
prJsonFields: top === "pr" && isPrReadCommand(sub) ? parsePrReadJsonFields(requestedJsonFields) : undefined,
|
||||
listState: parseIssueListState(args),
|
||||
listState: top === "issue" && (sub === "list" || sub === "board-row" && args[2] === "list") ? parseIssueListState(args) : "open",
|
||||
prListState: top === "pr" && sub === "list" ? parsePrListState(args) : "all",
|
||||
mode: parseBodyUpdateMode(args),
|
||||
expectUpdatedAt: optionValue(args, "--expect-updated-at"),
|
||||
expectBodySha: optionValue(args, "--expect-body-sha"),
|
||||
@@ -4729,15 +4738,16 @@ async function authStatus(repo: string): Promise<GitHubCommandResult> {
|
||||
};
|
||||
}
|
||||
|
||||
async function prList(repo: string, token: string, limit: number, jsonFields: PrListJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
async function prList(repo: string, token: string, state: PrListState, limit: number, jsonFields: PrListJsonField[] | undefined): Promise<GitHubCommandResult> {
|
||||
const { owner, name } = repoParts(repo);
|
||||
const prs = await githubRequest<GitHubPullRequest[]>(token, "GET", `/repos/${owner}/${name}/pulls?state=all&per_page=${limit}`);
|
||||
if (isGitHubError(prs)) return commandError("pr list", repo, prs);
|
||||
const prs = await githubRequest<GitHubPullRequest[]>(token, "GET", `/repos/${owner}/${name}/pulls?state=${state}&per_page=${limit}`);
|
||||
if (isGitHubError(prs)) return commandError("pr list", repo, prs, { state, limit });
|
||||
const fields = jsonFields ?? PR_LIST_JSON_FIELDS.slice();
|
||||
return {
|
||||
ok: true,
|
||||
command: "pr list",
|
||||
repo,
|
||||
state,
|
||||
limit,
|
||||
count: prs.length,
|
||||
jsonFields: fields,
|
||||
@@ -4793,7 +4803,7 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue board-row upsert <issueNumber> [--repo owner/name] --board-issue 20 --section open|closed [--category text] --branch <branch> --tasks <task> --summary <text> --focus <text> --validation <text> --progress <text> [--status OPEN|CLOSED] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh pr list [--repo owner/name] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr list [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr read <number> [--repo owner/name] [--json body,title,state,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup]",
|
||||
"bun scripts/cli.ts gh pr view <number> [--repo owner/name] [compatibility alias for pr read]",
|
||||
"bun scripts/cli.ts gh pr create --title <title> --body-file <file>|--body <text> --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
|
||||
@@ -4808,6 +4818,7 @@ export function ghHelp(): unknown {
|
||||
"Issue and PR create/read/update/comment/close/reopen use GitHub REST and do not require the gh binary when GH_TOKEN or GITHUB_TOKEN is present.",
|
||||
"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.",
|
||||
"PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.",
|
||||
"issue read is the canonical read path; view remains a compatibility alias. Read 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.",
|
||||
@@ -4850,9 +4861,9 @@ 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, "--notify-claudeqq-brief-diff is only supported by gh issue edit 24");
|
||||
}
|
||||
if (optionWasProvided(args, "--state") && !(top === "issue" && (sub === "list" || sub === "board-row" && third === "list"))) {
|
||||
if (optionWasProvided(args, "--state") && !(top === "issue" && (sub === "list" || sub === "board-row" && third === "list") || top === "pr" && sub === "list")) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--state is only supported by gh issue list and gh issue board-row list");
|
||||
return validationError(command, options.repo, "--state is only supported by gh issue list, gh issue board-row list, and gh pr list");
|
||||
}
|
||||
if (optionWasProvided(args, "--json") && !(top === "issue" && (isIssueReadCommand(sub) || sub === "list"))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
@@ -5057,7 +5068,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, `pr ${sub}`, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
|
||||
if (sub === "list") return prList(options.repo, token, options.limit, options.prListJsonFields);
|
||||
if (sub === "list") return prList(options.repo, token, options.prListState, options.limit, options.prListJsonFields);
|
||||
if (sub === "read") return prRead(options.repo, token, parseNumber(third, "pr read"), options.prJsonFields);
|
||||
return prView(options.repo, token, parseNumber(third, "pr view"), options.prJsonFields);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user