orca/src/shared/hosted-review-queue.ts

135 lines
3.6 KiB
TypeScript

import type {
HostedReviewIdentity,
HostedReviewQueueClassification,
HostedReviewQueueState,
HostedReviewQueueSummary,
HostedReviewUser
} from './hosted-review'
export type HostedReviewClassificationOptions = {
viewer?: HostedReviewUser | null
agentAuthorLogins?: string[]
}
export function hostedReviewIdentityKey(identity: HostedReviewIdentity): string {
return `${identity.provider}::${identity.host.toLowerCase()}::${identity.owner.toLowerCase()}::${identity.repo.toLowerCase()}::${identity.number}`
}
function hasRequestedReviewerSignal(
summary: HostedReviewQueueSummary,
viewer?: HostedReviewUser | null
): boolean {
if (!viewer?.login) {
return false
}
const requested = summary.requestedReviewerLogins
if (!requested || requested.length === 0) {
return false
}
const viewerLogin = viewer.login.toLowerCase()
return requested.some((login) => login.toLowerCase() === viewerLogin)
}
function isAgentAuthored(
summary: HostedReviewQueueSummary,
options?: HostedReviewClassificationOptions
): boolean {
if (summary.author?.isBot) {
return true
}
const author = summary.author?.login?.toLowerCase()
if (!author) {
return false
}
if (options?.agentAuthorLogins?.some((login) => login.toLowerCase() === author)) {
return true
}
return author.endsWith('[bot]') || author.includes('bot')
}
function getQueueState(
summary: HostedReviewQueueSummary,
options?: HostedReviewClassificationOptions
): HostedReviewQueueState {
const viewerLogin = options?.viewer?.login?.toLowerCase() ?? null
const authorLogin = summary.author?.login?.toLowerCase() ?? null
if (viewerLogin && authorLogin && viewerLogin === authorLogin) {
return 'mine'
}
if (hasRequestedReviewerSignal(summary, options?.viewer)) {
return 'requested'
}
if (isAgentAuthored(summary, options)) {
return 'agent'
}
return 'teammate'
}
export function reviewNeedsResponse(
summary: HostedReviewQueueSummary,
viewer?: HostedReviewUser | null
): boolean {
void viewer
if (summary.state !== 'open' && summary.state !== 'draft') {
return false
}
if ((summary.threadSummary?.unresolvedCount ?? 0) > 0) {
return true
}
if (summary.checksStatus === 'failure') {
return true
}
if (summary.mergeable === 'CONFLICTING') {
return true
}
if (summary.lastViewedAt === undefined) {
return false
}
const updatedAt = Date.parse(summary.updatedAt)
return Number.isFinite(updatedAt) && updatedAt > summary.lastViewedAt
}
export function reviewReadyToMerge(summary: HostedReviewQueueSummary): boolean {
if (summary.state !== 'open') {
return false
}
if (summary.draft) {
return false
}
if (summary.mergeable !== 'MERGEABLE') {
return false
}
if (
summary.identity.provider === 'github' &&
(summary.mergeStateStatus === 'BEHIND' || summary.mergeStateStatus === 'BLOCKED')
) {
return false
}
if (
summary.reviewDecision === 'review_required' ||
summary.reviewDecision === 'changes_requested'
) {
return false
}
if (summary.checksStatus !== 'success' && summary.checksStatus !== 'neutral') {
return false
}
if (summary.threadSummary?.unresolvedCount !== 0) {
return false
}
return true
}
export function classifyHostedReview(
summary: HostedReviewQueueSummary,
options?: HostedReviewClassificationOptions
): HostedReviewQueueClassification {
const state = getQueueState(summary, options)
return {
state,
requested: hasRequestedReviewerSignal(summary, options?.viewer),
needsResponse: reviewNeedsResponse(summary, options?.viewer),
readyToMerge: reviewReadyToMerge(summary)
}
}