Files
pikasTech-unidesk/scripts/src/gh/pr-ready.ts
T
2026-07-11 06:30:19 +02:00

170 lines
5.0 KiB
TypeScript

// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split
import { repoParts } from "./auth-and-safety";
import { commandError, errorPayload, githubGraphqlRequest, githubRequest, isGitHubError, validationError } from "./client";
import { prCompactSummary } from "./pr-summary";
import type { GitHubCommandResult, GitHubPullRequest } from "./types";
interface ReadyPullRequestGraphql {
id?: string;
number?: number;
title?: string;
state?: string;
isDraft?: boolean;
url?: string;
headRefName?: string | null;
baseRefName?: string | null;
updatedAt?: string | null;
}
interface ReadyPullRequestMutation {
markPullRequestReadyForReview?: {
pullRequest?: ReadyPullRequestGraphql | null;
} | null;
}
function mutationPlan(): Record<string, unknown> {
return {
method: "POST",
path: "/graphql",
mutation: "markPullRequestReadyForReview",
variables: { pullRequestId: "present" },
};
}
function readyPullRequestSummary(pr: ReadyPullRequestGraphql, fallback: GitHubPullRequest): Record<string, unknown> {
return {
id: pr.id ?? fallback.node_id ?? null,
number: pr.number ?? fallback.number,
title: pr.title ?? fallback.title,
state: (pr.state ?? fallback.state).toLowerCase(),
stateDetail: (pr.state ?? fallback.state).toLowerCase(),
draft: pr.isDraft ?? false,
url: pr.url ?? fallback.html_url,
head: { ref: pr.headRefName ?? fallback.head?.ref ?? null, sha: fallback.head?.sha ?? null },
base: { ref: pr.baseRefName ?? fallback.base?.ref ?? null, sha: fallback.base?.sha ?? null },
headRefName: pr.headRefName ?? fallback.head?.ref ?? null,
baseRefName: pr.baseRefName ?? fallback.base?.ref ?? null,
updatedAt: pr.updatedAt ?? fallback.updated_at ?? null,
};
}
export async function prReady(repo: string, token: string, number: number, dryRun: boolean): Promise<GitHubCommandResult> {
const command = "pr ready";
const { owner, name } = repoParts(repo);
const current = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
if (isGitHubError(current)) return commandError(command, repo, current, { number, phase: "fetch-pr" });
const pullRequest = prCompactSummary(current);
if (current.state !== "open") {
return validationError(command, repo, "只有 open 状态的 PR 才能转为 ready for review", {
number,
phase: "validate-pr-state",
currentState: current.state,
pullRequest,
});
}
if (typeof current.draft !== "boolean") {
return commandError(command, repo, errorPayload("invalid-response", "GitHub REST PR 响应缺少 draft 布尔状态,无法安全判断是否需要写入"), {
number,
phase: "validate-pr-draft",
pullRequest,
});
}
if (current.draft !== true) {
return {
ok: true,
command,
repo,
number,
dryRun,
planned: false,
changed: false,
action: "already-ready",
changedFields: [],
pullRequest,
rest: true,
graphQl: false,
};
}
const request = mutationPlan();
if (dryRun) {
return {
ok: true,
command,
repo,
number,
dryRun: true,
planned: true,
changed: false,
action: "would-mark-ready",
changedFields: ["draft"],
pullRequest,
request,
rest: true,
graphQl: false,
};
}
if (typeof current.node_id !== "string" || current.node_id.length === 0) {
return commandError(command, repo, errorPayload("invalid-response", "GitHub REST PR 响应缺少 markPullRequestReadyForReview 所需的 node_id"), {
number,
phase: "validate-pr-node-id",
pullRequest,
});
}
const mutation = `
mutation MarkPullRequestReadyForReview($pullRequestId: ID!) {
markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {
pullRequest {
id
number
title
state
isDraft
url
headRefName
baseRefName
updatedAt
}
}
}
`;
const response = await githubGraphqlRequest<ReadyPullRequestMutation>(token, mutation, { pullRequestId: current.node_id });
if (isGitHubError(response)) {
return commandError(command, repo, response, {
number,
phase: "mark-ready",
pullRequest,
request,
});
}
const ready = response.markPullRequestReadyForReview?.pullRequest;
if (ready === null || ready === undefined || ready.isDraft !== false) {
return commandError(command, repo, errorPayload("invalid-response", "GitHub GraphQL markPullRequestReadyForReview 响应未确认 isDraft=false", { details: response }), {
number,
phase: "verify-ready-response",
pullRequest,
request,
});
}
return {
ok: true,
command,
repo,
number: ready.number ?? number,
dryRun: false,
planned: false,
changed: true,
action: "marked-ready",
changedFields: ["draft"],
pullRequest: readyPullRequestSummary(ready, current),
request,
rest: true,
graphQl: true,
};
}