fix: support labels for gh issue create

This commit is contained in:
Codex
2026-05-20 20:22:50 +00:00
parent 8a822c686e
commit 19933a6d15
4 changed files with 125 additions and 14 deletions
+57 -2
View File
@@ -245,6 +245,32 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
sendJson(res, 201, { id: 9001, body: String(parsed.body ?? ""), html_url: "https://github.com/pikasTech/unidesk/issues/20#issuecomment-9001", user: { login: "tester" }, created_at: "2026-05-20T06:00:00Z", updated_at: "2026-05-20T06:00:00Z" });
return;
}
if (req.method === "POST" && req.url === "/repos/pikasTech/unidesk/issues") {
const parsed = JSON.parse(body) as JsonRecord;
const labels = Array.isArray(parsed.labels) ? parsed.labels.map(String) : [];
if (labels.includes("missing-label")) {
sendJson(res, 422, {
message: "Validation Failed",
errors: [{ resource: "Issue", field: "labels", code: "invalid", value: "missing-label" }],
documentation_url: "https://docs.github.com/rest/issues/issues#create-an-issue",
});
return;
}
sendJson(res, 201, {
id: 9100,
number: 91,
title: String(parsed.title ?? ""),
body: String(parsed.body ?? ""),
state: "open",
html_url: "https://github.com/pikasTech/unidesk/issues/91",
comments: 0,
user: { login: "tester" },
labels: labels.map((name) => ({ name })),
created_at: "2026-05-20T06:05:00Z",
updated_at: "2026-05-20T06:05:00Z",
});
return;
}
if (req.method === "DELETE" && req.url === "/repos/pikasTech/unidesk/issues/comments/9001") {
res.statusCode = 204;
res.end();
@@ -415,12 +441,40 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const appendFile = join(tmp, "append.md");
writeFileSync(appendFile, "\n- appended `code`\n| c | d |\n| --- | --- |\n| 3 | 4 |\n", "utf8");
const issueCreateDryRun = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "body file dry-run", "--body-file", appendFile, "--dry-run"], env);
const issueCreateRequestCountBeforeDryRun = mock.requests.length;
const issueCreateDryRun = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "body file dry-run", "--body-file", appendFile, "--label", "cli,infra", "--label", "ops", "--dry-run"], env);
assertCondition(issueCreateDryRun.status === 0, "issue create dry-run should succeed", issueCreateDryRun.json ?? { stdout: issueCreateDryRun.stdout });
const issueCreateDryRunData = dataOf(issueCreateDryRun.json ?? {});
const issueCreateBodySource = issueCreateDryRunData.bodySource as JsonRecord;
assertCondition(issueCreateDryRunData.planned === true && issueCreateBodySource.kind === "body-file" && issueCreateBodySource.path === appendFile, "issue create dry-run should expose body-file source", issueCreateDryRunData);
const issueCreateDryRunLabels = issueCreateDryRunData.labels as unknown[];
assertCondition(Array.isArray(issueCreateDryRunLabels) && issueCreateDryRunLabels.join(",") === "cli,infra,ops", "issue create dry-run should parse repeated and comma labels", issueCreateDryRunData);
const issueCreateDryRunRequest = issueCreateDryRunData.request as JsonRecord;
const issueCreateDryRunRequestBody = issueCreateDryRunRequest.body as JsonRecord;
assertCondition(Array.isArray(issueCreateDryRunRequestBody.labels) && (issueCreateDryRunRequestBody.labels as unknown[]).join(",") === "cli,infra,ops", "issue create dry-run request plan should include labels", issueCreateDryRunData);
assertCondition(issueCreateDryRunData.request && typeof issueCreateDryRunData.request === "object", "issue create dry-run should expose request plan", issueCreateDryRunData);
const issueCreateDryRunWriteCount = mock.requests.slice(issueCreateRequestCountBeforeDryRun).filter((request) => request.method === "POST" && request.url === "/repos/pikasTech/unidesk/issues").length;
assertCondition(issueCreateDryRunWriteCount === 0, "issue create dry-run must not POST GitHub", { requests: mock.requests.slice(issueCreateRequestCountBeforeDryRun) });
const issueCreateRequestCountBeforeWrite = mock.requests.length;
const issueCreate = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "body file write", "--body-file", appendFile, "--label", "cli", "--label", "infra,ops"], env);
assertCondition(issueCreate.status === 0, "issue create with labels should succeed", issueCreate.json ?? { stdout: issueCreate.stdout });
const issueCreateData = dataOf(issueCreate.json ?? {});
assertCondition(issueCreateData.command === "issue create", "issue create should report command name", issueCreateData);
const issueCreateLabels = issueCreateData.labels as unknown[];
assertCondition(Array.isArray(issueCreateLabels) && issueCreateLabels.join(",") === "cli,infra,ops", "issue create should report labels", issueCreateData);
const issueCreateRequest = mock.requests.slice(issueCreateRequestCountBeforeWrite).find((request) => request.method === "POST" && request.url === "/repos/pikasTech/unidesk/issues");
assertCondition(issueCreateRequest !== undefined, "issue create should POST to GitHub REST issues endpoint", { requests: mock.requests.slice(issueCreateRequestCountBeforeWrite) });
const issueCreatePayload = JSON.parse(issueCreateRequest?.body ?? "{}") as JsonRecord;
assertCondition(Array.isArray(issueCreatePayload.labels) && (issueCreatePayload.labels as unknown[]).join(",") === "cli,infra,ops", "issue create REST payload should include labels", issueCreatePayload);
const issueCreateMissingLabel = await runCli(["gh", "issue", "create", "--repo", "pikasTech/unidesk", "--title", "bad label", "--body-file", appendFile, "--label", "missing-label"], env);
assertCondition(issueCreateMissingLabel.status !== 0, "issue create missing label should fail structurally", issueCreateMissingLabel.json ?? { stdout: issueCreateMissingLabel.stdout });
const missingLabelData = failedDataOf(issueCreateMissingLabel.json ?? {});
assertCondition(missingLabelData.degradedReason === "validation-failed", "missing label should map to validation-failed", missingLabelData);
const missingLabelDetails = missingLabelData.details as JsonRecord;
const missingLabelNestedDetails = missingLabelDetails.details as JsonRecord;
assertCondition(Array.isArray(missingLabelNestedDetails.errors), "missing label error should preserve GitHub validation errors", missingLabelData);
const appendDryRun = await runCli(["gh", "issue", "update", "20", "--repo", "pikasTech/unidesk", "--mode", "append", "--body-file", appendFile, "--dry-run"], env);
assertCondition(appendDryRun.status === 0, "issue update append dry-run should succeed", appendDryRun.json ?? { stdout: appendDryRun.stdout });
@@ -464,7 +518,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"issue list default fields include labels and filter pull requests",
"issue scan-escape classifies pollution, explanatory mentions, and body risks",
"issue cleanup-plan remains dry-run with body/comment cleanup suggestions",
"issue create dry-run exposes body-file source and request plan",
"issue create dry-run parses repeated/comma labels 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 view supports body,title,state,comments selection",
"unsupported --json fields fail structurally",