import { graphql } from '../core.ts'
import type { GitHubAccount } from '../types.ts'
import getPrInfo from './get-info.ts'
import { normalizePrLocator, normalizeSetPrReviewStatusInput } from './pr-input.ts'
const CONVERT_TO_DRAFT = `
mutation ConvertToDraft($prId: ID!) {
convertPullRequestToDraft(input: { pullRequestId: $prId }) {
pullRequest { id number title isDraft }
}
}
`
const MARK_READY = `
mutation MarkReadyForReview($prId: ID!) {
markPullRequestReadyForReview(input: { pullRequestId: $prId }) {
pullRequest { id number title isDraft }
}
}
`
/** Bound GraphQL review-status mutations; same hang class as merge. */
const DEFAULT_MUTATION_TIMEOUT_MS = 25_000
/**
* Set a pull request to draft, ready-for-review, or toggle between them.
*
* The GraphQL mutation is bounded by `timeoutMs` (default 25000) so a stuck
* GitHub mutation fails fast instead of hanging the execute sandbox.
*
* @param params - PR locator plus `status: 'draft' | 'ready' | 'toggle'`,
* optional `account`, and optional `timeoutMs`.
* @returns Whether the status changed and the updated draft flag.
* @example
* import setPrReviewStatus from 'kody:@kentcdodds/github/pr/set-review-status'
* const result = await setPrReviewStatus({ owner: 'kentcdodds', repo: 'kody', prNumber: 1, status: 'ready' })
* // => { changed: true, draft: false, message: 'PR #1 is now ready for review.' }
*/
export default async function setPrReviewStatus(params: Record<string, unknown> = {}) {
const account = (params.account as GitHubAccount | undefined) ?? 'bot'
const timeoutMs = readTimeoutMs(params)
if (params.status === 'toggle') {
const locator = normalizePrLocator(params)
const pr = await getPrInfo({ ...locator, account })
const status = pr.draft ? 'ready' : 'draft'
return setPrReviewStatus({ ...locator, account, status, timeoutMs })
}
const { status, ...locator } = normalizeSetPrReviewStatusInput(params)
const pr = await getPrInfo({ ...locator, account })
if (pr.state !== 'open') {
throw new Error(
`PR #${pr.number} is ${pr.state}, not open — cannot change review status.`,
)
}
const alreadyCorrect =
(status === 'draft' && pr.draft) || (status === 'ready' && !pr.draft)
if (alreadyCorrect) {
return {
changed: false,
prNumber: pr.number,
title: pr.title,
url: pr.url,
draft: pr.draft,
message: `PR #${pr.number} is already ${pr.draft ? 'a draft' : 'ready for review'}.`,
}
}
const result = await graphql<Record<string, Record<string, { pullRequest: Record<string, unknown> }>>>({
account,
query: status === 'draft' ? CONVERT_TO_DRAFT : MARK_READY,
variables: { prId: pr.nodeId },
throwOnError: true,
timeoutMs,
})
const updated =
status === 'draft'
? result.data!.convertPullRequestToDraft.pullRequest
: result.data!.markPullRequestReadyForReview.pullRequest
return {
changed: true,
prNumber: pr.number,
title: pr.title,
url: pr.url,
draft: updated.isDraft as boolean,
message: `PR #${pr.number} is now ${updated.isDraft ? 'a draft' : 'ready for review'}.`,
}
}
function readTimeoutMs(params: Record<string, unknown>): number {
if (params.timeoutMs === undefined) return DEFAULT_MUTATION_TIMEOUT_MS
const value = Number(params.timeoutMs)
if (!Number.isFinite(value) || value <= 0) {
throw new Error('timeoutMs must be a positive number of milliseconds')
}
return value
}