Repository navigation
PR Review (2x Independent) #5
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
| # .github/workflows/pr-review.yml | |
| # PR Review — 2x Independent Opus | |
| # | |
| # Triggered by: | |
| # - repository_dispatch with event_type "pr-discovered" (automated pipeline) | |
| # - workflow_dispatch with pr_number + head_sha inputs (manual / R4-4 fix) | |
| # | |
| # Job topology: | |
| # gate → review-1 ──┐ | |
| # review-2 ──┤─→ merge-verdict | |
| # (review-2 runs independently; no shared cache or output | |
| # with review-1 until both are complete) | |
| # | |
| # IF_REVIEW_DOUBLE_REQUIRED repo variable: | |
| # unset / "true" (default) → both review-1 AND review-2 must complete before merge-verdict | |
| # "false" → review-2 is SKIPPED (single-review fast path, emergency only). | |
| # The fast path additionally requires a manual approval via | |
| # environment: branch-promotion and emits a warning ledger event. | |
| # This should never be used in normal operation. | |
| # | |
| # Concurrency: one run per PR number; cancel-in-progress is false so an in-flight | |
| # review is never silently aborted by a re-trigger. | |
| name: PR Review (2x Independent Opus) | |
| on: | |
| repository_dispatch: | |
| types: [pr-discovered] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: "Upstream dotnet/wpf PR number to review" | |
| required: true | |
| type: string | |
| head_sha: | |
| description: "Pinned head SHA of the upstream PR (from discovery ledger event)" | |
| required: true | |
| type: string | |
| concurrency: | |
| group: pr-review-${{ github.event.client_payload.pr_number || inputs.pr_number }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| issues: write | |
| id-token: write | |
| env: | |
| IF_REPO_ROOT: ${{ github.workspace }} | |
| PR_NUMBER: ${{ github.event.client_payload.pr_number || inputs.pr_number }} | |
| HEAD_SHA: ${{ github.event.client_payload.head_sha || inputs.head_sha }} | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Job 1: gate — autonomy kill-switch check | |
| # --------------------------------------------------------------------------- | |
| gate: | |
| name: Autonomy gate | |
| uses: ./.github/workflows/autonomy-check.yml | |
| with: | |
| requested_action: claude-invoke | |
| bypass_for_human_dispatch: ${{ github.event_name == 'workflow_dispatch' }} | |
| # --------------------------------------------------------------------------- | |
| # Job 2: review-1 — Reviewer 1 (Opus, temperature 0.0, diff only) | |
| # Runs independently of review-2; no output shared until merge-verdict. | |
| # --------------------------------------------------------------------------- | |
| review-1: | |
| name: Reviewer 1 (Opus, temp=0.0, diff-only) | |
| needs: gate | |
| if: needs.gate.outputs.proceed == 'true' | |
| runs-on: ubuntu-latest | |
| environment: bot-credentials | |
| outputs: | |
| verdict_file: /tmp/review-1.json | |
| steps: | |
| - name: Generate GH App token | |
| id: app-token | |
| uses: tibdex/github-app-token@v2 | |
| with: | |
| app_id: ${{ secrets.GH_APP_ID }} | |
| private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Run review-1 (Claude Opus — prompt pr-review-1.md) | |
| id: review | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| claude_args: >- | |
| --prompt-file .if-fork/prompts/pr-review-1.md | |
| --max-turns 12 | |
| --model claude-opus-4-7 | |
| --allowedTools Bash,Read,Grep,mcp__github__pull_request_read,mcp__github__get_file_contents | |
| --disallowedTools Edit,Write,mcp__github__create_pull_request,mcp__github__push_files | |
| github_token: ${{ steps.app-token.outputs.token }} | |
| env: | |
| PR_NUMBER: ${{ env.PR_NUMBER }} | |
| HEAD_SHA: ${{ env.HEAD_SHA }} | |
| VERDICT_OUTPUT_PATH: /tmp/review-1.json | |
| REVIEW_TEMPERATURE: "0.0" | |
| - name: Emit review_1 ledger event | |
| run: | | |
| python tools/ledger-event.py \ | |
| --event review_1 \ | |
| --pr-number "${{ env.PR_NUMBER }}" \ | |
| --head-sha "${{ env.HEAD_SHA }}" \ | |
| --actor pr-review \ | |
| --details-json '{"verdict_file": "/tmp/review-1.json", "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' \ | |
| --push | |
| env: | |
| GIT_AUTHOR_NAME: "Claude (Initial Force WPF Bot)" | |
| GIT_AUTHOR_EMAIL: "wpf-bot@initialforce.com" | |
| IF_FORK_ALLOW_UNSIGNED_LEDGER: "true" # phase-0: GPG key not yet provisioned for the bot identity | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| - name: Upload review-1 artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: review-1.json | |
| path: /tmp/review-1.json | |
| retention-days: 30 | |
| # --------------------------------------------------------------------------- | |
| # Job 3: review-2 — Reviewer 2 (Opus, temperature 0.7, diff + source context) | |
| # Runs INDEPENDENTLY from review-1 (both need only gate, not each other). | |
| # Skipped when IF_REVIEW_DOUBLE_REQUIRED is "false" (single-review fast path). | |
| # The fast path additionally requires a manual approval via environment: branch-promotion. | |
| # --------------------------------------------------------------------------- | |
| review-2: | |
| name: Reviewer 2 (Opus, temp=0.7, diff+context, isolated) | |
| needs: gate | |
| # NOTE: review-2 intentionally does NOT need review-1. | |
| # Independence is the core security property: two reviewers with no shared | |
| # state before merge-verdict collapses the results. | |
| # review-2 is skipped when IF_REVIEW_DOUBLE_REQUIRED == "false" (emergency | |
| # single-review fast path). A warning is emitted via merge-verdict in that case. | |
| # Default (unset) = require two reviews. | |
| if: needs.gate.outputs.proceed == 'true' && vars.IF_REVIEW_DOUBLE_REQUIRED != 'false' | |
| runs-on: ubuntu-latest | |
| environment: bot-credentials | |
| outputs: | |
| verdict_file: /tmp/review-2.json | |
| steps: | |
| - name: Generate GH App token | |
| id: app-token | |
| uses: tibdex/github-app-token@v2 | |
| with: | |
| app_id: ${{ secrets.GH_APP_ID }} | |
| private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Run review-2 (Claude Opus — prompt pr-review-2.md, different from review-1) | |
| id: review | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| claude_args: >- | |
| --prompt-file .if-fork/prompts/pr-review-2.md | |
| --max-turns 12 | |
| --model claude-opus-4-7 | |
| --allowedTools Bash,Read,Grep,mcp__github__pull_request_read,mcp__github__get_file_contents | |
| --disallowedTools Edit,Write,mcp__github__create_pull_request,mcp__github__push_files | |
| github_token: ${{ steps.app-token.outputs.token }} | |
| env: | |
| PR_NUMBER: ${{ env.PR_NUMBER }} | |
| HEAD_SHA: ${{ env.HEAD_SHA }} | |
| VERDICT_OUTPUT_PATH: /tmp/review-2.json | |
| REVIEW_TEMPERATURE: "0.7" | |
| - name: Emit review_2 ledger event | |
| run: | | |
| python tools/ledger-event.py \ | |
| --event review_2 \ | |
| --pr-number "${{ env.PR_NUMBER }}" \ | |
| --head-sha "${{ env.HEAD_SHA }}" \ | |
| --actor pr-review \ | |
| --details-json '{"verdict_file": "/tmp/review-2.json", "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' \ | |
| --push | |
| env: | |
| GIT_AUTHOR_NAME: "Claude (Initial Force WPF Bot)" | |
| GIT_AUTHOR_EMAIL: "wpf-bot@initialforce.com" | |
| IF_FORK_ALLOW_UNSIGNED_LEDGER: "true" # phase-0: GPG key not yet provisioned for the bot identity | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| - name: Upload review-2 artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: review-2.json | |
| path: /tmp/review-2.json | |
| retention-days: 30 | |
| # --------------------------------------------------------------------------- | |
| # Job 4: single-review-approval — manual gate when fast path is active | |
| # Required only when IF_REVIEW_DOUBLE_REQUIRED == "false". | |
| # environment: branch-promotion enforces a human approver before merge-verdict runs. | |
| # --------------------------------------------------------------------------- | |
| single-review-approval: | |
| name: Manual approval (single-review fast path) | |
| needs: [gate, review-1] | |
| if: needs.gate.outputs.proceed == 'true' && vars.IF_REVIEW_DOUBLE_REQUIRED == 'false' | |
| runs-on: ubuntu-latest | |
| environment: branch-promotion | |
| steps: | |
| - name: Generate GH App token | |
| id: app-token | |
| uses: tibdex/github-app-token@v2 | |
| with: | |
| app_id: ${{ secrets.GH_APP_ID }} | |
| private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Warn and record single-review fast path | |
| run: | | |
| echo "::warning::IF_REVIEW_DOUBLE_REQUIRED is 'false' — single-review fast path active. This bypasses the two-reviewer independence guarantee. Use only in emergencies." | |
| python tools/ledger-event.py \ | |
| --event review_single_path_warning \ | |
| --pr-number "${{ env.PR_NUMBER }}" \ | |
| --head-sha "${{ env.HEAD_SHA }}" \ | |
| --actor pr-review \ | |
| --details-json '{"reason": "IF_REVIEW_DOUBLE_REQUIRED_false", "fast_path": true, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' \ | |
| --push | |
| env: | |
| GIT_AUTHOR_NAME: "Claude (Initial Force WPF Bot)" | |
| GIT_AUTHOR_EMAIL: "wpf-bot@initialforce.com" | |
| IF_FORK_ALLOW_UNSIGNED_LEDGER: "true" # phase-0: GPG key not yet provisioned for the bot identity | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| # --------------------------------------------------------------------------- | |
| # Job 5: merge-verdict — Collapse both reviewer outputs into a final decision | |
| # Runs after BOTH review-1 and review-2 complete (or review-2 is skipped and | |
| # single-review-approval has been manually approved). | |
| # --------------------------------------------------------------------------- | |
| merge-verdict: | |
| name: Merge verdicts and emit outcome | |
| needs: [gate, review-1, review-2, single-review-approval] | |
| # Run even when review-2 / single-review-approval was skipped, but only when | |
| # gate proceeded successfully and review-1 ran to success or failure. | |
| # If single-review-approval ran (fast path active), it must have succeeded or been skipped. | |
| if: >- | |
| always() && | |
| needs.gate.outputs.proceed == 'true' && | |
| needs.review-1.result != 'cancelled' && | |
| (needs.review-1.result == 'success' || needs.review-1.result == 'failure') && | |
| (needs.single-review-approval.result == 'success' || needs.single-review-approval.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| environment: bot-credentials | |
| steps: | |
| - name: Generate GH App token | |
| id: app-token | |
| uses: tibdex/github-app-token@v2 | |
| with: | |
| app_id: ${{ secrets.GH_APP_ID }} | |
| private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Download review artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: "review-*.json" | |
| merge-multiple: true | |
| path: /tmp/verdicts/ | |
| - name: Merge verdicts | |
| id: merge | |
| run: | | |
| python tools/merge-verdicts.py \ | |
| --review-1 /tmp/verdicts/review-1.json \ | |
| --review-2 /tmp/verdicts/review-2.json \ | |
| --pr-number "${{ env.PR_NUMBER }}" \ | |
| --head-sha "${{ env.HEAD_SHA }}" \ | |
| --output /tmp/merged-verdict.json | |
| env: | |
| GIT_AUTHOR_NAME: "Claude (Initial Force WPF Bot)" | |
| GIT_AUTHOR_EMAIL: "wpf-bot@initialforce.com" | |
| IF_FORK_ALLOW_UNSIGNED_LEDGER: "true" # phase-0: GPG key not yet provisioned for the bot identity | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| - name: Dispatch approved PRs for ingestion | |
| if: steps.merge.outcome == 'success' | |
| run: | | |
| python tools/dispatch-approved.py \ | |
| --verdict /tmp/merged-verdict.json \ | |
| --repo "${{ github.repository }}" \ | |
| --token-env GH_TOKEN | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| - name: Open review-disagreement issue if escalated | |
| if: steps.merge.outcome == 'success' | |
| run: | | |
| VERDICT=$(python -c "import json,sys; d=json.load(open('/tmp/merged-verdict.json')); print(d.get('verdict','unknown'))") | |
| if [ "$VERDICT" = "escalated" ]; then | |
| gh issue create \ | |
| --title "review-disagreement: PR #${{ env.PR_NUMBER }} requires human review" \ | |
| --body "$(python -c " | |
| import json | |
| d = json.load(open('/tmp/merged-verdict.json')) | |
| print(f'## Review disagreement or escalation\n\n') | |
| print(f'**PR:** #{d.get(\"pr_number\", \"?\")} \n') | |
| print(f'**Verdict:** {d.get(\"verdict\", \"unknown\")}\n') | |
| print(f'**Reason:** {d.get(\"reason\", \"see verdicts\")} \n\n') | |
| print(f'**Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n') | |
| print(f'\nThis issue was opened automatically by the PR review workflow. A human must resolve the disagreement before this PR can be merged.') | |
| ")" \ | |
| --label "review-disagreement,needs-human" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| - name: Emit merged_verdict ledger event | |
| run: | | |
| VERDICT=$(python -c "import json,sys; d=json.load(open('/tmp/merged-verdict.json')); print(d.get('verdict','unknown'))" 2>/dev/null || echo "unknown") | |
| python tools/ledger-event.py \ | |
| --event merged_verdict \ | |
| --pr-number "${{ env.PR_NUMBER }}" \ | |
| --head-sha "${{ env.HEAD_SHA }}" \ | |
| --actor pr-review \ | |
| --details-json "{\"verdict\": \"$VERDICT\", \"run_url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ | |
| --push | |
| env: | |
| GIT_AUTHOR_NAME: "Claude (Initial Force WPF Bot)" | |
| GIT_AUTHOR_EMAIL: "wpf-bot@initialforce.com" | |
| IF_FORK_ALLOW_UNSIGNED_LEDGER: "true" # phase-0: GPG key not yet provisioned for the bot identity | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} |