Skip to content

coderabbit-rate-limit-retry #823

coderabbit-rate-limit-retry

coderabbit-rate-limit-retry #823

name: coderabbit-rate-limit-retry
on:
pull_request_target:
types: [opened, synchronize, reopened]
schedule:
- cron: '*/20 * * * *'
workflow_dispatch:
permissions:
checks: write
contents: read
pull-requests: write
issues: write
jobs:
retrigger:
name: retrigger-coderabbit-on-rate-limit
runs-on: ubuntu-latest
steps:
- name: Re-request CodeRabbit when backlog is high and check is stale
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const STALE_MINUTES = 20;
const BYPASS_LABEL = "ci:coderabbit-bypass";
const GATE_CHECK_NAME = "CodeRabbit Gate";
const MARKER = "<!-- codex:coderabbit-rate-limit-retry -->";
const nowMs = Date.now();
async function listOpenPRs() {
const all = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
return all;
}
async function getCodeRabbitState(prNumber) {
const checks = await github.graphql(
`query($owner:String!,$repo:String!,$number:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$number){
commits(last:1){
nodes{
commit{
statusCheckRollup{
contexts(first:50){
nodes{
__typename
... on CheckRun {
name
conclusion
status
completedAt
}
... on StatusContext {
context
state
createdAt
}
}
}
}
}
}
}
}
}
}`,
{ owner, repo, number: prNumber },
);
const nodes = checks.repository.pullRequest.commits.nodes[0]?.commit?.statusCheckRollup?.contexts?.nodes || [];
for (const n of nodes) {
if (n.__typename === "CheckRun" && n.name === "CodeRabbit") {
return {
state: (n.conclusion || n.status || "UNKNOWN").toUpperCase(),
at: n.completedAt ? new Date(n.completedAt).getTime() : nowMs,
};
}
if (n.__typename === "StatusContext" && n.context === "CodeRabbit") {
return {
state: (n.state || "UNKNOWN").toUpperCase(),
at: n.createdAt ? new Date(n.createdAt).getTime() : nowMs,
};
}
}
return { state: "MISSING", at: nowMs };
}
async function hasRecentRetryComment(prNumber) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const latest = comments
.filter((c) => c.user?.login === "github-actions[bot]" && c.body?.includes(MARKER))
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
if (!latest) return false;
const ageMin = (nowMs - new Date(latest.created_at).getTime()) / 60000;
return ageMin < STALE_MINUTES;
}
async function ensureBypassLabelExists() {
try {
await github.rest.issues.getLabel({
owner,
repo,
name: BYPASS_LABEL,
});
} catch (error) {
if (error.status !== 404) throw error;
await github.rest.issues.createLabel({
owner,
repo,
name: BYPASS_LABEL,
color: "B60205",
description: "Temporary bypass for CodeRabbit rate-limit under high PR backlog.",
});
}
}
async function hasLabel(prNumber, name) {
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
return labels.some((l) => l.name === name);
}
async function setBypassLabel(prNumber, enable) {
const present = await hasLabel(prNumber, BYPASS_LABEL);
if (enable && !present) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [BYPASS_LABEL],
});
core.notice(`PR #${prNumber}: applied label '${BYPASS_LABEL}'.`);
}
if (!enable && present) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: BYPASS_LABEL,
});
core.notice(`PR #${prNumber}: removed label '${BYPASS_LABEL}'.`);
}
}
async function publishGate(pr, pass, summary) {
await github.rest.checks.create({
owner,
repo,
name: GATE_CHECK_NAME,
head_sha: pr.head.sha,
status: "completed",
conclusion: pass ? "success" : "failure",
output: {
title: pass ? "CodeRabbit gate passed" : "CodeRabbit gate blocked",
summary,
},
});
}
async function processPR(pr) {
const state = await getCodeRabbitState(pr.number);
const ageMin = (nowMs - state.at) / 60000;
const stateOk = state.state === "SUCCESS" || state.state === "NEUTRAL";
const stale = ageMin >= STALE_MINUTES;
const bypassEligible = stale && !stateOk;
await setBypassLabel(pr.number, bypassEligible);
if (bypassEligible && !(await hasRecentRetryComment(pr.number))) {
const body = [
MARKER,
"@coderabbitai full review",
"",
`Automated retrigger: CodeRabbit state=${state.state}, age=${ageMin.toFixed(1)}m (stale after ${STALE_MINUTES}m).`,
].join("\n");
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body,
});
core.notice(`PR #${pr.number}: posted CodeRabbit retrigger comment.`);
}
const gatePass = stateOk || bypassEligible;
const summary = [
`CodeRabbit state: ${state.state}`,
`Age minutes: ${ageMin.toFixed(1)}`,
`Stale threshold: ${STALE_MINUTES}m`,
`Bypass eligible: ${bypassEligible}`,
].join("\n");
await publishGate(pr, gatePass, summary);
}
const openPRs = await listOpenPRs();
core.info(`Open PR count: ${openPRs.length}`);
await ensureBypassLabelExists();
const targetPRs = context.eventName === "pull_request_target"
? openPRs.filter((p) => p.number === context.payload.pull_request.number)
: openPRs;
for (const pr of targetPRs) {
await processPR(pr);
}