Skip to content

fix(github): het opruimen van previews mag productie niet raken #3762

fix(github): het opruimen van previews mag productie niet raken

fix(github): het opruimen van previews mag productie niet raken #3762

name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# A cross-repo PR gets no secrets, so CLAUDE_CODE_OAUTH_TOKEN is empty there and
# the action can only fail. Skip instead, and let review-gate report that.
# Compare full_name rather than reading `head.repo.fork`: inside a fork of this
# repo its own PRs are not cross-repo and do have secrets.
claude-review:
if: >-
${{ !github.event.pull_request.draft &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login != 'dependabot[bot]' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
# Opruimen gebeurt ná de review, niet ervoor. Wat hier gebeurt is alleen
# vastleggen wat er staat: de ids om straks op te ruimen, en de teksten om
# aan de review mee te geven. Klapt de run hierna, dan is er niets
# weggegooid en staat de vorige bevinding er nog. Dat is precies het geval
# waarin de oude opzet — eerst wissen, dan reviewen — hem kwijtraakte,
# terwijl de poort op een kritieke bevinding rood wordt: opnieuw pushen tot
# de review het niet meer opmerkt zou dan groen opleveren.
- name: Snapshot the previous review
id: snapshot
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: script/claude-review-comments.sh snapshot
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
use_sticky_comment: true
claude_args: "--allowedTools Bash,Read,Glob,Grep"
prompt: |
Review PR #${{ github.event.pull_request.number }} in ${{ github.repository }}.
Read `REVIEW.md` for the project context, review dimensions, severity scale, and
skip rules. Follow those guidelines strictly.
## CRITICAL SCOPE RULE
**ONLY review lines that were actually added or modified in this PR's diff.**
You MUST NOT comment on:
- Pre-existing code that was not changed in this PR
- Context lines shown in the diff that were not added/modified (lines without + prefix)
- Issues in unchanged files or unchanged sections of changed files
- Pre-existing patterns, naming, style, or technical debt in surrounding code
You may read full files for context to understand the changes, but every single
finding you report MUST point to a line that was added or modified in the diff.
If a line was not changed by this PR, it is OUT OF SCOPE, no matter how wrong it looks.
Before submitting each finding, verify: "Was this line actually changed in the PR diff?"
If the answer is no, drop the finding.
Skip dimensions that have no findings. Be precise and actionable — no "consider"
or "you might want to" comments.
## Output format
Structure your review as a single comment with sections for each dimension that has
findings. Use this severity scale:
- 🔴 **Critical** — wrong legal outcome, data loss, runtime crash, security vulnerability
- 🟠 **Significant** — likely bug, broken reference, missing edge case, lost jobs
- 🟡 **Minor** — code quality, style, non-blocking improvement
Also leave inline comments on specific lines where possible.
If there are no issues, say so briefly. Do not pad the review with praise or filler.
## The Critical marker is read by a machine
The merge gate `Claude review completed` searches this comment, your inline
comments and the body of the review you submit for the exact string
`🔴 **Critical**`, and turns red when it finds one, which blocks the merge.
Write the marker exactly as it appears above — same
emoji, same bold, same word — and only on a finding that is genuinely critical.
Do not reword it, do not drop the emoji, and do not write it anywhere you are
not reporting a critical finding: not in a heading, not in a legend of the
scale, not in an example. A merge is held or let through on that string.
End every comment you write for this review — the summary comment and each
inline comment — with this line, exactly:
<!-- claude-review -->
It renders as nothing and it is how the workflow tells your review apart from
an ordinary `@claude` answer in the same thread. A comment without it is left
alone: it is not cleaned up after the next review, and it is not handed to that
review as context.
${{ steps.snapshot.outputs.context }}
# Het bewijs waar review-gate op afgaat. `execution_file` wijst naar het
# uitvoerbestand van Claude en wordt pas gezet nadat de CLI gedraaid heeft;
# stapt de actie er eerder uit — de workflow-validation-skip, wanneer dit
# bestand afwijkt van de versie op de default branch — dan blijft die
# output leeg en wordt deze stap overgeslagen. De poort leest de uitkomst
# van deze stap uit de jobs van déze run, dus het bewijs is per constructie
# run-gebonden, en het bestaat ook wanneer de review nul bevindingen had.
# Hernoem deze stap niet zonder `PROOF_STEP` in review-gate mee te nemen.
- name: Record that the review ran
if: steps.claude-review.outputs.execution_file != ''
env:
EXECUTION_FILE: ${{ steps.claude-review.outputs.execution_file }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
echo "De review-actie is uitgevoerd; uitvoerbestand: ${EXECUTION_FILE}"
{
echo "### Claude review uitgevoerd"
echo
echo "De review-actie heeft gedraaid voor commit \`${HEAD_SHA}\`."
echo "Nul bevindingen is hier een geldige uitkomst; deze stap zegt alleen dat er gereviewd is."
} >>"$GITHUB_STEP_SUMMARY"
# Nu de nieuwe review er staat, mag de vorige weg, en alleen de vorige:
# wat in de momentopname stond en sindsdien niet is aangeraakt. Deze stap
# draait onder dezelfde voorwaarde als de bewijsstap, dus een run die
# klapte voordat er iets geschreven was gooit niets weg.
- name: Clean up the superseded review
if: steps.claude-review.outputs.execution_file != ''
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: script/claude-review-comments.sh clean-up
# The merge gate. Unlike claude-review this job has no `if:` — it must run and
# report on every PR, cross-repo and dependabot included, because a required
# check that never reports blocks such a PR forever.
review-gate:
name: Claude review completed
runs-on: ubuntu-latest
# Ruim boven de ~10 minuten die de review kost, zodat een trage run niet
# onterecht rood wordt; het script stopt zelf eerder met een duidelijke melding.
timeout-minutes: 40
permissions:
actions: read
contents: read
# `issues: read` is voor de samenvattende comment: die is een issue-comment
# en komt van `issues/{n}/comments`. Zonder die scope leest de poort hem
# niet en blokkeert elke PR op "niet op te halen".
issues: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
# De poort mag niet draaien op de versie van zichzelf die in de PR staat:
# een PR die het script naar `exit 0` verandert zou anders per definitie
# groen zijn. Draai daarom de versie van de base-branch, en blokkeer als
# die niet te krijgen is — terugvallen op de PR-versie is precies de
# bypass die deze stap moet dichten.
- name: Take the gate script from the base branch
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
GATE: script/await-claude-review.sh
run: |
set -uo pipefail
fail() {
echo "::error title=Claude review gate::$1"
exit 1
}
git fetch --depth=1 origin "$BASE_SHA" ||
fail "base-commit ${BASE_SHA} is niet op te halen, dus de versie van de poort is niet vast te stellen. Draai deze job opnieuw."
git cat-file -e "${BASE_SHA}:${GATE}" 2>/dev/null ||
fail "${GATE} bestaat niet op de base-branch (${BASE_SHA}). De poort kan dan niet voor zichzelf instaan; deze PR moet met de hand gereviewd worden."
git checkout "$BASE_SHA" -- "$GATE" ||
fail "${GATE} is niet uit ${BASE_SHA} te checkouten, dus de poort zou de versie uit deze PR draaien. Draai deze job opnieuw."
echo "Poort draait de versie van ${BASE_SHA}."
- name: Wait for the Claude review of this commit
# Alleen de coördinaten van deze run. Wat de poort als feit behandelt —
# fork, draft, auteur, de versie van dit workflowbestand — haalt zij zelf
# op, want dit env-blok staat in de PR die gereviewd wordt.
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
MAX_WAIT_SECONDS: '2100'
POLL_SECONDS: '20'
run: script/await-claude-review.sh