Skip to content

Commit d13743f

Browse files
akoclaude
andcommitted
Fix AI review workflow: fork permissions, payload size, comment posting
- Use pull_request_target for write permissions on fork PRs - Use file-based jq input (--rawfile) instead of shell variables to avoid arg limits on large diffs - Use gh pr comment --body-file instead of heredoc to fix formatting - Add HTTP status code logging for API call debugging - Split into separate steps for clearer workflow logs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 999527e commit d13743f

1 file changed

Lines changed: 119 additions & 72 deletions

File tree

.github/workflows/ai-review.yml

Lines changed: 119 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: AI PR Review
22

33
on:
4-
pull_request:
4+
pull_request_target:
55
types: [opened, synchronize, reopened]
66

77
permissions:
@@ -15,108 +15,155 @@ jobs:
1515
steps:
1616
- uses: actions/checkout@v5
1717
with:
18-
fetch-depth: 0
18+
# Checkout base repo (not PR head) for safe access to CLAUDE.md and proposals.
19+
# The diff is fetched via GitHub API, not from the PR code.
20+
ref: ${{ github.event.pull_request.base.sha }}
21+
fetch-depth: 1
1922

2023
- name: Get PR diff
2124
id: diff
2225
env:
2326
GH_TOKEN: ${{ github.token }}
2427
run: |
25-
gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr.diff
26-
# Truncate to ~100k chars to fit model context
27-
head -c 100000 /tmp/pr.diff > /tmp/pr-truncated.diff
28-
echo "diff_size=$(wc -c < /tmp/pr.diff)" >> "$GITHUB_OUTPUT"
29-
echo "truncated=$([ $(wc -c < /tmp/pr.diff) -gt 100000 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
28+
gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-full.diff
29+
FULL_SIZE=$(wc -c < /tmp/pr-full.diff)
30+
echo "diff_size=$FULL_SIZE" >> "$GITHUB_OUTPUT"
31+
32+
# Truncate to ~80k chars to stay within API limits
33+
if [ "$FULL_SIZE" -gt 80000 ]; then
34+
head -c 80000 /tmp/pr-full.diff > /tmp/pr.diff
35+
echo "truncated=true" >> "$GITHUB_OUTPUT"
36+
else
37+
cp /tmp/pr-full.diff /tmp/pr.diff
38+
echo "truncated=false" >> "$GITHUB_OUTPUT"
39+
fi
3040
3141
- name: Get PR info
32-
id: pr
3342
env:
3443
GH_TOKEN: ${{ github.token }}
3544
run: |
3645
gh pr view ${{ github.event.pull_request.number }} --json title,body > /tmp/pr-info.json
3746
38-
- name: Check for relevant proposals
39-
id: proposals
47+
- name: Gather project context
4048
run: |
41-
# Extract key terms from PR title for proposal matching
42-
PROPOSALS=""
43-
if [ -d "docs/03-development/proposals" ]; then
44-
PROPOSALS=$(ls docs/03-development/proposals/ 2>/dev/null | head -20)
45-
fi
46-
echo "list<<EOF" >> "$GITHUB_OUTPUT"
47-
echo "$PROPOSALS" >> "$GITHUB_OUTPUT"
48-
echo "EOF" >> "$GITHUB_OUTPUT"
49-
50-
- name: Read project context
51-
id: context
52-
run: |
53-
# Read CLAUDE.md summary (first 100 lines for context)
49+
# Read CLAUDE.md summary (first 100 lines)
5450
if [ -f "CLAUDE.md" ]; then
5551
head -100 CLAUDE.md > /tmp/project-context.txt
5652
else
57-
echo "No CLAUDE.md found" > /tmp/project-context.txt
53+
echo "No CLAUDE.md found." > /tmp/project-context.txt
5854
fi
5955
60-
- name: AI Review
61-
id: review
56+
# List proposals
57+
if [ -d "docs/03-development/proposals" ]; then
58+
ls docs/03-development/proposals/ 2>/dev/null | head -20 > /tmp/proposals.txt
59+
else
60+
echo "No proposals directory." > /tmp/proposals.txt
61+
fi
62+
63+
- name: Build API request
6264
env:
63-
GITHUB_TOKEN: ${{ github.token }}
65+
TRUNCATED: ${{ steps.diff.outputs.truncated }}
66+
DIFF_SIZE: ${{ steps.diff.outputs.diff_size }}
6467
run: |
65-
# Build the review prompt
66-
DIFF=$(cat /tmp/pr-truncated.diff)
67-
PR_INFO=$(cat /tmp/pr-info.json)
68-
PROJECT_CONTEXT=$(cat /tmp/project-context.txt)
69-
PROPOSALS="${{ steps.proposals.outputs.list }}"
70-
TRUNCATED="${{ steps.diff.outputs.truncated }}"
71-
7268
TRUNCATION_NOTE=""
7369
if [ "$TRUNCATED" = "true" ]; then
74-
TRUNCATION_NOTE="NOTE: The diff was truncated to 100k characters. Total size: ${{ steps.diff.outputs.diff_size }} bytes. Focus on what's visible."
70+
TRUNCATION_NOTE="NOTE: The diff was truncated to 80k characters. Total size: ${DIFF_SIZE} bytes. Focus on what is visible."
7571
fi
7672
77-
# Call GitHub Models API
78-
RESPONSE=$(curl -s -X POST \
73+
# Write the user prompt to a file (avoids shell variable limits)
74+
cat > /tmp/user-prompt.txt <<PROMPT
75+
Review this pull request.
76+
77+
PR Info:
78+
$(cat /tmp/pr-info.json)
79+
80+
Project context:
81+
$(cat /tmp/project-context.txt)
82+
83+
Proposals in repo:
84+
$(cat /tmp/proposals.txt)
85+
86+
${TRUNCATION_NOTE}
87+
88+
Focus on:
89+
1. Bugs, logic errors, race conditions, resource leaks
90+
2. Error handling gaps
91+
3. Security concerns (command injection, temp files, predictable paths)
92+
4. Scope — flag if PR bundles unrelated changes
93+
5. If proposals exist that match the PR topic, check alignment
94+
6. Check if PR claims to fix already-fixed issues
95+
96+
Structure your review as: Critical Issues, Moderate Issues, Minor Issues, What Looks Good, Recommendation (approve/request changes).
97+
98+
Diff:
99+
$(cat /tmp/pr.diff)
100+
PROMPT
101+
102+
# Build JSON payload using jq with file input (no shell variable limits)
103+
jq -n --rawfile prompt /tmp/user-prompt.txt '{
104+
model: "openai/gpt-4.1",
105+
messages: [
106+
{
107+
role: "system",
108+
content: "You are a code reviewer for a Go CLI project (mxcli) that reads/modifies Mendix application projects. Key patterns: ANTLR4 grammar → AST → visitor → executor → BSON writer. Generated ANTLR parser files (mdl/grammar/parser/) are noise — note but skip. Review thoroughly but concisely."
109+
},
110+
{
111+
role: "user",
112+
content: $prompt
113+
}
114+
],
115+
max_tokens: 4000
116+
}' > /tmp/request.json
117+
118+
echo "Request payload size: $(wc -c < /tmp/request.json) bytes"
119+
120+
- name: Call GitHub Models API
121+
env:
122+
GITHUB_TOKEN: ${{ github.token }}
123+
run: |
124+
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/response.json -X POST \
79125
-H "Authorization: Bearer $GITHUB_TOKEN" \
80126
-H "Content-Type: application/json" \
81-
https://models.github.ai/inference/chat/completions \
82-
-d "$(jq -n \
83-
--arg diff "$DIFF" \
84-
--arg pr_info "$PR_INFO" \
85-
--arg context "$PROJECT_CONTEXT" \
86-
--arg proposals "$PROPOSALS" \
87-
--arg truncation "$TRUNCATION_NOTE" \
88-
'{
89-
model: "openai/gpt-4.1",
90-
messages: [
91-
{
92-
role: "system",
93-
content: "You are a code reviewer for a Go CLI project (mxcli) that reads/modifies Mendix application projects. Key patterns: ANTLR4 grammar → AST → visitor → executor → BSON writer. Generated ANTLR parser files (mdl/grammar/parser/) are noise — note but skip. Review thoroughly but concisely."
94-
},
95-
{
96-
role: "user",
97-
content: ("Review this PR.\n\nPR Info:\n" + $pr_info + "\n\nProject context:\n" + $context + "\n\nProposals in repo:\n" + $proposals + "\n\n" + $truncation + "\n\nFocus on:\n1. Bugs, logic errors, race conditions, resource leaks\n2. Error handling gaps\n3. Security concerns (command injection, temp files, predictable paths)\n4. Scope — flag if PR bundles unrelated changes\n5. If proposals exist that match the PR topic, check alignment\n6. Check if PR claims to fix already-fixed issues\n\nStructure: Critical Issues, Moderate Issues, Minor Issues, What Looks Good, Recommendation (approve/request changes).\n\nDiff:\n```\n" + $diff + "\n```")
98-
}
99-
],
100-
max_tokens: 4000
101-
}'
102-
)")
103-
104-
# Extract the review text
105-
REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty')
127+
-d @/tmp/request.json \
128+
https://models.github.ai/inference/chat/completions)
106129
107-
if [ -z "$REVIEW" ]; then
108-
echo "::warning::AI review failed. Response: $(echo "$RESPONSE" | head -c 500)"
130+
echo "HTTP status: $HTTP_CODE"
131+
132+
if [ "$HTTP_CODE" != "200" ]; then
133+
echo "::warning::GitHub Models API returned HTTP $HTTP_CODE"
134+
cat /tmp/response.json | head -c 1000
109135
exit 0
110136
fi
111137
112-
# Post as PR comment
113-
gh pr comment ${{ github.event.pull_request.number }} --body "$(cat <<EOF
114-
## AI Code Review
138+
REVIEW=$(jq -r '.choices[0].message.content // empty' /tmp/response.json)
115139
116-
$REVIEW
140+
if [ -z "$REVIEW" ]; then
141+
echo "::warning::AI review returned empty content. Response:"
142+
cat /tmp/response.json | head -c 1000
143+
exit 0
144+
fi
117145
118-
---
119-
*Automated review by GitHub Models API (GPT-4.1) — [workflow source](${{ github.server_url }}/${{ github.repository }}/blob/main/.github/workflows/ai-review.yml)*
120-
EOF
121-
)"
146+
# Save review for next step
147+
echo "$REVIEW" > /tmp/review.txt
148+
echo "Review generated ($(wc -c < /tmp/review.txt) bytes)"
149+
150+
- name: Post review comment
151+
env:
152+
GH_TOKEN: ${{ github.token }}
153+
run: |
154+
if [ ! -f /tmp/review.txt ]; then
155+
echo "No review to post."
156+
exit 0
157+
fi
122158
159+
# Build comment body
160+
{
161+
echo "## AI Code Review"
162+
echo ""
163+
cat /tmp/review.txt
164+
echo ""
165+
echo "---"
166+
echo "*Automated review by GitHub Models API (GPT-4.1) — [workflow source](${{ github.server_url }}/${{ github.repository }}/blob/main/.github/workflows/ai-review.yml)*"
167+
} > /tmp/comment.md
168+
169+
gh pr comment ${{ github.event.pull_request.number }} --body-file /tmp/comment.md

0 commit comments

Comments
 (0)