Add fallback AI provider support to code review workflow #2
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
| name: AI Code Review | |
| on: | |
| pull_request: | |
| types: [opened, synchronize] | |
| workflow_dispatch: | |
| inputs: | |
| ai_provider: | |
| description: 'Primary AI provider for this run' | |
| required: false | |
| default: 'claude' | |
| type: choice | |
| options: | |
| - claude | |
| - openai | |
| jobs: | |
| review: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| # Priority: manual dispatch input > repo variable (vars.AI_PROVIDER) > default 'claude' | |
| env: | |
| AI_PROVIDER: ${{ github.event_name == 'workflow_dispatch' && inputs.ai_provider || vars.AI_PROVIDER || 'claude' }} | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get PR diff | |
| run: | | |
| BASE_REF="${{ github.base_ref || 'main' }}" | |
| git fetch origin "$BASE_REF" | |
| git diff "origin/$BASE_REF" > diff.txt | |
| - name: Build prompt | |
| env: | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| run: | | |
| printf '%s\n' "$PR_BODY" > pr_body.txt | |
| python3 -c " | |
| template = open('.github/prompt-template.txt').read() | |
| rules = open('.github/review-guidelines.md').read() | |
| pr_body = open('pr_body.txt').read() | |
| diff = open('diff.txt').read() | |
| open('prompt.txt', 'w').write(template.format(rules=rules, pr_body=pr_body, diff=diff)) | |
| " | |
| # Tries the configured primary provider. continue-on-error allows the | |
| # fallback step to run if this fails (bad key, quota, outage, etc.) | |
| - name: Call primary provider | |
| id: primary | |
| continue-on-error: true | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| run: | | |
| if [ "$AI_PROVIDER" = "claude" ]; then | |
| PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ | |
| '{model:"claude-sonnet-4-6",max_tokens:1024,messages:[{role:"user",content:$prompt}]}') | |
| RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \ | |
| -H "x-api-key: $ANTHROPIC_API_KEY" \ | |
| -H "anthropic-version: 2023-06-01" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD") | |
| REVIEW=$(echo "$RESPONSE" | jq -re '.content[0].text') | |
| USED="Claude Sonnet 4.6 (Anthropic)" | |
| else | |
| PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ | |
| '{model:"gpt-4o-mini",temperature:0.3,messages:[{role:"user",content:$prompt}]}') | |
| RESPONSE=$(curl -sf https://api.openai.com/v1/chat/completions \ | |
| -H "Authorization: Bearer $OPENAI_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD") | |
| REVIEW=$(echo "$RESPONSE" | jq -re '.choices[0].message.content') | |
| USED="GPT-4o-mini (OpenAI)" | |
| fi | |
| echo "REVIEW<<EOF" >> "$GITHUB_OUTPUT" | |
| printf '%s\n' "$REVIEW" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| echo "USED=$USED" >> "$GITHUB_OUTPUT" | |
| # Only runs if the primary provider failed — automatically uses the other provider. | |
| - name: Call fallback provider | |
| id: fallback | |
| if: steps.primary.outcome == 'failure' | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| run: | | |
| echo "::warning::Primary provider ($AI_PROVIDER) failed — switching to fallback." | |
| if [ "$AI_PROVIDER" = "claude" ]; then | |
| PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ | |
| '{model:"gpt-4o-mini",temperature:0.3,messages:[{role:"user",content:$prompt}]}') | |
| RESPONSE=$(curl -sf https://api.openai.com/v1/chat/completions \ | |
| -H "Authorization: Bearer $OPENAI_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD") || { echo "::error::Both Claude and OpenAI failed."; exit 1; } | |
| REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ("OpenAI error: " + (.error.message // "unknown"))') | |
| USED="GPT-4o-mini (OpenAI) ⚠️ fallback from Claude" | |
| else | |
| PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ | |
| '{model:"claude-sonnet-4-6",max_tokens:1024,messages:[{role:"user",content:$prompt}]}') | |
| RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \ | |
| -H "x-api-key: $ANTHROPIC_API_KEY" \ | |
| -H "anthropic-version: 2023-06-01" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD") || { echo "::error::Both OpenAI and Claude failed."; exit 1; } | |
| REVIEW=$(echo "$RESPONSE" | jq -r '.content[0].text // ("Claude error: " + (.error.message // "unknown"))') | |
| USED="Claude Sonnet 4.6 (Anthropic) ⚠️ fallback from OpenAI" | |
| fi | |
| echo "REVIEW<<EOF" >> "$GITHUB_OUTPUT" | |
| printf '%s\n' "$REVIEW" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| echo "USED=$USED" >> "$GITHUB_OUTPUT" | |
| - name: Post review comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| env: | |
| PRIMARY_REVIEW: ${{ steps.primary.outputs.REVIEW }} | |
| PRIMARY_USED: ${{ steps.primary.outputs.USED }} | |
| FALLBACK_REVIEW: ${{ steps.fallback.outputs.REVIEW }} | |
| FALLBACK_USED: ${{ steps.fallback.outputs.USED }} | |
| with: | |
| script: | | |
| const review = process.env.PRIMARY_REVIEW || process.env.FALLBACK_REVIEW; | |
| const label = process.env.PRIMARY_USED || process.env.FALLBACK_USED; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `## 🤖 AI Code Review — ${label}\n\n${review}` | |
| }); | |
| - name: Print review (manual run) | |
| if: github.event_name == 'workflow_dispatch' | |
| env: | |
| PRIMARY_REVIEW: ${{ steps.primary.outputs.REVIEW }} | |
| PRIMARY_USED: ${{ steps.primary.outputs.USED }} | |
| FALLBACK_REVIEW: ${{ steps.fallback.outputs.REVIEW }} | |
| FALLBACK_USED: ${{ steps.fallback.outputs.USED }} | |
| run: | | |
| LABEL="${PRIMARY_USED:-$FALLBACK_USED}" | |
| REVIEW="${PRIMARY_REVIEW:-$FALLBACK_REVIEW}" | |
| echo "=== AI Review — $LABEL ===" | |
| printf '%s\n' "$REVIEW" |