Skip to content

PR Review (2x Independent) #5

PR Review (2x Independent)

PR Review (2x Independent) #5

Workflow file for this run

# .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 }}