🦩 Update: Flamingo Code Documentation #111
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 (doc-orchestrator 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 doc-orchestrator workflow. | |
| push: | |
| paths: | |
| - '.github/workflows/flamingo-code-review.yml' | |
| # ENTERS-REVIEW ONLY (2026-08-12; corrected 2026-09-07). The automatic | |
| # triggers are the moments a pull request ENTERS review, and nothing else. | |
| # | |
| # 'synchronize' (per-push) stays deliberately OFF. 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; the 2026 industry default is to review | |
| # at review moments and offer an explicit re-review on demand. | |
| # | |
| # 'opened' and 'reopened' were OFF between 2026-08-15 and 2026-09-07, on the | |
| # assumption that PRs are opened as drafts and later promoted. They are not | |
| # — PRs here are born non-draft, GitHub NEVER fires ready_for_review for | |
| # those, and so the automatic review effectively never ran. Drafts stay | |
| # excluded: an 'opened' event for a draft is dropped by the job's | |
| # draft == false guard, and that PR is reviewed later, on its | |
| # ready_for_review. | |
| # | |
| # 'labeled' is the on-demand re-review — the affordance that makes the | |
| # no-per-push default liveable. Commits pushed AFTER the first review are not | |
| # auto-reviewed, so adding the flamingo-review label is how a human | |
| # asks for another pass. 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 on one review per | |
| # pull request. 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 MANUAL TRIGGER (2026-09-08). A new top-level pull-request | |
| # comment beginning with '@flamingo-review' asks for another 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. | |
| # | |
| # 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 doc-orchestrator 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 doc-orchestrator's 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')))) | |
| 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 "🦩 Could not read this pull request from the GitHub API, so the review was not started. Try the command again." | |
| 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 "🦩 This pull request comes from a fork, so it is not reviewed automatically. Dispatch it from the Flamingo hub instead." | |
| fi | |
| if [ "$AUTHOR_TYPE" = "Bot" ]; then | |
| refuse "🦩 This pull request was opened by a bot, so it is not reviewed automatically. Dispatch it from the Flamingo hub 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/' \ | |
| | 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" | |
| 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. | |
| # Automatic events and subscribed pushes still skip drafts. | |
| # 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')))) || | |
| (github.event.action != 'labeled' && | |
| github.event.action != 'synchronize' && | |
| github.event.pull_request.draft == false)))) | |
| 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.DOC_ORCH_WEBHOOK_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 canonical scripts surface and the pre-rename one. load_script_manifest | |
| # picks whichever this deployment actually serves and pins SCRIPTS_BASE_URL. | |
| CI_SCRIPTS_URL="${HUB_BASE_URL%/}/api/ci/scripts" | |
| LEGACY_SCRIPTS_URL="${HUB_BASE_URL%/}/api/doc-orchestrator/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" | |
| # The scripts surface was renamed from /api/doc-orchestrator/scripts to the | |
| # pipeline-neutral /api/ci/scripts (it always served BOTH pipelines). The | |
| # workflow file ships in the repo and the routes ship with the deployment, | |
| # so the two are one version apart in BOTH directions across the rollout. | |
| # Probe the canonical surface, fall back to the legacy one, and let the | |
| # winner decide SCRIPTS_BASE_URL for every download that follows. | |
| # "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 | |
| echo "::warning::this hub does not serve $CI_SCRIPTS_URL — falling back to the legacy $LEGACY_SCRIPTS_URL; it predates the rename" | |
| SCRIPTS_BASE_URL="$LEGACY_SCRIPTS_URL" | |
| rc=0 | |
| _try_manifest "$LEGACY_SCRIPTS_URL" || rc=$? | |
| if [ "$rc" = "0" ]; then | |
| echo "✅ script manifest loaded ($(jq -r 'length' "$SCRIPT_MANIFEST") scripts)" | |
| return 0 | |
| fi | |
| if [ "$rc" = "2" ]; then exit 1; fi | |
| # Neither surface published a manifest: 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.DOC_ORCH_WEBHOOK_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 doc-orchestrator 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.DOC_ORCH_WEBHOOK_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 canonical scripts surface and the pre-rename one. load_script_manifest | |
| # picks whichever this deployment actually serves and pins SCRIPTS_BASE_URL. | |
| CI_SCRIPTS_URL="${HUB_BASE_URL%/}/api/ci/scripts" | |
| LEGACY_SCRIPTS_URL="${HUB_BASE_URL%/}/api/doc-orchestrator/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" | |
| # The scripts surface was renamed from /api/doc-orchestrator/scripts to the | |
| # pipeline-neutral /api/ci/scripts (it always served BOTH pipelines). The | |
| # workflow file ships in the repo and the routes ship with the deployment, | |
| # so the two are one version apart in BOTH directions across the rollout. | |
| # Probe the canonical surface, fall back to the legacy one, and let the | |
| # winner decide SCRIPTS_BASE_URL for every download that follows. | |
| # "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 | |
| echo "::warning::this hub does not serve $CI_SCRIPTS_URL — falling back to the legacy $LEGACY_SCRIPTS_URL; it predates the rename" | |
| SCRIPTS_BASE_URL="$LEGACY_SCRIPTS_URL" | |
| rc=0 | |
| _try_manifest "$LEGACY_SCRIPTS_URL" || rc=$? | |
| if [ "$rc" = "0" ]; then | |
| echo "✅ script manifest loaded ($(jq -r 'length' "$SCRIPT_MANIFEST") scripts)" | |
| return 0 | |
| fi | |
| if [ "$rc" = "2" ]; then exit 1; fi | |
| # Neither surface published a manifest: 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.DOC_ORCH_WEBHOOK_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 | |
| # An INFRASTRUCTURE skip owes the pull request a word; an opt-out does | |
| # not. Every PR-facing step above and below is gated on | |
| # steps.rules.outputs.skip != 'true', so when the hub's rule endpoint is | |
| # down the pull request got a green completed run and NO comment at all — | |
| # indistinguishable from a reviewer that never triggered, which is the | |
| # exact state the placeholder machinery exists to eliminate. | |
| # Gated on the STABLE skip_kind token, not on 'degraded' (which carries | |
| # the HTTP code and would need copy per code). review_disabled never | |
| # reaches here: silence is the right answer to opting out. | |
| - name: Say the hub could not be reached | |
| if: (github.event_name == 'pull_request' || needs.resolve_command.outputs.pr_number != '') && steps.rules.outputs.skip_kind == 'corpus_unavailable' | |
| 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 }} | |
| run: | | |
| if grep -aq -- "argv.includes('--resolve')" /tmp/code-review-post.mjs; then | |
| node /tmp/code-review-post.mjs --resolve corpus_unavailable || true | |
| else | |
| echo "the hub's post.mjs predates --resolve — the pull request gets no notice" | |
| fi | |
| - name: Report progress — reviewing | |
| env: | |
| WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} | |
| RUN_TOKEN: ${{ github.event.client_payload.run_token || github.event.inputs.run_token || '' }} | |
| 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 --progress review | |
| # The graph library's runtime (wasm tree-sitter + grammars + yaml), pinned | |
| # and installed into an isolated tree under RUNNER_TEMP; the reviewer | |
| # reaches it through CODE_GRAPH_DEPS_DIR. continue-on-error: a registry | |
| # outage degrades the review to "no cross-repo facts" (the reviewer | |
| # treats an unset CODE_GRAPH_DEPS_DIR as "no graph"), it never fails it. | |
| # The command is CODE_GRAPH_INSTALL_COMMAND — one spelling for both workflows. | |
| # Gated on the library having been downloaded: without it nothing can | |
| # import this runtime, so installing it would be wasted work and would | |
| # export a CODE_GRAPH_DEPS_DIR no script reads. | |
| - name: Install the code-graph parsers for the deletion gate | |
| if: steps.rules.outputs.skip != 'true' && steps.scripts.outputs.graph_lib == 'true' | |
| continue-on-error: true | |
| run: mkdir -p "$RUNNER_TEMP/code-graph-deps" && cd "$RUNNER_TEMP/code-graph-deps" && printf '%s' '{"name":"code-graph-deps","version":"1.0.0","private":true,"dependencies":{"web-tree-sitter":"0.27.0","@vscode/tree-sitter-wasm":"0.3.1","yaml":"2.9.1"}}' > package.json && printf '%s' '{"name":"code-graph-deps","version":"1.0.0","lockfileVersion":3,"requires":true,"packages":{"":{"name":"code-graph-deps","version":"1.0.0","dependencies":{"web-tree-sitter":"0.27.0","@vscode/tree-sitter-wasm":"0.3.1","yaml":"2.9.1"}},"node_modules/web-tree-sitter":{"version":"0.27.0","resolved":"https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.27.0.tgz","integrity":"sha512-XK08gj6RwTMQatAG7uVRP8MunqotL/XC19vHgkSPKmELgbGPBj4ECvB8haHOUnyj6ls2B8t42UTro14zxGgAHg=="},"node_modules/@vscode/tree-sitter-wasm":{"version":"0.3.1","resolved":"https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz","integrity":"sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ=="},"node_modules/yaml":{"version":"2.9.1","resolved":"https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz","integrity":"sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="}}}' > package-lock.json && npm ci --ignore-scripts --no-audit --no-fund && echo "CODE_GRAPH_DEPS_DIR=$RUNNER_TEMP/code-graph-deps" >> "$GITHUB_ENV" || { echo "::warning::graph dependencies failed their lockfile-enforced install; continuing without them"; rm -rf "$RUNNER_TEMP/code-graph-deps"; exit 1; } | |
| # Reviews the change against the hub's rules and code graph. With the | |
| # tools on, the reviewer calls the hub for rule text and graph facts as | |
| # it works (its log prints every call under "Tool use"); with them off, | |
| # it works from the text the previous step fetched. Either way the | |
| # deterministic deletion gate asks the hub who consumes what a finding | |
| # would remove. | |
| - name: Review with the rules and code graph from the hub | |
| # The id lets the report step read the reviewer's own degraded output | |
| # (e.g. skipped_trivial_diff) alongside the rules step's. | |
| id: review | |
| if: steps.rules.outputs.skip != 'true' | |
| # MODE / RUN_ID arrive via the generated workflow-level env; only the | |
| # secret is scoped to the step (doc-workflow security discipline). | |
| # The reviewer calls Claude THROUGH THE HUB (/api/ci/claude, the | |
| # unified caller), so the step carries the hub's workflow secret and | |
| # the target repo needs no ANTHROPIC_API_KEY of its own. | |
| # | |
| # ANTHROPIC_API_KEY is NOT passed, and must not come back. It was a | |
| # transition shim for the deploy boundary — the scripts come from the | |
| # hub while this file ships in the repo, so a NEW workflow could run an | |
| # OLD script that still called Anthropic directly. That window is shut: | |
| # no shipped code-review script reads the variable — on main as well | |
| # as on this branch, which is what matters for the hub's OWN pull | |
| # requests, whose workflow runs from the PR head against scripts main | |
| # serves. A build gate asserts the two halves stay in agreement, so the key | |
| # cannot drift back into the one step that runs node over untrusted | |
| # pull-request content. | |
| env: | |
| WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} | |
| REVIEW_BASE_REF: ${{ github.event.pull_request.base.ref || needs.resolve_command.outputs.base_ref }} | |
| # The RUN STAMP. The reviewer and the miner write .review-run-id from | |
| # this value and the report step discards every artifact not stamped | |
| # with ITS run id — so this step MUST carry it. It did not, for one | |
| # commit in this branch's history: the stamp would have been written | |
| # empty on every CI run, the report step would have deleted the run's | |
| # own findings, and every review, sweep and mine would have reported | |
| # zero findings under a green status while the pull request showed | |
| # them. A build gate now asserts the writer's step has it. | |
| WF_RUN_ID: ${{ github.run_id }} | |
| # Where "Install graph dependencies" put the graph library's runtime; | |
| # empty when that step failed, which the reviewer reads as "no graph". | |
| CODE_GRAPH_DEPS_DIR: ${{ env.CODE_GRAPH_DEPS_DIR }} | |
| run: /tmp/code-review-run.sh | |
| # Post the findings to the PULL REQUEST itself and publish the | |
| # 'Flamingo Code Review' check run. pull_request events only — a sweep | |
| # has no PR to post to (its findings live in the hub dashboard). Uses the | |
| # runner's own GITHUB_TOKEN under the workflow's minimal grant | |
| # (pull-requests: write for the comments, checks: write for the check | |
| # run). One UPSERTED summary comment + inline comments where the diff | |
| # supports them; already-posted fingerprints are never re-commented, so | |
| # a new push adds only NEW findings. The check run is 'neutral' unless | |
| # the repo's served mode is 'blocking' AND action_required findings | |
| # exist. Writes posted.json (fingerprint -> comment id) for the report | |
| # step's callback — the reaction-learning loop reads it. | |
| - name: Report progress — finalizing | |
| env: | |
| WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} | |
| RUN_TOKEN: ${{ github.event.client_payload.run_token || github.event.inputs.run_token || '' }} | |
| 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 --progress post | |
| - name: Post to the pull request | |
| id: post | |
| if: (github.event_name == 'pull_request' || needs.resolve_command.outputs.pr_number != '') && steps.rules.outputs.skip != 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DEGRADED: ${{ steps.review.outputs.degraded }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || needs.resolve_command.outputs.pr_number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || needs.resolve_command.outputs.head_sha }} | |
| WF_RUN_ID: ${{ github.run_id }} | |
| run: node /tmp/code-review-post.mjs | |
| # TWO steps, one per outcome, because the reason has to be EXACT and a | |
| # status function (cancelled()) is only valid in an if: condition, never | |
| # in an env: block where it would be read as a literal. Reading | |
| # job.status from env instead was close but not guaranteed to say | |
| # 'cancelled' during a concurrency supersede — and getting it wrong means | |
| # telling the author a superseded run CRASHED. Cancellation is the case | |
| # that matters most: it reports no step failure at all, which is the | |
| # orphan source seen in production. | |
| # | |
| # ONE renderer, so the outcome is rendered from ONE value per step: the | |
| # fallback branch reports $OUTCOME while the flag carries its own copy, | |
| # and two hand-kept spellings would eventually name different outcomes in | |
| # the same comment. | |
| - name: Close out the running comment (cancelled) | |
| if: cancelled() && (github.event_name == 'pull_request' || needs.resolve_command.outputs.pr_number != '') && steps.rules.outputs.skip != 'true' && steps.post.outcome != 'success' | |
| 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 }} | |
| run: | | |
| if grep -aq -- "argv.includes('--resolve')" /tmp/code-review-post.mjs; then | |
| node /tmp/code-review-post.mjs --resolve cancelled || true | |
| else | |
| echo "the hub's post.mjs predates --resolve — leaving it to the run that superseded this one" | |
| fi | |
| - name: Close out the running comment (crashed) | |
| if: failure() && (github.event_name == 'pull_request' || needs.resolve_command.outputs.pr_number != '') && steps.rules.outputs.skip != 'true' && steps.post.outcome != 'success' | |
| 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 }} | |
| REPO_FULL: ${{ github.repository }} | |
| OUTCOME: crashed | |
| run: | | |
| if grep -aq -- "argv.includes('--resolve')" /tmp/code-review-post.mjs; then | |
| node /tmp/code-review-post.mjs --resolve crashed || true | |
| else | |
| echo "the hub's post.mjs predates --resolve — posting a standalone notice instead" | |
| BODY=$(printf '%s\n%s\n%s\n\n%s' '<!-- flamingo-code-review -->' '<!-- flamingo-code-review no-verdict -->' '## 🦩 Flamingo Code Review' "**Did not finish on this push** ($OUTCOME). No findings were posted: this is an infrastructure state, not a clean review. See the [workflow run](${GITHUB_SERVER_URL:-https://github.com}/$REPO_FULL/actions/runs/$WF_RUN_ID).") | |
| CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG" | |
| trap 'rm -f "$CURL_CFG"' EXIT | |
| printf 'header = "Authorization: Bearer %s"\n' "$GITHUB_TOKEN" > "$CURL_CFG" | |
| curl -s -K "$CURL_CFG" -X POST "${GITHUB_API_URL:-https://api.github.com}/repos/$REPO_FULL/issues/$PR_NUMBER/comments" \ | |
| -H "Content-Type: application/json" \ | |
| --data "$(printf '%s' "$BODY" | jq -Rs '{body: .}')" > /dev/null || true | |
| fi | |
| # The report script downloads FIRST (dedicated step above), so a missing | |
| # /tmp/code-review-report.sh here means the report-capability bootstrap | |
| # itself failed (endpoint down, hash mismatch before anything else ran). | |
| # The guarded inline curl below still posts a minimal failure callback for | |
| # exactly that case; the hub's tiered reaper remains the last resort. | |
| - name: Report back to the hub | |
| if: always() && env.HUB_BASE_URL != '' | |
| # RUN_ID / MODE / HUB_BASE_URL arrive via the generated workflow-level | |
| # env. RUN_TOKEN is SENSITIVE (per-run bearer) so it is step-scoped | |
| # HERE — the only consumer — and masked by the script; the review step | |
| # that processes untrusted PR content never sees it. Remaining step env | |
| # is the secret plus values that exist nowhere but the GitHub context — | |
| # which enters through env, never interpolated into a script body. | |
| env: | |
| WEBHOOK_SECRET: ${{ secrets.DOC_ORCH_WEBHOOK_SECRET }} | |
| RUN_TOKEN: ${{ github.event.client_payload.run_token || github.event.inputs.run_token || '' }} | |
| JOB_STATUS: ${{ job.status }} | |
| # Either step can degrade a run: the rules step (corpus unavailable) | |
| # or the reviewer itself (trivial-diff skip, unresolvable PR base). | |
| DEGRADED: ${{ steps.rules.outputs.degraded || steps.review.outputs.degraded }} | |
| # Dispatch events carry no pull_request context — github.sha is the | |
| # tip of the checked-out ref, so sweep runs record the commit too. | |
| 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 }} | |
| # The ONE justified inline-bash exception beyond the bootstrap: when the | |
| # report script itself never downloaded, no unified script exists to | |
| # report the failure — so a minimal guarded curl posts it. Kept tiny on | |
| # purpose; all real reporting logic stays in code-review-report.sh. | |
| run: | | |
| if [ ! -x /tmp/code-review-report.sh ]; then | |
| echo "::error::code-review-report.sh missing — sending bootstrap-failure callback" | |
| # RUN_TOKEN rides jq's env for the same reason the bearer rides a | |
| # curl config file — see curlAuthPreamble. | |
| CURL_CFG=$(mktemp) && chmod 600 "$CURL_CFG" | |
| trap 'rm -f "$CURL_CFG"' EXIT | |
| printf 'header = "Authorization: Bearer %s"\n' "$WEBHOOK_SECRET" > "$CURL_CFG" | |
| jq -n --arg run_id "${RUN_ID:-}" \ | |
| --arg wf "$WF_RUN_ID" --arg repo "$REPO_FULL" --arg mode "$MODE" \ | |
| '{run_id: $run_id, run_token: ($ENV.RUN_TOKEN // ""), status: "failure", | |
| degraded_reason: "bootstrap_failed", workflow_run_id: $wf, | |
| repo_full_name: $repo, mode: $mode, | |
| error_message: "Script bootstrap failed: the report script never downloaded from the hub."}' | | |
| curl -sS --max-time 30 -K "$CURL_CFG" -X POST "$HUB_BASE_URL/api/code-review/webhook" \ | |
| -H "Content-Type: application/json" -d @- || true | |
| exit 1 | |
| fi | |
| /tmp/code-review-report.sh |