← Public packages
@gsimone/pr-health
Flag your open PRs that are stuck: red CI, merge conflicts, unanswered review-bot comments, or gone quiet.
src/index.ts
85 lines · 2.7 KB · TypeScriptimport { graphql, describeAccessErrors, OPEN_PRS_QUERY } from './github.ts'
import { classify, formatDigest, type Finding, type PullRequest } from './classify.ts'
export type CheckInput = {
/** Extra GitHub search qualifiers, e.g. "repo:me/thing" or "org:acme". */
scope?: string
/** Max PRs to examine. Default 50. */
limit?: number
/** Bot comments on PRs idle longer than this are history. Default 14. */
botReplyWindowDays?: number
/** Silence past this many days means abandoned, not stuck. Default 120. */
abandonedDays?: number
}
export type CheckResult = {
viewer: string
examined: number
findings: Array<Finding>
digest: string
/** Set when some PRs were unreadable, e.g. an org that refuses the token. */
accessWarning: string | null
}
/**
* Check your open pull requests and return only the ones that are actually
* stuck: red CI, merge conflicts, requested changes, or a review-bot comment
* nobody answered on a still-active PR. Drafts and long-abandoned PRs are
* skipped on purpose. Call this for an on-demand answer; the `daily-check` job
* calls it on a schedule and mails you only when something changed.
*
* @param input - Optional search scope, limit, and tuning thresholds
* @returns Viewer login, PRs examined, the stuck ones, a ready-to-read digest,
* and an access warning when some PRs could not be read
*
* @example
* import checkPrHealth from 'kody:@gsimone/pr-health'
*
* const { digest } = await checkPrHealth()
*/
export default async function checkPrHealth(
input: CheckInput = {},
): Promise<CheckResult> {
const limit = input.limit ?? 50
const viewerResult = await graphql<{ viewer: { login: string } }>(
'query { viewer { login } }',
)
const viewer = viewerResult.data.viewer.login
// Your own PRs plus anything awaiting your review — both can be "stuck".
const scope = input.scope ? ` ${input.scope}` : ''
const search = `is:open is:pr involves:${viewer}${scope}`
const { data, errors } = await graphql<{
search: { nodes: Array<PullRequest | null> }
}>(OPEN_PRS_QUERY, { search, first: limit })
const nodes = (data.search?.nodes ?? []).filter(
(node): node is PullRequest => typeof node?.number === 'number',
)
const now = Date.now()
const findings = nodes
.map((pr) =>
classify(pr, {
viewer,
now,
botReplyWindowDays: input.botReplyWindowDays,
abandonedDays: input.abandonedDays,
}),
)
.filter((finding): finding is Finding => finding !== null)
// Worst first: most reasons, then most recently touched.
.sort(
(a, b) =>
b.reasons.length - a.reasons.length || a.idleDays - b.idleDays,
)
return {
viewer,
examined: nodes.length,
findings,
digest: formatDigest(findings),
accessWarning: describeAccessErrors(errors),
}
}