mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(rabbit): handle CR action acks + edited rate-limit comments; schedule via GH Action (#1425)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: CodeRabbit Retrigger
|
||||
# Periodically scans open PRs and posts `@coderabbitai review` on any whose
|
||||
# rate-limit window has elapsed. Uses the same GitHub App credentials as
|
||||
# release-production.yml so the comments are authored by the bot account.
|
||||
on:
|
||||
schedule:
|
||||
# Every 20 minutes. CodeRabbit Pro reviews 5 PRs/hr, so this gives
|
||||
# ~3 ticks per hour — enough to catch elapsed windows without thrashing.
|
||||
- cron: "*/20 * * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
max:
|
||||
description: "Max retriggers this run (CR Pro = 5/hr)"
|
||||
required: false
|
||||
default: "5"
|
||||
dry_run:
|
||||
description: "Print what would be done; post nothing"
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
concurrency:
|
||||
group: rabbit-retrigger
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
retrigger:
|
||||
name: Retrigger CodeRabbit
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.XGITHUB_APP_ID }}
|
||||
private_key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.x
|
||||
- name: Run rabbit
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
RABBIT_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -e
|
||||
ARGS=(run --max "${{ inputs.max || '5' }}")
|
||||
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||
ARGS+=(--dry-run)
|
||||
fi
|
||||
node scripts/rabbit/cli.mjs "${ARGS[@]}"
|
||||
+51
-20
@@ -14,8 +14,17 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const RATE_LIMIT_MARKER = "rate limited by coderabbit.ai";
|
||||
// CR's review summaries carry this marker; rate-limit comments also include it
|
||||
// alongside RATE_LIMIT_MARKER, so always check rate-limit first.
|
||||
const REVIEW_SUMMARY_MARKER = "summarize by coderabbit.ai";
|
||||
// "Actions performed" acks (e.g. response to `@coderabbitai review`) carry this
|
||||
// marker but no review content — they must NOT count as recovery.
|
||||
const ACTION_ACK_MARKER = "auto-generated reply by CodeRabbit";
|
||||
const RETRIGGER_BODY = "@coderabbitai review";
|
||||
const CR_LOGINS = new Set(["coderabbitai", "coderabbitai[bot]"]);
|
||||
// If we already posted `@coderabbitai review` but CR has only ack'd (or stayed
|
||||
// silent) for this long, assume CR was secondarily rate-limited and retry.
|
||||
const STALE_RETRIGGER_SEC = 10 * 60;
|
||||
|
||||
function gh(args, { input } = {}) {
|
||||
return execFileSync("gh", args, {
|
||||
@@ -143,25 +152,43 @@ function analyzePr(comments, graceSec) {
|
||||
|
||||
if (!latestRateLimit) return { status: "no-rate-limit" };
|
||||
|
||||
// If CR has already posted a non-rate-limit comment AFTER the rate limit,
|
||||
// it has effectively recovered — skip.
|
||||
if (
|
||||
latestCr !== latestRateLimit &&
|
||||
new Date(latestCr.created_at) > new Date(latestRateLimit.created_at) &&
|
||||
!latestCr.body.includes(RATE_LIMIT_MARKER)
|
||||
) {
|
||||
return { status: "review-since" };
|
||||
}
|
||||
|
||||
// If anyone (us or otherwise) has already posted `@coderabbitai review`
|
||||
// after the rate limit, don't double-trigger.
|
||||
const retriggerSince = comments.find(
|
||||
// CR has effectively recovered ONLY if a real review summary landed after
|
||||
// the rate-limit. The `summarize by coderabbit.ai` marker uniquely identifies
|
||||
// walkthrough/review comments; rate-limit comments include it too, so also
|
||||
// require absence of the rate-limit marker. "Actions performed" acks carry
|
||||
// the ACTION_ACK_MARKER instead and must not count as recovery.
|
||||
// Anchor "since" comparisons to whichever is later: the rate-limit comment's
|
||||
// creation or its last edit. CR edits the same comment to refresh the wait,
|
||||
// so a comment created before the latest edit is no longer evidence of
|
||||
// recovery.
|
||||
const limitSinceTs = new Date(
|
||||
latestRateLimit.updated_at || latestRateLimit.created_at,
|
||||
);
|
||||
const realReviewSince = crComments.find(
|
||||
(c) =>
|
||||
new Date(c.created_at) > new Date(latestRateLimit.created_at) &&
|
||||
new Date(c.created_at) > limitSinceTs &&
|
||||
c.body.includes(REVIEW_SUMMARY_MARKER) &&
|
||||
!c.body.includes(RATE_LIMIT_MARKER),
|
||||
);
|
||||
if (realReviewSince) return { status: "review-since" };
|
||||
|
||||
// If anyone has posted `@coderabbitai review` since the rate limit AND it's
|
||||
// recent, don't double-trigger. If it's stale and CR still hasn't posted a
|
||||
// real review, CR was likely silently rate-limited again — fall through and
|
||||
// retrigger.
|
||||
const retriggersSince = comments.filter(
|
||||
(c) =>
|
||||
new Date(c.created_at) > limitSinceTs &&
|
||||
c.body.trim().toLowerCase().startsWith(RETRIGGER_BODY),
|
||||
);
|
||||
if (retriggerSince) {
|
||||
return { status: "already-retriggered", at: retriggerSince.created_at };
|
||||
const lastRetrigger = retriggersSince[retriggersSince.length - 1];
|
||||
if (lastRetrigger) {
|
||||
const ageSec =
|
||||
(Date.now() - new Date(lastRetrigger.created_at).getTime()) / 1000;
|
||||
if (ageSec < STALE_RETRIGGER_SEC) {
|
||||
return { status: "already-retriggered", at: lastRetrigger.created_at };
|
||||
}
|
||||
// Stale retrigger with no real review since — fall through to retrigger.
|
||||
}
|
||||
|
||||
const waitSec = parseWaitSeconds(latestRateLimit.body);
|
||||
@@ -169,22 +196,26 @@ function analyzePr(comments, graceSec) {
|
||||
return { status: "ready", reason: "unparseable wait — assuming elapsed" };
|
||||
}
|
||||
|
||||
// CR edits the same rate-limit comment on each retry instead of posting a
|
||||
// fresh one — it rewrites the wait timer and bumps `updated_at`. Anchor the
|
||||
// expiry to the latest update, not the original post, otherwise stale waits
|
||||
// always look elapsed and we trigger straight into a closed window.
|
||||
const limitAnchor = latestRateLimit.updated_at || latestRateLimit.created_at;
|
||||
const expiresAt =
|
||||
new Date(latestRateLimit.created_at).getTime() +
|
||||
(waitSec + graceSec) * 1000;
|
||||
new Date(limitAnchor).getTime() + (waitSec + graceSec) * 1000;
|
||||
const now = Date.now();
|
||||
if (now < expiresAt) {
|
||||
return {
|
||||
status: "waiting",
|
||||
remainingSec: Math.ceil((expiresAt - now) / 1000),
|
||||
ratedLimitedAt: latestRateLimit.created_at,
|
||||
ratedLimitedAt: limitAnchor,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ready",
|
||||
waitSec,
|
||||
ratedLimitedAt: latestRateLimit.created_at,
|
||||
ratedLimitedAt: limitAnchor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user