fix: expose issue lifecycle json fields
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -98,6 +98,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
user: { login: "tester" },
|
||||
created_at: "2026-05-20T00:00:00Z",
|
||||
updated_at: "2026-05-20T01:00:00Z",
|
||||
closed_at: null,
|
||||
};
|
||||
const shorthandIssue = {
|
||||
id: 7000,
|
||||
@@ -110,6 +111,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
user: { login: "tester" },
|
||||
created_at: "2026-05-20T02:00:00Z",
|
||||
updated_at: shorthandIssueUpdatedAt,
|
||||
closed_at: null,
|
||||
};
|
||||
const boardIssueBodyInitial = [
|
||||
"# Code Queue",
|
||||
@@ -254,6 +256,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
labels: [{ name: "cli", color: "1d76db", description: "CLI work" }],
|
||||
created_at: "2026-05-20T02:00:00Z",
|
||||
updated_at: "2026-05-20T03:00:00Z",
|
||||
closed_at: null,
|
||||
},
|
||||
{
|
||||
id: 2002,
|
||||
@@ -267,6 +270,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
labels: [],
|
||||
created_at: "2026-05-20T02:05:00Z",
|
||||
updated_at: "2026-05-20T03:05:00Z",
|
||||
closed_at: null,
|
||||
},
|
||||
{
|
||||
id: 3001,
|
||||
@@ -296,6 +300,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
labels: [],
|
||||
created_at: "2026-05-20T02:30:00Z",
|
||||
updated_at: "2026-05-20T03:30:00Z",
|
||||
closed_at: null,
|
||||
},
|
||||
];
|
||||
const scanIssues = [
|
||||
@@ -413,6 +418,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
labels: [],
|
||||
created_at: "2026-05-18T02:00:00Z",
|
||||
updated_at: "2026-05-20T03:00:00Z",
|
||||
closed_at: "2026-05-20T03:15:00Z",
|
||||
},
|
||||
{
|
||||
id: 2040,
|
||||
@@ -426,6 +432,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
labels: [],
|
||||
created_at: "2026-05-19T02:00:00Z",
|
||||
updated_at: "2026-05-20T03:00:00Z",
|
||||
closed_at: "2026-05-20T03:20:00Z",
|
||||
},
|
||||
{
|
||||
id: 2041,
|
||||
@@ -439,6 +446,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
labels: [],
|
||||
created_at: "2026-05-19T02:05:00Z",
|
||||
updated_at: "2026-05-20T03:05:00Z",
|
||||
closed_at: "2026-05-20T03:25:00Z",
|
||||
},
|
||||
];
|
||||
const comments = [
|
||||
@@ -663,6 +671,11 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
|
||||
const mock = await startMockGitHub();
|
||||
const tmp = mkdtempSync(join(tmpdir(), "unidesk-gh-issue-guard-"));
|
||||
const startedAt = Date.now();
|
||||
const heartbeat = setInterval(() => {
|
||||
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
|
||||
process.stderr.write(`[gh-issue-contract] elapsed=${elapsedSeconds}s requests=${mock.requests.length}\n`);
|
||||
}, 10_000);
|
||||
const env = {
|
||||
GH_TOKEN: "contract-token-should-not-print",
|
||||
UNIDESK_GITHUB_API_URL: mock.baseUrl,
|
||||
@@ -680,6 +693,18 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(listOpenIssues[0]?.title === "master:补齐 UniDesk CLI gh issue list 与 PR 驱动最小闭环前置能力", "issue list should expose title field", listOpenData);
|
||||
assertCondition(!("labels" in listOpenIssues[0]), "issue list --json should select only requested fields", listOpenIssues[0]);
|
||||
|
||||
const listOpenLifecycle = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "open", "--limit", "2", "--json", "number,state,closed,closedAt"], env);
|
||||
assertCondition(listOpenLifecycle.status === 0, "issue list lifecycle fields should succeed", listOpenLifecycle.json ?? { stdout: listOpenLifecycle.stdout });
|
||||
const listOpenLifecycleData = dataOf(listOpenLifecycle.json ?? {});
|
||||
const listOpenLifecycleIssues = listOpenLifecycleData.issues as JsonRecord[];
|
||||
assertCondition(listOpenLifecycleIssues[0]?.closed === false && listOpenLifecycleIssues[0]?.closedAt === null, "open issue list rows should expose closed=false and closedAt=null", listOpenLifecycleData);
|
||||
|
||||
const listClosedLifecycle = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "closed", "--limit", "2", "--json", "number,state,closed,closedAt"], env);
|
||||
assertCondition(listClosedLifecycle.status === 0, "closed issue list lifecycle fields should succeed", listClosedLifecycle.json ?? { stdout: listClosedLifecycle.stdout });
|
||||
const listClosedLifecycleData = dataOf(listClosedLifecycle.json ?? {});
|
||||
const listClosedLifecycleIssues = listClosedLifecycleData.issues as JsonRecord[];
|
||||
assertCondition(listClosedLifecycleIssues[0]?.state === "closed" && listClosedLifecycleIssues[0]?.closed === true && listClosedLifecycleIssues[0]?.closedAt === "2026-05-20T03:15:00Z", "closed issue list rows should expose closed=true and closedAt", listClosedLifecycleData);
|
||||
|
||||
const acceptanceList = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "open", "--limit", "5", "--json", "number,title,state,url"], env);
|
||||
assertCondition(acceptanceList.status === 0, "acceptance issue list command should succeed under mock GitHub", acceptanceList.json ?? { stdout: acceptanceList.stdout });
|
||||
const acceptanceListData = dataOf(acceptanceList.json ?? {});
|
||||
@@ -1256,18 +1281,19 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(shorthandConflictData.degradedReason === "validation-failed", "conflicting --repo should be validation-failed", shorthandConflictData);
|
||||
assertCondition(String(shorthandConflictData.message ?? "").includes("resolves to repo pikasTech/HWLAB"), "conflict message should name the derived repo", shorthandConflictData);
|
||||
const issueConflictCommands = shorthandConflictData.supportedCommands as string[];
|
||||
assertCondition(Array.isArray(issueConflictCommands) && issueConflictCommands.some((command) => command === "bun scripts/cli.ts gh issue read 7 --repo pikasTech/HWLAB --json body,title,state,comments"), "conflict should include the exact supported issue read command", shorthandConflictData);
|
||||
assertCondition(Array.isArray(issueConflictCommands) && issueConflictCommands.some((command) => command === "bun scripts/cli.ts gh issue read 7 --repo pikasTech/HWLAB --json body,title,state,closed,closedAt,comments,number,url,author,createdAt,updatedAt"), "conflict should include the exact supported issue read command", shorthandConflictData);
|
||||
|
||||
const rawIssueList = await runCli(["gh", "issue", "list", "--raw"], env);
|
||||
assertCondition(rawIssueList.status === 0, "issue list --raw should be a supported explicit list disclosure path", rawIssueList.json ?? { stdout: rawIssueList.stdout });
|
||||
const rawIssueListData = dataOf(rawIssueList.json ?? {});
|
||||
assertCondition(rawIssueListData.command === "issue list" && rawIssueListData.rawCount === 3, "issue list --raw should keep compact list semantics with raw pagination metadata", rawIssueListData);
|
||||
|
||||
const readFields = await runCli(["gh", "issue", "read", "20", "--repo", "pikasTech/unidesk", "--json", "body,title,state,comments"], env);
|
||||
const readFields = await runCli(["gh", "issue", "read", "20", "--repo", "pikasTech/unidesk", "--json", "body,title,state,closed,closedAt,comments"], env);
|
||||
assertCondition(readFields.status === 0, "common --json field selection should succeed", readFields.json ?? { stdout: readFields.stdout });
|
||||
const readFieldsData = dataOf(readFields.json ?? {});
|
||||
const fieldsJson = readFieldsData.json as JsonRecord;
|
||||
assertCondition(fieldsJson.title === "长期总看板", "selected json title should be exposed", fieldsJson);
|
||||
assertCondition(fieldsJson.closed === false && fieldsJson.closedAt === null, "open issue read should expose lifecycle fields", fieldsJson);
|
||||
assertCondition(Array.isArray(fieldsJson.comments) && fieldsJson.comments.length === 1, "selected json comments should be exposed", fieldsJson);
|
||||
|
||||
const unsupported = await runCli(["gh", "issue", "read", "20", "--repo", "pikasTech/unidesk", "--json", "body,unknown"], env);
|
||||
@@ -1760,7 +1786,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
"issue create dry-run parses repeated/comma labels, supports stdin, rejects inline --body, and exposes request plan",
|
||||
"issue create sends labels through REST and preserves GitHub validation errors for missing labels",
|
||||
"issue list unsupported fields and states fail structurally",
|
||||
"issue read supports body,title,state,comments selection",
|
||||
"issue read supports body,title,state,closed,closedAt,comments selection",
|
||||
"unknown/full disclosure option guidance remains actionable",
|
||||
"unsupported --json fields fail structurally",
|
||||
"issue edit --body-file rejects literal null",
|
||||
@@ -1783,6 +1809,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
clearInterval(heartbeat);
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
await mock.close();
|
||||
}
|
||||
|
||||
+14
-7
@@ -24,8 +24,8 @@ const DEFAULT_BOARD_KNOWN_META_ISSUES = [CODE_QUEUE_BOARD_TARGET_ISSUE, COMMANDE
|
||||
const BOARD_AUDIT_REQUIRED_COLUMNS = ["branch", "acceptance", "relatedTask", "progress"] as const;
|
||||
const BOARD_ROW_FIELDS = ["progress", "status", "validation", "branch", "tasks", "focus"] as const;
|
||||
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 ISSUE_VIEW_JSON_FIELDS = ["body", "title", "state", "closed", "closedAt", "comments", "number", "url", "author", "createdAt", "updatedAt"] as const;
|
||||
const ISSUE_LIST_JSON_FIELDS = ["number", "title", "state", "closed", "closedAt", "url", "updatedAt", "createdAt", "author", "labels"] as const;
|
||||
const PR_LIST_JSON_FIELDS = [
|
||||
"body",
|
||||
"title",
|
||||
@@ -428,6 +428,7 @@ interface GitHubIssue {
|
||||
pull_request?: unknown;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
closed_at?: string | null;
|
||||
}
|
||||
|
||||
interface GitHubComment {
|
||||
@@ -972,7 +973,7 @@ function readViewSupportedCommands(kind: "issue" | "pr", repo: string, number: n
|
||||
|
||||
function readViewSupportedJsonFields(kind: "issue" | "pr"): string {
|
||||
return kind === "issue"
|
||||
? "body,title,state,comments"
|
||||
? ISSUE_VIEW_JSON_FIELDS.join(",")
|
||||
: "body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup";
|
||||
}
|
||||
|
||||
@@ -2072,6 +2073,8 @@ function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; prev
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
closed: issue.state === "closed",
|
||||
closedAt: issue.closed_at ?? null,
|
||||
url: issue.html_url,
|
||||
author: issue.user?.login ?? null,
|
||||
createdAt: issue.created_at ?? null,
|
||||
@@ -2097,6 +2100,8 @@ function issueLifecycleSummary(issue: GitHubIssue): Record<string, unknown> {
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
closed: issue.state === "closed",
|
||||
closedAt: issue.closed_at ?? null,
|
||||
url: issue.html_url,
|
||||
author: issue.user?.login ?? null,
|
||||
createdAt: issue.created_at ?? null,
|
||||
@@ -2151,6 +2156,8 @@ function issueListSummary(issue: GitHubIssue, fields: IssueListJsonField[]): Rec
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
closed: issue.state === "closed",
|
||||
closedAt: issue.closed_at ?? null,
|
||||
url: issue.html_url,
|
||||
updatedAt: issue.updated_at ?? null,
|
||||
createdAt: issue.created_at ?? null,
|
||||
@@ -6376,8 +6383,8 @@ export function ghHelp(): unknown {
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts gh auth status [--repo owner/name]",
|
||||
"bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,closed,closedAt,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for issue read]",
|
||||
"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] [--full|--raw]",
|
||||
@@ -6420,9 +6427,9 @@ 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 and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
|
||||
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. Supported --json fields are number,title,state,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
|
||||
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. Supported --json fields are number,title,state,closed,closedAt,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/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. Read supports legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"issue read is the canonical read path; view remains a compatibility alias. Read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. Read supports lifecycle fields closed/closedAt plus legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/update/edit and gh pr list/read/view. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body only on commands that explicitly support full disclosure.",
|
||||
"GitHub CLI output larger than 20 KiB is automatically written to /tmp/unidesk-cli-output/*.json; stdout stays bounded JSON with outputTruncated=true, the dump path, total bytes/lines, and head/tail previews.",
|
||||
"issue create accepts --body-file <file|-> plus repeatable --label values and comma-separated labels; inline --body is intentionally unsupported for issue creation. Dry-run prints the parsed labels and non-dry-run sends them in the GitHub REST create-issue payload.",
|
||||
|
||||
Reference in New Issue
Block a user