-
Notifications
You must be signed in to change notification settings - Fork 0
149 lines (140 loc) · 6.38 KB
/
ai-review.yml
File metadata and controls
149 lines (140 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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"