feat: add gh pr preflight helper

This commit is contained in:
Codex
2026-05-23 08:42:41 +00:00
parent cdaf28a5c4
commit 90ffd028de
6 changed files with 323 additions and 8 deletions
+41
View File
@@ -239,6 +239,8 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
assertCondition(usage.some((line) => line.includes("gh pr read")), "gh help should list pr read", { usage });
assertCondition(usage.some((line) => line.includes("gh pr view")), "gh help should list pr view", { usage });
assertCondition(usage.some((line) => line.includes("gh pr read") && line.includes("owner/repo#number") && line.includes("--raw|--full")), "gh help should document pr shorthand and raw/full disclosure", { usage });
assertCondition(usage.some((line) => line.includes("gh preflight")), "gh help should list top-level preflight alias", { usage });
assertCondition(usage.some((line) => line.includes("gh pr preflight")), "gh help should list pr preflight", { 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 edit")), "gh help should list pr edit", { usage });
assertCondition(usage.some((line) => line.includes("gh pr comment")), "gh help should list pr comment", { usage });
@@ -250,6 +252,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
assertCondition(notes.some((line) => line.includes("--raw and --full are explicit full-disclosure aliases")), "gh help should explain raw/full read disclosure", { notes });
assertCondition(notes.some((line) => line.includes("PR list defaults to --state all")), "gh help should document pr list default state", { notes });
assertCondition(notes.some((line) => line.includes("stateDetail") && line.includes("mergedAt")), "gh help should describe closeout field normalization", { notes });
assertCondition(notes.some((line) => line.includes("low-noise read-only closeout helper")), "gh help should document PR preflight closeout helper", { notes });
const mock = await startMockGitHub();
const env = {
@@ -367,6 +370,41 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
assertCondition(unsupportedReadMessage.includes("projectCards"), "unsupported pr read field message should name the bad field", unsupportedReadFieldData);
assertCondition(unsupportedReadMessage.includes("mergedAt") && unsupportedReadMessage.includes("mergeCommit"), "unsupported pr read field message should include supported closeout fields", unsupportedReadFieldData);
const closeoutPreflight = await runCli(["gh", "pr", "preflight", "42", "--repo", "pikasTech/unidesk"], env);
assertCondition(closeoutPreflight.status === 0, "pr preflight should succeed through REST and GraphQL", closeoutPreflight.json ?? { stdout: closeoutPreflight.stdout });
assertCondition(!closeoutPreflight.stdout.includes("contract-token"), "pr preflight must not print token values", { stdout: closeoutPreflight.stdout });
const closeoutPreflightData = dataOf(closeoutPreflight.json ?? {});
assertCondition(closeoutPreflightData.command === "pr preflight", "pr preflight should report command", closeoutPreflightData);
assertCondition(closeoutPreflightData.readOnly === true && closeoutPreflightData.writesRemote === false, "pr preflight must stay read-only", closeoutPreflightData);
assertCondition(!("raw" in closeoutPreflightData), "pr preflight default output should omit raw payloads", closeoutPreflightData);
const authCapability = closeoutPreflightData.authCapability as JsonRecord;
assertCondition(authCapability.ok === true && authCapability.tokenPresent === true && authCapability.tokenSource === "GH_TOKEN", "pr preflight should expose redacted auth capability", authCapability);
assertCondition(authCapability.valuesPrinted === false, "pr preflight should explicitly avoid secret values", authCapability);
const preflightPr = closeoutPreflightData.pullRequest as JsonRecord;
assertCondition(preflightPr.number === 42 && preflightPr.bodyOmitted === true, "pr preflight should return bounded PR metadata", preflightPr);
const mergeability = closeoutPreflightData.mergeability as JsonRecord;
assertCondition(mergeability.mergeable === "MERGEABLE" && mergeability.mergeStateStatus === "CLEAN", "pr preflight should expose mergeability", mergeability);
assertCondition(mergeability.readyForCommanderMerge === true && mergeability.conclusion === "ready", "pr preflight should summarize closeout readiness", mergeability);
const preflightStatus = closeoutPreflightData.statusChecks as JsonRecord;
const preflightCounts = preflightStatus.counts as JsonRecord;
assertCondition(preflightStatus.state === "SUCCESS" && preflightStatus.rawOmitted === true, "pr preflight default status rollup should be compact", preflightStatus);
assertCondition(preflightCounts.success === 2, "pr preflight should count successful contexts", preflightStatus);
const policy = closeoutPreflightData.policy as JsonRecord;
assertCondition(policy.mergesPr === false && policy.mergeCommandSupported === false && policy.unideskCliMergeSupported === false, "pr preflight policy should block UniDesk CLI merge execution", policy);
const aliasPreflight = await runCli(["gh", "preflight", "42", "--repo", "pikasTech/unidesk"], env);
assertCondition(aliasPreflight.status === 0, "top-level gh preflight alias should succeed", aliasPreflight.json ?? { stdout: aliasPreflight.stdout });
const aliasPreflightData = dataOf(aliasPreflight.json ?? {});
assertCondition(aliasPreflightData.command === "preflight", "top-level gh preflight should report alias command", aliasPreflightData);
assertCondition((aliasPreflightData.policy as JsonRecord).mergesPr === false, "top-level gh preflight alias must not merge", aliasPreflightData);
const fullPreflight = await runCli(["gh", "pr", "preflight", "42", "--repo", "pikasTech/unidesk", "--full"], env);
assertCondition(fullPreflight.status === 0, "pr preflight --full should succeed", fullPreflight.json ?? { stdout: fullPreflight.stdout });
const fullPreflightData = dataOf(fullPreflight.json ?? {});
const fullStatus = fullPreflightData.statusChecks as JsonRecord;
assertCondition(fullStatus.rawOmitted === false && Array.isArray(fullStatus.contexts), "pr preflight --full should include status contexts", fullStatus);
assertCondition(typeof fullPreflightData.raw === "object" && fullPreflightData.raw !== null, "pr preflight --full should include raw read payload summary", fullPreflightData);
const preflight = await runBun([
"scripts/code-queue-pr-preflight-example.ts",
"--repo",
@@ -558,6 +596,9 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
"GitHub DNS/API transients are retryable and distinct from auth or PR semantic failures",
"pr view closeout metadata fields are accepted and hydrated through GraphQL",
"pr read unsupported fields fail structurally with supported closeout fields listed",
"pr preflight exposes redacted auth plus compact merge/status closeout metadata",
"top-level gh preflight alias works for commander closeout",
"pr preflight --full is the explicit status-context disclosure path",
"pr create dry-run exposes planned operation",
"pr comment dry-run preserves markdown text",
"pr update/edit use low-noise REST PATCH without GraphQL projectCards",