mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
ci: add ratelimits handling for close-stale-prs.yml (#11578)
This commit is contained in:
parent
06d63ca54c
commit
1bd5dc5382
116
.github/workflows/close-stale-prs.yml
vendored
116
.github/workflows/close-stale-prs.yml
vendored
@ -18,6 +18,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
close-stale-prs:
|
close-stale-prs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Close inactive PRs
|
- name: Close inactive PRs
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
@ -25,6 +26,15 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
const DAYS_INACTIVE = 60
|
const DAYS_INACTIVE = 60
|
||||||
|
const MAX_RETRIES = 3
|
||||||
|
|
||||||
|
// Adaptive delay: fast for small batches, slower for large to respect
|
||||||
|
// GitHub's 80 content-generating requests/minute limit
|
||||||
|
const SMALL_BATCH_THRESHOLD = 10
|
||||||
|
const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 PRs)
|
||||||
|
const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 PRs) = ~30 ops/min, well under 80 limit
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
|
||||||
const { owner, repo } = context.repo
|
const { owner, repo } = context.repo
|
||||||
const dryRun = context.payload.inputs?.dryRun === "true"
|
const dryRun = context.payload.inputs?.dryRun === "true"
|
||||||
@ -32,6 +42,42 @@ jobs:
|
|||||||
core.info(`Dry run mode: ${dryRun}`)
|
core.info(`Dry run mode: ${dryRun}`)
|
||||||
core.info(`Cutoff date: ${cutoff.toISOString()}`)
|
core.info(`Cutoff date: ${cutoff.toISOString()}`)
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withRetry(fn, description = 'API call') {
|
||||||
|
let lastError
|
||||||
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await fn()
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
const isRateLimited = error.status === 403 &&
|
||||||
|
(error.message?.includes('rate limit') || error.message?.includes('secondary'))
|
||||||
|
|
||||||
|
if (!isRateLimited) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse retry-after header, default to 60 seconds
|
||||||
|
const retryAfter = error.response?.headers?.['retry-after']
|
||||||
|
? parseInt(error.response.headers['retry-after'])
|
||||||
|
: 60
|
||||||
|
|
||||||
|
// Exponential backoff: retryAfter * 2^attempt
|
||||||
|
const backoffMs = retryAfter * 1000 * Math.pow(2, attempt)
|
||||||
|
|
||||||
|
core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`)
|
||||||
|
|
||||||
|
await sleep(backoffMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`)
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
query($owner: String!, $repo: String!, $cursor: String) {
|
query($owner: String!, $repo: String!, $cursor: String) {
|
||||||
repository(owner: $owner, name: $repo) {
|
repository(owner: $owner, name: $repo) {
|
||||||
@ -73,17 +119,27 @@ jobs:
|
|||||||
const allPrs = []
|
const allPrs = []
|
||||||
let cursor = null
|
let cursor = null
|
||||||
let hasNextPage = true
|
let hasNextPage = true
|
||||||
|
let pageCount = 0
|
||||||
|
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
const result = await github.graphql(query, {
|
pageCount++
|
||||||
owner,
|
core.info(`Fetching page ${pageCount} of open PRs...`)
|
||||||
repo,
|
|
||||||
cursor,
|
const result = await withRetry(
|
||||||
})
|
() => github.graphql(query, { owner, repo, cursor }),
|
||||||
|
`GraphQL page ${pageCount}`
|
||||||
|
)
|
||||||
|
|
||||||
allPrs.push(...result.repository.pullRequests.nodes)
|
allPrs.push(...result.repository.pullRequests.nodes)
|
||||||
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
|
||||||
cursor = result.repository.pullRequests.pageInfo.endCursor
|
cursor = result.repository.pullRequests.pageInfo.endCursor
|
||||||
|
|
||||||
|
core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`)
|
||||||
|
|
||||||
|
// Delay between pagination requests (use small batch delay for reads)
|
||||||
|
if (hasNextPage) {
|
||||||
|
await sleep(SMALL_BATCH_DELAY_MS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info(`Found ${allPrs.length} open pull requests`)
|
core.info(`Found ${allPrs.length} open pull requests`)
|
||||||
@ -114,28 +170,66 @@ jobs:
|
|||||||
|
|
||||||
core.info(`Found ${stalePrs.length} stale pull requests`)
|
core.info(`Found ${stalePrs.length} stale pull requests`)
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Close stale PRs
|
||||||
|
// ============================================
|
||||||
|
const requestDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD
|
||||||
|
? LARGE_BATCH_DELAY_MS
|
||||||
|
: SMALL_BATCH_DELAY_MS
|
||||||
|
|
||||||
|
core.info(`Using ${requestDelayMs}ms delay between operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
|
||||||
|
|
||||||
|
let closedCount = 0
|
||||||
|
let skippedCount = 0
|
||||||
|
|
||||||
for (const pr of stalePrs) {
|
for (const pr of stalePrs) {
|
||||||
const issue_number = pr.number
|
const issue_number = pr.number
|
||||||
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author.login}: ${pr.title}`)
|
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
try {
|
||||||
|
// Add comment
|
||||||
|
await withRetry(
|
||||||
|
() => github.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number,
|
issue_number,
|
||||||
body: closeComment,
|
body: closeComment,
|
||||||
})
|
}),
|
||||||
|
`Comment on PR #${issue_number}`
|
||||||
|
)
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
// Close PR
|
||||||
|
await withRetry(
|
||||||
|
() => github.rest.pulls.update({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number: issue_number,
|
pull_number: issue_number,
|
||||||
state: "closed",
|
state: "closed",
|
||||||
})
|
}),
|
||||||
|
`Close PR #${issue_number}`
|
||||||
|
)
|
||||||
|
|
||||||
core.info(`Closed PR #${issue_number} from ${pr.author.login}: ${pr.title}`)
|
closedCount++
|
||||||
|
core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
|
||||||
|
|
||||||
|
// Delay before processing next PR
|
||||||
|
await sleep(requestDelayMs)
|
||||||
|
} catch (error) {
|
||||||
|
skippedCount++
|
||||||
|
core.error(`Failed to close PR #${issue_number}: ${error.message}`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
||||||
|
core.info(`\n========== Summary ==========`)
|
||||||
|
core.info(`Total open PRs found: ${allPrs.length}`)
|
||||||
|
core.info(`Stale PRs identified: ${stalePrs.length}`)
|
||||||
|
core.info(`PRs closed: ${closedCount}`)
|
||||||
|
core.info(`PRs skipped (errors): ${skippedCount}`)
|
||||||
|
core.info(`Elapsed time: ${elapsed}s`)
|
||||||
|
core.info(`=============================`)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user