Repository navigation
329 lines (305 loc) · 16 KB
/
Copy pathcoderabbit-gate.yml
File metadata and controls
329 lines (305 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
name: CodeRabbit Gate
# Asks CodeRabbit for a review once CI has passed, and only for pull requests in
# scope. Nothing is reviewed automatically, so no review is spent on a revision
# that is failing or on a pull request nobody wants reviewed.
#
# The consuming repository sets `reviews.auto_review.enabled: false` in its
# `.coderabbit.yml`. That turns automatic reviews off entirely; this workflow then
# posts `@coderabbitai review`, the command CodeRabbit documents as working
# regardless of auto-review settings — it says so itself when it skips a pull
# request: "To trigger a single review, invoke the `@coderabbitai review` command."
#
# The command is the trigger. The `review-ready` label is applied alongside it as
# a visual marker only, best-effort: a repository that has not declared the label
# still works, it just does not get the badge. Do NOT put that label back into
# `reviews.auto_review.labels` — it would become a second, competing trigger.
#
# Scope is two independent dimensions, OR'd together:
#
# review_base_branches exact base branch names — "review anything landing here"
# review_head_patterns globs on the head branch — "review this kind of branch
# wherever it lands", for code that reached no other
# review on its way (hotfix/*, for example)
#
# Setting both to an empty string disables reviews without editing any workflow.
#
# `ignore_authors` is checked first and is a veto, not a third dimension — it
# cannot be OR'd with the two above or it would lose to any positive match. It
# defaults to dependabot[bot], whose bumps land on the same base branches as
# human work and would otherwise spend the hourly review allowance on diffs that
# are a version number.
#
# NOTE for callers: the caller's own `pull_request` trigger bounds this. A base
# branch listed here but absent from `on.pull_request.branches` never sees the
# workflow run at all, so the command is never posted. The effective scope is the
# intersection of the two.
on:
workflow_call:
inputs:
pr_number:
description: Pull request to act on
required: true
type: number
checks_passed:
description: Whether every required check succeeded — pass the caller's `!contains(needs.*.result, ...)` verdict.
required: false
type: boolean
default: false
head_sha:
description: Commit the caller's verdict refers to. Also the idempotency key — one review request per commit. When it no longer matches the pull request head, the request is skipped, so a stale run cannot ask for a review of a commit it never validated.
required: true
type: string
review_base_branches:
description: Comma-separated exact base branch names whose pull requests are reviewed. Empty removes this dimension.
required: false
type: string
default: develop
review_head_patterns:
description: Comma-separated globs matched against the head branch. A match is reviewed regardless of base. Empty removes this dimension.
required: false
type: string
default: hotfix/*
ignore_authors:
description: Comma-separated logins whose pull requests are never reviewed, checked before scope as a veto. Write the login however you know it — `dependabot`, `dependabot[bot]` and `app/dependabot` are the same entry, since REST, GraphQL and the `gh` CLI each spell a bot differently. Empty reviews every author.
required: false
type: string
default: dependabot[bot]
label:
description: Visual marker applied when the review is requested. Cosmetic — failure to apply it never fails the job. Empty disables it.
required: false
type: string
default: review-ready
dry_run:
description: Log the decision without commenting or labelling
required: false
type: boolean
default: false
runner_type:
description: Runner label for the job
required: false
type: string
default: blacksmith-4vcpu-ubuntu-2404
secrets:
MANAGE_TOKEN:
description: 'Token with `pull-requests: write` on the target repository. Falls back to `github.token` when absent, which is enough for same-repository pull requests.'
required: false
# No `workflow_dispatch`: the repository convention is that reusable workflows do
# not offer one — manual triggering belongs in a `self-*` entrypoint, where the
# inputs can be constrained. There is none for this gate because the manual
# equivalent is simpler: comment `@coderabbitai review` on the pull request.
permissions:
contents: read
jobs:
gate:
name: Request CodeRabbit Review
runs-on: ${{ vars.GENERAL_RUNNERS || inputs.runner_type }}
permissions:
pull-requests: write
steps:
- name: Request review when the pull request is green and in scope
env:
GH_TOKEN: ${{ secrets.MANAGE_TOKEN || github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr_number }}
CHECKS_PASSED: ${{ inputs.checks_passed }}
HEAD_SHA: ${{ inputs.head_sha }}
REVIEW_BASE_BRANCHES: ${{ inputs.review_base_branches }}
REVIEW_HEAD_PATTERNS: ${{ inputs.review_head_patterns }}
IGNORE_AUTHORS: ${{ inputs.ignore_authors }}
LABEL: ${{ inputs.label }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
# Read the live pull request rather than trusting event payload values,
# which are frozen at dispatch. A run started before the pull request
# was retargeted would otherwise decide against a branch that no longer
# applies.
read -r base head live_sha author < <(
gh pr view "$PR_NUMBER" --repo "$REPO" \
--json baseRefName,headRefName,headRefOid,author \
--jq '"\(.baseRefName) \(.headRefName) \(.headRefOid) \(.author.login)"'
)
# Verbose only under dry_run, per the repository's dry_run contract. A
# real run emits a single `::notice::` with the decision — this job's
# only observable product is *why* a review was or was not requested.
if [ "$DRY_RUN" = "true" ]; then
echo "::notice::DRY RUN — no comment and no label will be posted"
echo " pull request : #${PR_NUMBER}"
echo " head → base : ${head} → ${base}"
echo " author : ${author}"
echo " checks passed : ${CHECKS_PASSED}"
echo " verdict commit : ${HEAD_SHA}"
echo " live head : ${live_sha}"
echo " scope bases : [${REVIEW_BASE_BRANCHES}]"
echo " scope head globs : [${REVIEW_HEAD_PATTERNS}]"
echo " ignored authors : [${IGNORE_AUTHORS}]"
fi
# `checks_passed` describes one specific commit. Without concurrency
# control an older run can finish after a newer push, and would then
# ask for a review of a head whose checks it never saw.
if [ "$HEAD_SHA" != "$live_sha" ]; then
echo "::notice::Stale run — the verdict covers ${HEAD_SHA:0:7} but PR #${PR_NUMBER} now heads at ${live_sha:0:7}; not requesting a review"
exit 0
fi
# Authors vetoed outright, before scope is even considered. Dependabot
# opens its bumps against the same base branches as human work, so the
# base dimension cannot tell the two apart — and each bump would spend
# one of the hourly review allowance on a diff whose entire content is
# a version number.
#
# A veto rather than a third scope dimension: the dimensions are OR'd,
# so an exclusion expressed as one would lose to any positive match.
# That is the "release PRs reviewed anyway" failure in
# docs/coderabbit-gate.md — a negative match cannot veto a positive
# trigger when both feed the same OR.
#
# Three spellings of the same bot, all of which must match. REST says
# `dependabot[bot]`, GraphQL says `dependabot`, and `gh pr view --json
# author` — the call right above — says `app/dependabot`, because the
# CLI prefixes bot logins with `app/`. Verified on PR #786: GraphQL
# returned `{"is_bot":true,"login":"app/dependabot"}` where REST
# returned `dependabot[bot]`.
#
# So strip the affixes from both sides rather than comparing raw. A
# caller writes whichever spelling it last saw, and the one this job
# reads is the one no caller would think to write.
normalise_login() {
local login="${1,,}"
login="${login#app/}"
printf '%s' "${login%\[bot\]}"
}
author_key=$(normalise_login "$author")
while IFS= read -r entry; do
[ -z "$entry" ] && continue
if [ "$author_key" = "$(normalise_login "$entry")" ]; then
echo "::notice::PR #${PR_NUMBER} was opened by ${author}, an ignored author — not requesting a review"
exit 0
fi
done < <(tr ',' '\n' <<< "$IGNORE_AUTHORS" | xargs -n1 2>/dev/null)
matched=""
while IFS= read -r entry; do
[ -z "$entry" ] && continue
if [ "$base" = "$entry" ]; then
matched="base '${entry}'"
break
fi
done < <(tr ',' '\n' <<< "$REVIEW_BASE_BRANCHES" | xargs -n1 2>/dev/null)
if [ -z "$matched" ]; then
while IFS= read -r entry; do
[ -z "$entry" ] && continue
# shellcheck disable=SC2254
case "$head" in
$entry) matched="head pattern '${entry}'"; break ;;
esac
done < <(tr ',' '\n' <<< "$REVIEW_HEAD_PATTERNS" | xargs -n1 2>/dev/null)
fi
if [ -z "$matched" ]; then
echo "::notice::PR #${PR_NUMBER} (${head} → ${base}) is out of the review scope — not requesting a review"
exit 0
fi
if [ "$CHECKS_PASSED" != "true" ]; then
echo "::notice::PR #${PR_NUMBER} is in scope via ${matched}, but a required check did not pass — not requesting a review"
exit 0
fi
# Idempotency. Unlike a label, a comment is not a no-op when repeated:
# every `@coderabbitai review` spends another review. Re-runs and
# concurrent runs on the same commit are both reachable, so the marker
# below — carrying the commit — is what makes "one review per revision"
# true rather than merely intended.
marker="<!-- coderabbit-gate: ${live_sha} -->"
if gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
--jq '.[].body' | grep -qF "$marker"; then
echo "::notice::A review was already requested for ${live_sha:0:7} on PR #${PR_NUMBER} — not asking again"
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo " decision : would request a review (in scope via ${matched}, checks passed)"
exit 0
fi
# ----------------- Collapse the superseded noise -----------------
# Each revision adds a request from this workflow, and CodeRabbit echoes
# every one of them with an "Action performed / Review finished" reply, so
# a pull request revised a few times buries its human conversation under
# pairs of bot comments.
#
# Both sides are folded away with GraphQL `minimizeComment` before the new
# request goes out, so the fresh one needs no exclusion. Minimising keeps
# the history: the comments remain, one click from being expanded.
#
# Listing goes through GraphQL rather than REST because only GraphQL
# exposes `isMinimized`. The REST body is unchanged by minimisation — it
# carries no marker — so a REST-based filter cannot tell an already-folded
# comment from a fresh one, and every run would re-minimise everything.
#
# Findings are structurally out of reach: this reads `pullRequest.comments`,
# which is issue comments only. Reviews and review comments — where
# findings live — are separate connections. Do not "improve" this by
# reaching into `reviews`: on PR #717 the review carrying CodeRabbit's
# permissions warning also carries 13 actionable findings, and the two are
# the same object.
#
# There is exactly one safe way to fold a review, and it is not here:
# coderabbit-collapse.yml folds one only when every inline thread that
# review opened is resolved, so the findings have been dealt with by
# definition. It refuses to touch a review with no thread at all —
# precisely the case this note is about. That check needs the review →
# threads edge, which this job never reads, and a trigger tied to thread
# activity rather than to a push, so the two stay separate.
#
# Kept non-fatal throughout — losing a review because the tidying failed
# would be a bad trade.
collapse() {
local node_id="$1" classifier="$2"
# shellcheck disable=SC2016 # $id and $c are GraphQL variables, not shell ones
gh api graphql \
-f query='mutation($id:ID!,$c:ReportedContentClassifiers!){minimizeComment(input:{subjectId:$id,classifier:$c}){minimizedComment{isMinimized}}}' \
-F id="$node_id" -F c="$classifier" >/dev/null 2>&1
}
owner=${REPO%%/*}
name=${REPO##*/}
collapsed=0
# One listing, both categories, already-minimised excluded. Each line is
# "<node_id> <classifier>".
while read -r node_id classifier; do
[ -z "$node_id" ] && continue
if collapse "$node_id" "$classifier"; then
collapsed=$((collapsed + 1))
fi
done < <(
# shellcheck disable=SC2016 # GraphQL variables
gh api graphql --paginate \
-f query='
query($owner:String!,$name:String!,$pr:Int!,$endCursor:String){
repository(owner:$owner,name:$name){
pullRequest(number:$pr){
comments(first:100,after:$endCursor){
pageInfo{hasNextPage endCursor}
nodes{id isMinimized body}
}
}
}
}' \
-F owner="$owner" -F name="$name" -F pr="$PR_NUMBER" \
--jq '
.data.repository.pullRequest.comments.nodes[]
| select(.isMinimized == false)
| if (.body | contains("coderabbit-gate:")) and (.body | contains("@coderabbitai review"))
then "\(.id) RESOLVED"
elif (.body | contains("CodeRabbit review command invocation:"))
then "\(.id) OUTDATED"
else empty
end' 2>/dev/null || true
)
if [ "$DRY_RUN" = "true" ] && [ "$collapsed" -gt 0 ]; then
echo " collapsed : ${collapsed} superseded bot comment(s)"
fi
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$(printf '@coderabbitai review\n\n%s' "$marker")"
echo "::notice::Requested a CodeRabbit review on PR #${PR_NUMBER} — in scope via ${matched} and every required check passed"
# Cosmetic only, and deliberately last: a repository that never declared
# the label still gets its review. Never let this fail the job.
if [ -n "$LABEL" ]; then
if gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "$LABEL" 2>/dev/null; then
echo "🏷️ Marked with '${LABEL}'"
else
echo "::notice::Label '${LABEL}' not applied — it is not declared in ${REPO}. Cosmetic only; the review was requested regardless."
fi
fi