Files
orca/src/main/github/merged-pr-commit-membership.ts
T
OrcaWinandOrcaWin 4687e90a16 [comment audit] Continue main-process comment cleanup (#32946)
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci]

* [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci]

* [comment audit] Tighten Git execution and source-control comment contracts [skip ci]

* [comment audit] Clarify repository probes and maintenance comments [skip ci]

* [comment audit] Shorten status, diff-cache, and checkout comments [skip ci]

* [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci]

* [comment audit] Shorten forge identity and review-refresh comments [skip ci]

* [comment audit] Tighten hosted-review client comment contracts [skip ci]

* [comment audit] Clarify GitLab compatibility and review comments [skip ci]

* [comment audit] Tighten hook ownership and watchdog rationale [skip ci]

* [comment audit] Condense host and integration compatibility comments [skip ci]

* [comment audit] Clarify IPC authority and remote-routing rationale [skip ci]

* [comment audit] Finalize file-import and containment comment cleanup

* [comment audit] Fix stale Jira type imports blocking full CI

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 16:27:37 -07:00

128 lines
4.6 KiB
TypeScript

import { ghExecFileAsync } from './gh-utils'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from './rate-limit'
import { githubHostExecOptions } from './github-api-repository'
import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key'
import type { OwnerRepo } from './github-repository-identity'
type GhExecOptions = Parameters<typeof ghExecFileAsync>[1]
// Definitive merged-commit membership is immutable; errors expire sooner so later pushes can resolve unknowns.
const MEMBERSHIP_CACHE_MAX_ENTRIES = 200
const MEMBERSHIP_DEFINITIVE_TTL_MS = 6 * 60 * 60 * 1000
const MEMBERSHIP_ERROR_TTL_MS = 5 * 60 * 1000
const COMMIT_PULLS_PAGE_SIZE = 100
// Full pages through the cap cannot prove absence; keep pathological commit fan-out unknown.
const COMMIT_PULLS_MAX_PAGES = 5
export type MergedPRCommitMembership = 'contained' | 'not-contained' | 'unknown'
const membershipCache = new Map<string, { value: MergedPRCommitMembership; expiresAt: number }>()
function pruneMergedPRCommitMembershipCache(now = Date.now()): void {
for (const [cacheKey, cached] of membershipCache) {
if (cached.expiresAt <= now) {
membershipCache.delete(cacheKey)
}
}
while (membershipCache.size > MEMBERSHIP_CACHE_MAX_ENTRIES) {
const oldestKey = membershipCache.keys().next().value
if (oldestKey === undefined) {
break
}
membershipCache.delete(oldestKey)
}
}
export function resetMergedPRCommitMembershipCacheForTest(): void {
membershipCache.clear()
}
/** Check PR-history membership rather than branch-name reuse; failures return unknown. */
export async function isCommitPartOfMergedPR(args: {
ownerRepo: OwnerRepo
prNumber: number
commitOid: string
ghOptions: GhExecOptions
}): Promise<MergedPRCommitMembership> {
const oid = args.commitOid.trim().toLowerCase()
if (!/^[0-9a-f]{4,64}$/.test(oid) || !Number.isInteger(args.prNumber)) {
return 'unknown'
}
const owner = args.ownerRepo.owner
const repo = args.ownerRepo.repo
const cacheKey = `${githubRepoIdentityKey(args.ownerRepo)}#${args.prNumber}@${oid}`
const ghOptions = { ...args.ghOptions, ...githubHostExecOptions(args.ownerRepo) }
const now = Date.now()
pruneMergedPRCommitMembershipCache(now)
const cached = membershipCache.get(cacheKey)
if (cached && cached.expiresAt > now) {
return cached.value
}
// An unrun probe cannot justify clearing a durable linked PR.
if (repositoryRateLimitGuard(args.ownerRepo, 'core', ghOptions).blocked) {
return 'unknown'
}
try {
// Paginate until found or a short page proves absence; a full final page leaves membership unknown.
for (let page = 1; page <= COMMIT_PULLS_MAX_PAGES; page += 1) {
if (page > 1 && repositoryRateLimitGuard(args.ownerRepo, 'core', ghOptions).blocked) {
membershipCache.set(cacheKey, {
value: 'unknown',
expiresAt: now + MEMBERSHIP_ERROR_TTL_MS
})
return 'unknown'
}
noteRepositoryRateLimitSpend(args.ownerRepo, 'core', 1, ghOptions)
const { stdout } = await ghExecFileAsync(
[
'api',
`repos/${owner}/${repo}/commits/${oid}/pulls?per_page=${COMMIT_PULLS_PAGE_SIZE}&page=${page}`
],
ghOptions
)
const parsed = JSON.parse(stdout) as unknown
// Non-array success is unverified, not evidence that a durable link should be cleared.
if (!Array.isArray(parsed)) {
membershipCache.set(cacheKey, {
value: 'unknown',
expiresAt: now + MEMBERSHIP_ERROR_TTL_MS
})
return 'unknown'
}
const entries = parsed
const contained = entries.some(
(entry) =>
typeof entry === 'object' &&
entry !== null &&
(entry as { number?: unknown }).number === args.prNumber
)
if (contained) {
membershipCache.set(cacheKey, {
value: 'contained',
expiresAt: now + MEMBERSHIP_DEFINITIVE_TTL_MS
})
return 'contained'
}
if (entries.length < COMMIT_PULLS_PAGE_SIZE) {
membershipCache.set(cacheKey, {
value: 'not-contained',
expiresAt: now + MEMBERSHIP_DEFINITIVE_TTL_MS
})
return 'not-contained'
}
}
membershipCache.set(cacheKey, {
value: 'unknown',
expiresAt: now + MEMBERSHIP_ERROR_TTL_MS
})
return 'unknown'
} catch {
// A local commit can become queryable after push; preserve links until verification succeeds.
membershipCache.set(cacheKey, {
value: 'unknown',
expiresAt: now + MEMBERSHIP_ERROR_TTL_MS
})
return 'unknown'
}
}