fix: bound github merge metadata cache (#4154)

This commit is contained in:
Neil
2026-05-31 06:12:38 -07:00
committed by GitHub
parent 74455bfe52
commit 09d2227a04
2 changed files with 65 additions and 2 deletions
+36
View File
@@ -100,6 +100,7 @@ import {
setPRAutoMerge,
updatePRState,
updatePRTitle,
_getMergeQueueCacheSizeForTests,
_resetOwnerRepoCache,
_resetMergeQueueCacheForTests
} from './client'
@@ -1686,6 +1687,41 @@ describe('GitHub GraphQL rate-limit guard', () => {
).toHaveLength(1)
})
it('bounds merge metadata cache entries across many base branches', async () => {
getOwnerRepoMock.mockResolvedValue({ owner: 'stablyai', repo: 'orca' })
let prViewCount = 0
ghExecFileAsyncMock.mockImplementation(async (args) => {
if (args.includes('graphql')) {
return { stdout: JSON.stringify({ data: { repository: { mergeQueue: null } } }) }
}
prViewCount += 1
return {
stdout: JSON.stringify({
number: prViewCount,
title: 'PR',
state: 'OPEN',
url: `https://github.com/stablyai/orca/pull/${prViewCount}`,
statusCheckRollup: [],
updatedAt: '2026-04-01T00:00:00Z',
isDraft: false,
mergeable: 'MERGEABLE',
reviewDecision: 'APPROVED',
mergeStateStatus: 'CLEAN',
autoMergeRequest: null,
baseRefName: `base-${prViewCount}`,
baseRefOid: 'base-oid',
headRefOid: 'head-oid'
})
}
})
for (let i = 0; i < 260; i++) {
await getPRForBranch('/repo-root', `feature/${i}`, i + 1)
}
expect(_getMergeQueueCacheSizeForTests()).toBe(256)
})
it('returns conflicting file details instead of running gh merge when PR is dirty', async () => {
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
+29 -2
View File
@@ -90,6 +90,7 @@ type GhExecOptions = ReturnType<typeof ghRepoExecOptions>
const ORCA_REPO = 'stablyai/orca'
const MERGE_QUEUE_CACHE_TTL_MS = 10 * 60 * 1000
const MERGE_QUEUE_UNKNOWN_CACHE_TTL_MS = 60 * 1000
const MERGE_QUEUE_CACHE_MAX_ENTRIES = 256
type GitHubRepositoryMergeMetadata = {
mergeQueueRequired: boolean | null
mergeMethodSettings?: GitHubPRMergeMethodSettings
@@ -103,6 +104,25 @@ export function _resetMergeQueueCacheForTests(): void {
repositoryMergeMetadataCache.clear()
}
export function _getMergeQueueCacheSizeForTests(): number {
return repositoryMergeMetadataCache.size
}
function pruneRepositoryMergeMetadataCache(now = Date.now()): void {
for (const [cacheKey, cached] of repositoryMergeMetadataCache) {
if (cached.expiresAt <= now) {
repositoryMergeMetadataCache.delete(cacheKey)
}
}
while (repositoryMergeMetadataCache.size > MERGE_QUEUE_CACHE_MAX_ENTRIES) {
const oldestKey = repositoryMergeMetadataCache.keys().next().value
if (oldestKey === undefined) {
break
}
repositoryMergeMetadataCache.delete(oldestKey)
}
}
async function assertRateLimitBudget(bucket: RateLimitBucketKind): Promise<void> {
await getRateLimit()
const guard = rateLimitGuard(bucket)
@@ -1803,10 +1823,16 @@ function cacheRepositoryMergeMetadata(
value: GitHubRepositoryMergeMetadata,
ttlMs: number
): void {
const now = Date.now()
pruneRepositoryMergeMetadataCache(now)
// Why: merge metadata is keyed by user-controlled branch names. Keep the
// per-session cache bounded even if many short-lived branches are inspected.
repositoryMergeMetadataCache.delete(cacheKey)
repositoryMergeMetadataCache.set(cacheKey, {
value,
expiresAt: Date.now() + ttlMs
expiresAt: now + ttlMs
})
pruneRepositoryMergeMetadataCache(now)
}
async function detectRepositoryMergeMetadata(
@@ -1817,8 +1843,9 @@ async function detectRepositoryMergeMetadata(
const cacheKey = `${ownerRepo.owner.toLowerCase()}/${ownerRepo.repo.toLowerCase()}:${
branchName ?? '__repo__'
}`
pruneRepositoryMergeMetadataCache()
const cached = repositoryMergeMetadataCache.get(cacheKey)
if (cached && cached.expiresAt > Date.now()) {
if (cached) {
return cached.value
}
const guard = rateLimitGuard('graphql')