Repository navigation
1220 lines (1137 loc) · 69.1 KB
/
Copy pathflamingo-code-review.yml
File metadata and controls
1220 lines (1137 loc) · 69.1 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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Generated by the Flamingo hub — do not edit by hand.
# Per-repo settings live in the hub admin (/admin/code-review); this file is
# byte-identical across every reviewed repository AND every deployment, which
# is what makes drift detectable by comparison. The hub's address arrives in
# the dispatch payload (Code Documentation style) or, for pull_request runs, the
# org Actions variable FLAMINGO_HUB_BASE_URL.
# Named like its sibling pipeline ('🦩 Flamingo Code Documentation') so both
# read as one product family in the Actions sidebar.
name: 🦩 Flamingo Code Review
on:
# Push trigger - registers workflow with GitHub Actions (required for
# workflow_dispatch API). Only fires when this file itself changes; the job
# skips it — same pattern as the Code Documentation workflow.
push:
paths:
- '.github/workflows/flamingo-code-review.yml'
# REVIEWED ON REQUEST (2026-10-04). Nothing is reviewed automatically when a
# pull request ENTERS review ('opened' / 'ready_for_review' / 'reopened'): a
# pull request rarely holds everything a review needs at the moment it is
# opened, so that first pass was mostly spent on a change that was not
# finished. Those three events now run the small 'invite' job below, which
# posts the summary comment as an INVITATION; its checkbox (or the comment
# command, or the label) starts the first review when the author is ready.
# A draft gets its invitation on ready_for_review.
#
# 'synchronize' (per-push) is not a review moment either. Reviewing every
# commit on an open PR is the largest avoidable cost in an AI review
# pipeline, and it trains authors to tune the bot out.
#
# 'labeled' is the on-demand review from the sidebar: adding the
# flamingo-review label asks for a pass, the first one or another. A SEPARATE job removes it the moment the event
# arrives, so adding it again asks again, and it can never strand itself on a
# pull request the review gate declines. Every other label name is dropped by
# the job's if — a skipped run, zero billable minutes.
#
# A 'synchronize' carrying that label is treated as the same request. GitHub
# runs the workflow from the pull request's HEAD, so a label applied to a
# branch whose head predates this file fires an event no workflow was there
# to handle: the label lands, nothing runs, nothing consumes it, and because
# it is now already present, adding it again fires NOTHING AT ALL. Honouring
# it on the next push is what breaks that stall — the request gets served
# once and consumed, instead of sitting on the pull request looking applied.
#
# 'synchronize' (every push) is otherwise here ONLY to serve the
# flamingo-review-always label, and the job's if drops it on every pull
# request that does not carry it. That is the per-PR escalation: subscribe the
# risky refactor to continuous review, leave everything else to be reviewed
# when asked. A skipped push run costs no billable minutes.
#
# Every repeat pass diffs only the delta since the last reviewed head — see
# the incremental anchor in code-review-review.mjs.
pull_request:
types: [opened, ready_for_review, reopened, labeled, synchronize]
# THE PRIMARY TRIGGER (2026-09-08). A new top-level pull-request
# comment beginning with '@flamingo-review' asks for a pass;
# '@flamingo-review full' additionally overrules the incremental anchor
# and re-reads the whole cumulative diff.
#
# This exists because the label could not TEACH ITSELF. The summary comment
# is the only surface an author reliably sees, a comment cannot link to or
# explain a sidebar label, and so the once-per-PR default read as "the bot
# only reviewed my first commit". A command can be printed in the very
# comment that prompts the question — see the footer in code-review-post.mjs.
# It is also where the market landed: CodeRabbit ('@coderabbitai review' /
# 'full review') and Cursor Bugbot ('cursor review' / 'bugbot run') both take
# a new top-level comment, and both split incremental from full.
#
# 'edited' is the CHECKBOX lane — the closest thing a GitHub comment has to a
# button, and the same mechanism CodeRabbit uses for its clickable actions.
# Checking a task-list box in the summary comment edits that comment, which
# fires issue_comment.edited; the boxes are the two commands, clickable, and
# the invitation's single box is the first review.
#
# Its costs are real and accepted, not overlooked: 'edited' fires on EVERY
# comment edit in the repository, so an unrelated typo fix evaluates this
# workflow and skips (zero billable minutes, but a skipped run in the Actions
# list — the same trade already made for push and synchronize). And a
# checkbox is STATE, so resolve_command unchecks it after consuming, which is
# what lets it be clicked again.
#
# WRITE ACCESS IS ENFORCED BY GITHUB HERE, not by us: only a user with write
# permission can toggle a task list in someone else's comment, so the
# author_association gate that gutters the 'created' lane has no equivalent —
# and needs none. What IS checked is that the edited comment is the bot's own
# summary and that a human did the editing, so the uncheck below cannot
# re-trigger the workflow it just served.
#
# issue_comment fires only for top-level comments; a reply inside a review
# thread is pull_request_review_comment and is NOT a trigger here (the same
# boundary Bugbot documents). Review-thread replies stay conversation.
#
# SECURITY, and the reason for the resolve_command job below: unlike a
# pull_request run, an issue_comment run executes in the BASE repository with
# a WRITE token and full secrets. The event carries no head SHA and no head
# repository, so 'is this a fork?' cannot be answered in a job 'if' — and
# checking out refs/pull/N/head without answering it would run this workflow
# over fork-authored code holding that token. resolve_command answers it via
# the API BEFORE anything is checked out, and forks are refused there.
issue_comment:
types: [created, edited]
repository_dispatch:
types: [flamingo-code-review]
# Lets the hub target a SETUP BRANCH before the install PR merges — the same
# test-before-merge flow the doc pipeline uses. repository_dispatch only ever
# fires on the default branch.
workflow_dispatch:
# ═══════════════════════════════════════════════════════════════════════════
# GENERATED FROM SINGLE SOURCE OF TRUTH: CODE_REVIEW_PARAMS (this module)
# This section is auto-generated when creating workflow PRs
# ═══════════════════════════════════════════════════════════════════════════
inputs:
run_id:
description: 'Hub run-record id binding this execution to a code_review_runs row. Empty for pull_request runs — the hub creates the row from the callback.'
required: false
default: ''
run_token:
description: 'Per-run token minted by the hub at dispatch. Binds the callback to THIS run — the org-wide secret alone would let any repo report against another repo’s run.'
required: false
default: ''
mode:
description: 'Review mode: pr (diff-scoped), sweep (whole-repo scanner) or mine (rule mining over the full checkout).'
required: false
default: 'sweep'
hub_base_url:
description: 'Absolute hub origin. Dispatches carry it Code Documentation style; pull_request runs fall back to the org/repo Actions variable FLAMINGO_HUB_BASE_URL (shipped by Sync Secrets).'
required: false
default: ''
review_budget_chars:
description: 'Sweep corpus budget in characters for this run (review_config.sweep_budget_chars). Empty = UNCAPPED — the whole eligible corpus is reviewed in 80000-char batches. The local twin defaults to 80000 instead; set the per-repo value to cap CI spend.'
required: false
default: ''
# ═══════════════════════════════════════════════════════════════════════════
# END GENERATED SECTION
# ═══════════════════════════════════════════════════════════════════════════
# GENERATED FROM SINGLE SOURCE OF TRUTH: CODE_REVIEW_PARAMS (this module)
# The Code Documentation parameter chain for EVERY dispatch param: payload
# first, workflow_dispatch input second, then the param's runtime fallback.
# Workflow-level, so every dispatch var reaches every step and every
# downloaded script. SECURITY: only NON-SENSITIVE values live here — secrets
# are passed per-step, same as the doc workflow.
env:
RUN_ID: ${{ github.event.client_payload.run_id || github.event.inputs.run_id || '' }}
MODE: ${{ github.event.client_payload.mode || github.event.inputs.mode || ((github.event_name == 'pull_request' || github.event_name == 'issue_comment') && 'pr' || 'sweep') }}
HUB_BASE_URL: ${{ github.event.client_payload.hub_base_url || github.event.inputs.hub_base_url || vars.FLAMINGO_HUB_BASE_URL }}
REVIEW_BUDGET_CHARS: ${{ github.event.client_payload.review_budget_chars || github.event.inputs.review_budget_chars || '' }}
# Concurrency sits on the REVIEW JOB (below), not the workflow: a
# workflow-level group is joined when the RUN is queued, before any job if:,
# so a human replying to the "Running…" placeholder (issue_comment.created,
# same PR number) or any label event cancelled the in-flight review and then
# skipped every job — nothing re-reviewed. A skipped job never joins a
# job-level group.
permissions:
contents: read
pull-requests: write
checks: write
jobs:
# Consuming the label is its OWN job, gated on nothing but "that label was
# added". Living inside the review job meant a label applied to a pull request the
# review gate declines — a bot PR, a fork — was never consumed: the label sat
# there permanently, and because it was already present, adding it again did
# nothing. A silent dead end with no feedback. Split out, the request is
# always consumed, whether or not a review follows it.
#
# It does NOT remove flamingo-review-always: that label is durable state
# ("keep reviewing this PR"), not a one-shot request.
consume-review-request:
# The fork clause is not a token guard — a fork pull_request run gets a
# READ-ONLY token whatever this file declares, so the DELETE below could
# not succeed there even without it. It is here because a job that
# provably cannot do its work should not start: on a fork the label
# strands either way, and burning a runner to fail silently only hides
# that. Same clause, same reason, as the review job below.
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
((github.event.action == 'labeled' && github.event.label.name == 'flamingo-review') ||
(github.event.action == 'synchronize' &&
contains(github.event.pull_request.labels.*.name, 'flamingo-review')))
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Consume the on-demand review label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO_FULL: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Best-effort: losing this race must never fail anything.
run: |
CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG"
trap 'rm -f "$CURL_CFG"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$GITHUB_TOKEN" > "$CURL_CFG"
curl -sS --max-time 30 -K "$CURL_CFG" -X DELETE \
"${GITHUB_API_URL:-https://api.github.com}/repos/$REPO_FULL/issues/$PR_NUMBER/labels/flamingo-review" > /dev/null || true
# Resolves a COMMENT COMMAND into the pull-request context the review job
# needs, and refuses the cases the job 'if' provably cannot judge.
#
# It exists because an issue_comment event is context-poor by design: it
# carries the comment and the ISSUE, never the pull request. No head SHA, no
# head repository, no draft flag, no author type. Everything the review job's
# guard reads from github.event.pull_request.* is simply absent, and the two
# facts that gate SAFETY — is this a fork, is the author a bot — can only be
# answered by asking the API. A job 'if' cannot make an API call, so this job
# is the 'if' that could not be written as one.
#
# Ordering is the point: nothing is checked out until after the fork answer.
# An issue_comment run holds a WRITE token, so checking out refs/pull/N/head
# first and asking afterwards would have already run this workflow over
# fork-authored code with that token in scope.
#
# The cheap, event-local half of the gate stays in the 'if' below (a
# command-shaped comment, on an open pull request, from someone with write
# access) so an outsider's comment never starts a runner at all.
#
# 'github.event.issue.pull_request' is tested for TRUTHINESS, never against
# null. Comparing an object to null in an Actions expression coerces both to
# numbers, the object becomes NaN, and every NaN comparison is false — so the
# null form would have refused every real pull request while looking correct.
#
# Underscored job id ON PURPOSE: 'needs.<id>' is a context path, and a hyphen
# there parses as subtraction. A hyphenated id would need needs['...'] at
# every one of the reads below, and the first person to add a plain one would
# get a silently-empty value rather than an error.
resolve_command:
if: >-
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.issue.state == 'open' &&
((github.event.action == 'created' &&
startsWith(github.event.comment.body, '@flamingo-review') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event.action == 'edited' &&
github.event.comment.user.type == 'Bot' &&
github.event.sender.type != 'Bot' &&
(contains(github.event.comment.body, '[x] Review the new commits') ||
contains(github.event.comment.body, '[x] Review the whole diff again') ||
contains(github.event.comment.body, '[x] Review this pull request'))))
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
# The review job reads ONLY 'proceed'. Every refusal is a false here plus
# an explanation posted to the pull request — never a red run, because a
# command this pipeline declines to serve is not a broken pipeline, and a
# failed check on someone's PR for asking a question is noise.
proceed: ${{ steps.resolve.outputs.proceed }}
pr_number: ${{ steps.resolve.outputs.pr_number }}
head_sha: ${{ steps.resolve.outputs.head_sha }}
base_ref: ${{ steps.resolve.outputs.base_ref }}
full: ${{ steps.resolve.outputs.full }}
steps:
- name: Resolve the pull request behind the comment
id: resolve
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO_FULL: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
# 'created' (a typed command) vs 'edited' (a checked box) — the two
# lanes read the request from different places in the same body.
EVENT_ACTION: ${{ github.event.action }}
# Read as DATA, never interpolated into the script body: a comment is
# attacker-authored text, and ${{ }}-ing it into 'run:' is the
# canonical Actions script-injection sink. The full-pass test below is
# a shell string comparison against this variable.
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
set -euo pipefail
CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG"
trap 'rm -f "$CURL_CFG"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$GITHUB_TOKEN" > "$CURL_CFG"
# 'proceed' is written ONCE, at the end, and only on success. Every
# other exit — a refusal, or the script dying under set -e — leaves it
# unset, which the review job reads as an empty string and declines.
# Fail-closed by construction rather than by remembering to write a
# false on every path out.
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
# One acknowledgement, immediately: a command that produces no visible
# effect for the ~40s before the review job posts anything is
# indistinguishable from one that was never picked up, and the second
# thing a human does about that is ask again.
curl -sS --max-time 30 -K "$CURL_CFG" -X POST \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL:-https://api.github.com}/repos/$REPO_FULL/issues/comments/$COMMENT_ID/reactions" \
-d '{"content":"eyes"}' > /dev/null || true
PR_JSON=$(curl -sS --max-time 30 -K "$CURL_CFG" \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL:-https://api.github.com}/repos/$REPO_FULL/pulls/$PR_NUMBER") || PR_JSON=""
# Each read tolerates a body that is not JSON at all (an HTML error
# page from a proxy, a truncated response). Without the '|| true' set
# -e would kill the script mid-parse, and a script that dies here
# emits no 'proceed' AND no explanation — the command would look
# ignored, which is the exact failure this whole change exists to
# remove. An empty HEAD_SHA falls into the refusal below instead.
HEAD_SHA=$(printf '%s' "$PR_JSON" | jq -r '.head.sha // empty' 2>/dev/null || true)
HEAD_REPO=$(printf '%s' "$PR_JSON" | jq -r '.head.repo.full_name // empty' 2>/dev/null || true)
# The BASE the review must diff against. An issue_comment carries no
# GITHUB_BASE_REF (that variable exists only on pull_request events),
# and the reviewer's remaining fallbacks all need origin/HEAD or an
# authenticated 'git remote show' — neither of which a runner has. So
# every commanded review died as 'pr_base_unresolvable' while the
# answer was sitting in the pull request payload we already fetched.
BASE_REF=$(printf '%s' "$PR_JSON" | jq -r '.base.ref // empty' 2>/dev/null || true)
AUTHOR_TYPE=$(printf '%s' "$PR_JSON" | jq -r '.user.type // empty' 2>/dev/null || true)
refuse() {
# Say why, in the thread that asked. A silent no-op here is the
# exact failure mode the label had.
printf '%s' "$1" | jq -Rs '{body: .}' | \
curl -sS --max-time 30 -K "$CURL_CFG" -X POST \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL:-https://api.github.com}/repos/$REPO_FULL/issues/$PR_NUMBER/comments" \
-d @- > /dev/null || true
exit 0
}
if [ -z "$HEAD_SHA" ]; then
refuse "🦩 **Review not started.** This pull request could not be read from the GitHub API. Ask again in a moment."
fi
# The SAME two exclusions the review job applies to every other
# event, restated here because the job 'if' had nothing to read them
# from. A fork additionally cannot be waived by any command: this run
# holds a write token.
if [ "$HEAD_REPO" != "$REPO_FULL" ]; then
refuse "🦩 **Review not started.** This pull request comes from a fork, and a fork cannot be reviewed from a comment. Start it from the Flamingo hub (Code Review) instead."
fi
if [ "$AUTHOR_TYPE" = "Bot" ]; then
refuse "🦩 **Review not started.** This pull request was opened by a bot, and a bot's pull request cannot be reviewed from a comment. Start it from the Flamingo hub (Code Review) instead."
fi
# DRAFTS ARE ALLOWED — same rule the on-demand label already follows:
# an explicit request means it whether or not the PR is finished.
# Which of the two passes was asked for, in whichever lane asked.
# A TYPED command is a prefix test; a CHECKED BOX is a substring test,
# because the box sits inside a whole rendered comment.
FULL=false
case "$EVENT_ACTION" in
edited)
case "$COMMENT_BODY" in
*"[x] Review the whole diff again"*) FULL=true ;;
esac
;;
*)
case "$COMMENT_BODY" in
'@flamingo-review full'*) FULL=true ;;
esac
;;
esac
# RE-ARM the checkbox by unchecking it. Without this the box stays
# checked until the next review overwrites the whole comment, so it
# reads as "already requested" and cannot be clicked again — and on a
# refused request it would stay checked forever, which is exactly the
# stranded-state failure the on-demand label was split into its own
# job to avoid.
#
# Best-effort: a review that ran is worth more than a tidy checkbox,
# and this edit is made by the workflow token, whose events never
# trigger workflows — so it cannot loop back into this job.
if [ "$EVENT_ACTION" = "edited" ]; then
printf '%s' "$COMMENT_BODY" \
| sed -e 's/\[x\] Review the new commits/[ ] Review the new commits/' -e 's/\[x\] Review the whole diff again/[ ] Review the whole diff again/' -e 's/\[x\] Review this pull request/[ ] Review this pull request/' \
| jq -Rs '{body: .}' \
| curl -sS --max-time 30 -K "$CURL_CFG" -X PATCH \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL:-https://api.github.com}/repos/$REPO_FULL/issues/comments/$COMMENT_ID" \
-d @- > /dev/null || true
fi
{
echo "proceed=true"
echo "head_sha=$HEAD_SHA"
echo "base_ref=$BASE_REF"
echo "full=$FULL"
} >> "$GITHUB_OUTPUT"
# A pull request that ENTERS review is INVITED, not reviewed. This job posts
# the summary comment with one checkbox; ticking it (or typing the command)
# is what starts the first review, through resolve_command above. It asks the
# hub first, so a repository whose review is switched off gets no invitation
# to a review that would not run; a hub that cannot be reached still gets
# one, because the invitation promises nothing about this moment. It reports
# nothing to the hub: no run row, and above all no reviewed-head anchor, which
# would make the first real review skip everything up to this commit.
#
# The gate is the one the automatic review used to have: never a bot's pull
# request (a hub-opened fix PR), never a fork (read-only token), never a draft.
invite:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action != 'labeled' &&
github.event.action != 'synchronize' &&
github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
# Fail LOUD, not silent: a pull_request run on a repo whose org never set
# FLAMINGO_HUB_BASE_URL would otherwise curl an empty origin and die with
# an unrelated error. Also normalizes a trailing slash ONCE for every
# downstream consumer (a value of https://hub.example/ would otherwise
# yield //api double-slash paths in four places).
- name: Validate configuration
run: |
if [ -z "$HUB_BASE_URL" ]; then
echo "::error::HUB_BASE_URL is empty — set the org Actions variable FLAMINGO_HUB_BASE_URL (or pass hub_base_url in the dispatch payload)."
exit 1
fi
echo "HUB_BASE_URL=${HUB_BASE_URL%/}" >> "$GITHUB_ENV"
- name: Download the review scripts from the hub
env:
WEBHOOK_SECRET: ${{ secrets.FLAMINGO_HUB_SECRET }}
run: |
set -euo pipefail
SCRIPT_MANIFEST=/tmp/flamingo-script-manifest.json
# WEBHOOK_SECRET reaches curl through a 0600 config file, never argv — see
# curlAuthPreamble, which always traps the removal.
CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG"
trap 'rm -f "$CURL_CFG"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$WEBHOOK_SECRET" > "$CURL_CFG"
# The scripts surface. load_script_manifest pins SCRIPTS_BASE_URL to it.
CI_SCRIPTS_URL="${HUB_BASE_URL%/}/api/ci/scripts"
# _try_manifest <base> — 0 loaded, 1 no manifest surface there, 2 fatal.
# The manifest is asked for ONE group: its keys are the files to download.
_try_manifest() {
local base="$1" code
code=$(curl -sS -w '%{http_code}' -o "$SCRIPT_MANIFEST" \
-K "$CURL_CFG" \
"$base/manifest.json?group=$SCRIPT_GROUP") || code="000"
if [ "$code" = "404" ]; then rm -f "$SCRIPT_MANIFEST"; return 1; fi
if [ "$code" != "200" ]; then
echo "❌ manifest request to $base failed (HTTP $code)"
rm -f "$SCRIPT_MANIFEST"
return 2
fi
# The digests are the TOP-LEVEL object. successResponse is the standard
# emitter but it does NOT add a wrapper — it is NextResponse.json(data)
# plus the no-store header — so there is no .data to reach through.
# A 200 that is not a manifest is how a hub which does not serve this path
# answers (the proxy rewrites unknown routes and returns HTML), so it
# means "wrong surface", not "corrupt".
if ! jq -e 'type == "object" and length > 0 and (to_entries | all(.value | type == "string"))' "$SCRIPT_MANIFEST" >/dev/null 2>&1; then
rm -f "$SCRIPT_MANIFEST"
return 1
fi
return 0
}
# load_script_manifest <group>
load_script_manifest() {
SCRIPT_GROUP="$1"
# "cmd; rc=$?" dies under the set -euo pipefail these steps run with —
# errexit fires before rc is read and the step ends with NO output. And
# "if ! cmd; then rc=$?" is worse: inside the branch $? is the status of
# the NEGATION (0), so every failure reads as success. "|| rc=$?" is the
# one form that both suppresses errexit and preserves the real code.
local rc=0
_try_manifest "$CI_SCRIPTS_URL" || rc=$?
if [ "$rc" = "0" ]; then
SCRIPTS_BASE_URL="$CI_SCRIPTS_URL"
echo "✅ script manifest loaded ($(jq -r 'length' "$SCRIPT_MANIFEST") scripts)"
return 0
fi
if [ "$rc" = "2" ]; then exit 1; fi
# No manifest on the scripts surface: a hub older than the manifest
# itself. The manifest is the file list, so there is nothing to download.
echo "❌ no script manifest on this hub ($HUB_BASE_URL): it cannot name the $SCRIPT_GROUP scripts. Redeploy the hub."
exit 1
}
# download_script_group <group> — the hub names the files, this workflow
# names only the group. Downloads every script of the group, in served order.
download_script_group() {
load_script_manifest "$1"
local name
# The loop runs in THIS shell (no pipe), so a failed download exits the step.
while IFS= read -r name; do
download_and_verify "$name"
done < <(jq -r 'keys_unsorted[]' "$SCRIPT_MANIFEST")
}
download_and_verify() {
local script_name="$1"
local output_path="/tmp/$script_name"
local expected_hash
expected_hash=$(jq -r --arg n "$script_name" '.[$n] // empty' "$SCRIPT_MANIFEST")
if [ -z "$expected_hash" ]; then
echo "❌ $script_name is not in the server's script manifest!"
echo " The hub serves no such script, or it failed to read on the server."
exit 1
fi
if ! printf '%s' "$expected_hash" | grep -Eq '^[0-9a-f]{64}$'; then
echo "❌ the manifest entry for $script_name is not a SHA-256 digest — refusing to run it."
exit 1
fi
curl -fsSL "$SCRIPTS_BASE_URL/$script_name" \
-K "$CURL_CFG" \
-o "$output_path"
local actual_hash=$(shasum -a 256 "$output_path" | cut -d' ' -f1)
if [ "$actual_hash" != "$expected_hash" ]; then
echo "❌ HASH MISMATCH for $script_name!"
echo " Expected: $expected_hash"
echo " Actual: $actual_hash"
echo " The download was corrupted in transit — both values come from the same deployment."
exit 1
fi
# Make shell scripts executable
if [[ "$script_name" == *.sh ]]; then
chmod +x "$output_path"
fi
echo "✅ $script_name verified (hash: ${actual_hash:0:16}...)"
}
download_script_group "review"
- name: Ask the hub whether this repository is reviewed
id: rules
env:
WEBHOOK_SECRET: ${{ secrets.FLAMINGO_HUB_SECRET }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: /tmp/code-review-fetch-rules.sh
- name: Invite a review on the pull request
if: steps.rules.outputs.skip != 'true' || steps.rules.outputs.skip_kind == 'corpus_unavailable'
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
WF_RUN_ID: ${{ github.run_id }}
run: |
if grep -aq -- "argv.includes('--invite')" /tmp/code-review-post.mjs; then
node /tmp/code-review-post.mjs --invite
else
echo "the hub's post.mjs predates --invite — the pull request gets no invitation; the comment command still starts a review"
fi
review:
# Superseding a PR cancels the in-flight review of the stale head SHA; the
# fresh run reviews (and reports on) the new one. The always() report step
# still fires on the cancelled job, so its row records 'cancelled' rather
# than dangling. Job-level on purpose — see the note above permissions.
concurrency:
group: flamingo-code-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
cancel-in-progress: true
# Skip actual work when triggered by push (push only registers the
# workflow with GitHub, which is what lets workflow_dispatch target a
# setup branch before the install PR merges).
# Bots and forks never run, on EVERY event. A fork PR's token is read-only,
# so the run could not post a comment or a check even if it finished; the
# bot clause is what stops a hub-opened ai-fix PR from being reviewed into
# another fix PR, and no label may waive it — "only a human applies a
# label" is an assumption, not an invariant, since anything holding
# pull-requests: write can label. To review a bot PR, dispatch it from the
# hub admin, which is an authenticated, recorded decision.
#
# DRAFT is different, and only the on-demand label waives it: an explicit
# request says "review this now" and means it whether or not the PR is
# finished, which is how every comparable tool treats its manual trigger.
# Subscribed pushes still skip drafts.
#
# The events where a pull request ENTERS review (opened / ready_for_review /
# reopened) are NOT here: they run the invite job, never a review.
# NOTE: there is deliberately no vars. kill switch here. The hub's
# per-repo enabled dial already covers it and answers 409 REVIEW_DISABLED,
# which produces a recorded run. A second switch living in GitHub would be
# invisible to the admin screen and would produce no callback at all.
#
# A COMMENT COMMAND arrives here already judged: resolve_command made the
# API calls this expression cannot, and its 'proceed' carries the same two
# exclusions (fork, bot) plus the "is it really a pull request" answer.
#
# 'needs' therefore requires always(): on every OTHER event resolve_command
# is skipped, and a skipped dependency would otherwise skip this job too —
# i.e. adding the comment trigger would have silently disabled every
# existing trigger. always() re-admits them; the clauses below still decide.
needs: [resolve_command]
if: >-
always() &&
github.event_name != 'push' &&
(github.event_name == 'repository_dispatch' ||
needs.resolve_command.outputs.proceed == 'true' ||
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.head.repo.full_name == github.repository &&
((github.event.action == 'labeled' && github.event.label.name == 'flamingo-review') ||
(github.event.action == 'synchronize' &&
(contains(github.event.pull_request.labels.*.name, 'flamingo-review') ||
(github.event.pull_request.draft == false &&
contains(github.event.pull_request.labels.*.name, 'flamingo-review-always')))))))
runs-on: ubuntu-latest
# NO custom timeout-minutes — deliberately. GitHub's 6h hosted-runner
# ceiling is the only clock: on hitting it the always() report step still
# runs, so a sweep ships its per-batch checkpoint home and the NEXT sweep
# resumes from the covered files (nothing is lost, nothing re-paid). Hung
# runs are the hub reaper's job (run-safety-net-utils.ts, no-report cutoff
# sized past the platform ceiling) — a second hand-tuned ceiling here was
# one more number to keep in sync for no added safety.
steps:
# Fail LOUD, not silent: a pull_request run on a repo whose org never set
# FLAMINGO_HUB_BASE_URL would otherwise curl an empty origin and die with
# an unrelated error. Also normalizes a trailing slash ONCE for every
# downstream consumer (a value of https://hub.example/ would otherwise
# yield //api double-slash paths in four places).
- name: Validate configuration
run: |
if [ -z "$HUB_BASE_URL" ]; then
echo "::error::HUB_BASE_URL is empty — set the org Actions variable FLAMINGO_HUB_BASE_URL (or pass hub_base_url in the dispatch payload)."
exit 1
fi
echo "HUB_BASE_URL=${HUB_BASE_URL%/}" >> "$GITHUB_ENV"
# REPORT CAPABILITY FIRST. Only workflow-helpers.sh + code-review-report.sh
# download here — before checkout, cache, or any other script — so every
# later failure (including the download step for the REST of the scripts
# 404ing or hash-mismatching, the class that produced phantom 'running'
# rows) still has a verified report script to call home with.
- name: Download the report script
env:
WEBHOOK_SECRET: ${{ secrets.FLAMINGO_HUB_SECRET }}
run: |
set -euo pipefail
SCRIPT_MANIFEST=/tmp/flamingo-script-manifest.json
# WEBHOOK_SECRET reaches curl through a 0600 config file, never argv — see
# curlAuthPreamble, which always traps the removal.
CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG"
trap 'rm -f "$CURL_CFG"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$WEBHOOK_SECRET" > "$CURL_CFG"
# The scripts surface. load_script_manifest pins SCRIPTS_BASE_URL to it.
CI_SCRIPTS_URL="${HUB_BASE_URL%/}/api/ci/scripts"
# _try_manifest <base> — 0 loaded, 1 no manifest surface there, 2 fatal.
# The manifest is asked for ONE group: its keys are the files to download.
_try_manifest() {
local base="$1" code
code=$(curl -sS -w '%{http_code}' -o "$SCRIPT_MANIFEST" \
-K "$CURL_CFG" \
"$base/manifest.json?group=$SCRIPT_GROUP") || code="000"
if [ "$code" = "404" ]; then rm -f "$SCRIPT_MANIFEST"; return 1; fi
if [ "$code" != "200" ]; then
echo "❌ manifest request to $base failed (HTTP $code)"
rm -f "$SCRIPT_MANIFEST"
return 2
fi
# The digests are the TOP-LEVEL object. successResponse is the standard
# emitter but it does NOT add a wrapper — it is NextResponse.json(data)
# plus the no-store header — so there is no .data to reach through.
# A 200 that is not a manifest is how a hub which does not serve this path
# answers (the proxy rewrites unknown routes and returns HTML), so it
# means "wrong surface", not "corrupt".
if ! jq -e 'type == "object" and length > 0 and (to_entries | all(.value | type == "string"))' "$SCRIPT_MANIFEST" >/dev/null 2>&1; then
rm -f "$SCRIPT_MANIFEST"
return 1
fi
return 0
}
# load_script_manifest <group>
load_script_manifest() {
SCRIPT_GROUP="$1"
# "cmd; rc=$?" dies under the set -euo pipefail these steps run with —
# errexit fires before rc is read and the step ends with NO output. And
# "if ! cmd; then rc=$?" is worse: inside the branch $? is the status of
# the NEGATION (0), so every failure reads as success. "|| rc=$?" is the
# one form that both suppresses errexit and preserves the real code.
local rc=0
_try_manifest "$CI_SCRIPTS_URL" || rc=$?
if [ "$rc" = "0" ]; then
SCRIPTS_BASE_URL="$CI_SCRIPTS_URL"
echo "✅ script manifest loaded ($(jq -r 'length' "$SCRIPT_MANIFEST") scripts)"
return 0
fi
if [ "$rc" = "2" ]; then exit 1; fi
# No manifest on the scripts surface: a hub older than the manifest
# itself. The manifest is the file list, so there is nothing to download.
echo "❌ no script manifest on this hub ($HUB_BASE_URL): it cannot name the $SCRIPT_GROUP scripts. Redeploy the hub."
exit 1
}
# download_script_group <group> — the hub names the files, this workflow
# names only the group. Downloads every script of the group, in served order.
download_script_group() {
load_script_manifest "$1"
local name
# The loop runs in THIS shell (no pipe), so a failed download exits the step.
while IFS= read -r name; do
download_and_verify "$name"
done < <(jq -r 'keys_unsorted[]' "$SCRIPT_MANIFEST")
}
download_and_verify() {
local script_name="$1"
local output_path="/tmp/$script_name"
local expected_hash
expected_hash=$(jq -r --arg n "$script_name" '.[$n] // empty' "$SCRIPT_MANIFEST")
if [ -z "$expected_hash" ]; then
echo "❌ $script_name is not in the server's script manifest!"
echo " The hub serves no such script, or it failed to read on the server."
exit 1
fi
if ! printf '%s' "$expected_hash" | grep -Eq '^[0-9a-f]{64}$'; then
echo "❌ the manifest entry for $script_name is not a SHA-256 digest — refusing to run it."
exit 1
fi
curl -fsSL "$SCRIPTS_BASE_URL/$script_name" \
-K "$CURL_CFG" \
-o "$output_path"
local actual_hash=$(shasum -a 256 "$output_path" | cut -d' ' -f1)
if [ "$actual_hash" != "$expected_hash" ]; then
echo "❌ HASH MISMATCH for $script_name!"
echo " Expected: $expected_hash"
echo " Actual: $actual_hash"
echo " The download was corrupted in transit — both values come from the same deployment."
exit 1
fi
# Make shell scripts executable
if [[ "$script_name" == *.sh ]]; then
chmod +x "$output_path"
fi
echo "✅ $script_name verified (hash: ${actual_hash:0:16}...)"
}
download_script_group "review-report"
# Early liveness ping — the FIRST real step after report capability is
# secured. Stamps workflow_run_id + status 'running' on the hub's run row,
# so "dispatch accepted but nothing ever ran" (no ping — reaped fast) is
# distinguishable from "started, then crashed" (pinged — longer deadline).
# Best-effort by design: the script exits 0 regardless; the always()
# report below is the authoritative callback.
- name: Tell the hub the run started
env:
WEBHOOK_SECRET: ${{ secrets.FLAMINGO_HUB_SECRET }}
RUN_TOKEN: ${{ github.event.client_payload.run_token || github.event.inputs.run_token || '' }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || needs.resolve_command.outputs.head_sha || github.sha }}
WF_RUN_ID: ${{ github.run_id }}
REPO_FULL: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || needs.resolve_command.outputs.pr_number }}
run: /tmp/code-review-report.sh --started
# v5 = the Node 24 drop-in (v4 targets EOL Node 20 and warns on every run).
- name: Check out the code under review
uses: actions/checkout@v5
with:
fetch-depth: 0
# A pull_request run is already checked out at the right ref, and a
# dispatch wants the branch it targeted — both leave this EMPTY,
# which is checkout's own "use the triggering ref" default.
#
# An issue_comment run is the exception and the reason this line
# exists: that event's ref is the DEFAULT BRANCH, not the pull
# request. Without an explicit ref the command would cheerfully
# review main and report the result onto someone's PR.
#
# Pinned to the SHA resolve_command read, not to refs/pull/N/head:
# the sha is what the run reports on, what the incremental anchor is
# compared against, and what the check lands on. Resolving the ref a
# second time here could pick up a push that landed in between and
# review a different commit than the one recorded.
ref: ${{ needs.resolve_command.outputs.head_sha }}
# The review job must never hold a push credential.
persist-credentials: false
# The SAME download_and_verify function as the Code Documentation workflow
# (single source: lib/config/workflow-scripts-bootstrap.ts, parity with
# the doc template asserted at build time), fetching from the SAME
# scripts endpoint. This bootstrap (and the report-capability step above,
# which shares it) is the ONLY inline bash in the file — every other step
# runs a verified unified script, so a script change ships from the hub
# without touching this caller. Helpers + report already downloaded above;
# this step fetches the rest, and its failure is REPORTABLE.
- name: Download the review scripts from the hub
id: scripts
env:
WEBHOOK_SECRET: ${{ secrets.FLAMINGO_HUB_SECRET }}
run: |
set -euo pipefail
SCRIPT_MANIFEST=/tmp/flamingo-script-manifest.json
# WEBHOOK_SECRET reaches curl through a 0600 config file, never argv — see
# curlAuthPreamble, which always traps the removal.
CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG"
trap 'rm -f "$CURL_CFG"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$WEBHOOK_SECRET" > "$CURL_CFG"
# The scripts surface. load_script_manifest pins SCRIPTS_BASE_URL to it.
CI_SCRIPTS_URL="${HUB_BASE_URL%/}/api/ci/scripts"
# _try_manifest <base> — 0 loaded, 1 no manifest surface there, 2 fatal.
# The manifest is asked for ONE group: its keys are the files to download.
_try_manifest() {
local base="$1" code
code=$(curl -sS -w '%{http_code}' -o "$SCRIPT_MANIFEST" \
-K "$CURL_CFG" \
"$base/manifest.json?group=$SCRIPT_GROUP") || code="000"
if [ "$code" = "404" ]; then rm -f "$SCRIPT_MANIFEST"; return 1; fi
if [ "$code" != "200" ]; then
echo "❌ manifest request to $base failed (HTTP $code)"
rm -f "$SCRIPT_MANIFEST"
return 2
fi
# The digests are the TOP-LEVEL object. successResponse is the standard
# emitter but it does NOT add a wrapper — it is NextResponse.json(data)
# plus the no-store header — so there is no .data to reach through.
# A 200 that is not a manifest is how a hub which does not serve this path
# answers (the proxy rewrites unknown routes and returns HTML), so it
# means "wrong surface", not "corrupt".
if ! jq -e 'type == "object" and length > 0 and (to_entries | all(.value | type == "string"))' "$SCRIPT_MANIFEST" >/dev/null 2>&1; then
rm -f "$SCRIPT_MANIFEST"
return 1
fi
return 0
}
# load_script_manifest <group>
load_script_manifest() {
SCRIPT_GROUP="$1"
# "cmd; rc=$?" dies under the set -euo pipefail these steps run with —
# errexit fires before rc is read and the step ends with NO output. And
# "if ! cmd; then rc=$?" is worse: inside the branch $? is the status of
# the NEGATION (0), so every failure reads as success. "|| rc=$?" is the
# one form that both suppresses errexit and preserves the real code.
local rc=0
_try_manifest "$CI_SCRIPTS_URL" || rc=$?
if [ "$rc" = "0" ]; then
SCRIPTS_BASE_URL="$CI_SCRIPTS_URL"
echo "✅ script manifest loaded ($(jq -r 'length' "$SCRIPT_MANIFEST") scripts)"
return 0
fi
if [ "$rc" = "2" ]; then exit 1; fi
# No manifest on the scripts surface: a hub older than the manifest
# itself. The manifest is the file list, so there is nothing to download.
echo "❌ no script manifest on this hub ($HUB_BASE_URL): it cannot name the $SCRIPT_GROUP scripts. Redeploy the hub."
exit 1
}
# download_script_group <group> — the hub names the files, this workflow
# names only the group. Downloads every script of the group, in served order.
download_script_group() {
load_script_manifest "$1"
local name
# The loop runs in THIS shell (no pipe), so a failed download exits the step.
while IFS= read -r name; do
download_and_verify "$name"
done < <(jq -r 'keys_unsorted[]' "$SCRIPT_MANIFEST")
}
download_and_verify() {
local script_name="$1"
local output_path="/tmp/$script_name"
local expected_hash
expected_hash=$(jq -r --arg n "$script_name" '.[$n] // empty' "$SCRIPT_MANIFEST")
if [ -z "$expected_hash" ]; then
echo "❌ $script_name is not in the server's script manifest!"
echo " The hub serves no such script, or it failed to read on the server."
exit 1
fi
if ! printf '%s' "$expected_hash" | grep -Eq '^[0-9a-f]{64}$'; then
echo "❌ the manifest entry for $script_name is not a SHA-256 digest — refusing to run it."
exit 1
fi
curl -fsSL "$SCRIPTS_BASE_URL/$script_name" \
-K "$CURL_CFG" \
-o "$output_path"
local actual_hash=$(shasum -a 256 "$output_path" | cut -d' ' -f1)
if [ "$actual_hash" != "$expected_hash" ]; then
echo "❌ HASH MISMATCH for $script_name!"
echo " Expected: $expected_hash"
echo " Actual: $actual_hash"
echo " The download was corrupted in transit — both values come from the same deployment."
exit 1
fi
# Make shell scripts executable
if [[ "$script_name" == *.sh ]]; then
chmod +x "$output_path"
fi
echo "✅ $script_name verified (hash: ${actual_hash:0:16}...)"
}
# Digests come from the deployment that serves the bytes, never from
# this file: a pinned hash here would be the PR head ref's while the
# scripts are main's, which is exactly how a PR used to brick its own
# review. See workflow-scripts-bootstrap.ts.
#
# The FILE LIST is the hub's too: this step names a group and downloads
# every script the manifest lists for it (SCRIPT_GROUPS in
# lib/config/ci-script-catalog.ts), so a module added to the group
# reaches this repo on its next run with no workflow re-push.
download_script_group "review"
# The graph library rides in the same group, and is OPTIONAL: the
# reviewer imports it dynamically and reviews without cross-repo facts
# when it is absent. Whether it came is read from the manifest that
# was just served (never from /tmp, which a self-hosted runner keeps
# between jobs). The graph_lib output gates the dependency
# install below, so a review without the library installs nothing.
if jq -e 'has("code-graph-lib.mjs")' "$SCRIPT_MANIFEST" >/dev/null 2>&1; then
echo "graph_lib=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::this hub does not serve the code-graph library; reviewing without cross-repo facts"
echo "graph_lib=false" >> "$GITHUB_OUTPUT"
fi
# ONE hub call answers how this repository is reviewed: the settings
# (mode, models, locale, whether the hub's TOOLS are on), the hash that
# anchors incremental review, and the rule INDEX. Where the tools are on,
# that is all the review step is handed: it reads rule text and the code
# graph through the hub's tools (get_code_rules, get_code_rule,
# get_repo_ecosystem, find_code_consumers, …) as it reviews. Where they
# are off, the same call carries the full rule text. Nothing is cached
# between runs: every review asks the hub afresh.
- name: Ask the hub how to review this repository
id: rules
env:
WEBHOOK_SECRET: ${{ secrets.FLAMINGO_HUB_SECRET }}
# PR runs identify the pull request so the hub can answer with the
# last successfully reviewed head SHA (X-Last-Reviewed-Sha) — the
# incremental-review anchor persisted as .last-reviewed-sha.
PR_NUMBER: ${{ github.event.pull_request.number || needs.resolve_command.outputs.pr_number }}
# '@flamingo-review full' asks the hub to WITHHOLD that anchor,
# so this pass re-reads the pull request's whole cumulative diff.
# Empty on every other trigger, which keeps them incremental.
REVIEW_FULL: ${{ needs.resolve_command.outputs.full }}
run: /tmp/code-review-fetch-rules.sh
# Stage checkpoints are their OWN credentialed steps. The review step
# holds the shared webhook secret — unavoidably, since the reviewer's
# Claude calls go through the hub's secret-gated proxy rather than
# straight to Anthropic — but NOT the run token, which is the credential
# that can close out a run row. So a checkpoint ping happens at a step
# boundary, not inside the step that reads untrusted PR content.
# Say so ON THE PULL REQUEST before spending minutes in the model. Placed
# AFTER the rules step on purpose: the skip decision is known here, so a run that
# reviews nothing never leaves a placeholder to strand. The comment
# carries the summary marker, so the final "Post to the pull request"
# step UPDATES this exact comment into the verdict rather than adding a
# second one, and it is re-created (not edited) each run so the live
# review sits at the BOTTOM of the timeline instead of scrolling away
# under later discussion. Best-effort: never fails the review.
- name: Say the review is running
if: (github.event_name == 'pull_request' || needs.resolve_command.outputs.pr_number != '') && steps.rules.outputs.skip != 'true'
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || needs.resolve_command.outputs.pr_number }}
WF_RUN_ID: ${{ github.run_id }}
# Feature-DETECTED, not assumed: across a deploy boundary this workflow
# can be handed a post.mjs that predates --running, which would ignore
# the flag and run the full post pass with no findings.json — posting a
# bogus verdict mid-review. Grep first; skip quietly if unsupported.
run: |
if grep -aq -- "argv.includes('--running')" /tmp/code-review-post.mjs; then
node /tmp/code-review-post.mjs --running
else
echo "the hub's post.mjs predates --running — skipping the placeholder"
fi