135 lines
3.6 KiB
TypeScript
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)
|
|
}
|
|
}
|