|
| 1 | +name: AI PR Review |
| 2 | + |
| 3 | +on: |
| 4 | + pull_request: |
| 5 | + types: [opened, synchronize, reopened] |
| 6 | + |
| 7 | +permissions: |
| 8 | + contents: read |
| 9 | + pull-requests: write |
| 10 | + models: read |
| 11 | + |
| 12 | +jobs: |
| 13 | + review: |
| 14 | + runs-on: ubuntu-latest |
| 15 | + steps: |
| 16 | + - uses: actions/checkout@v5 |
| 17 | + with: |
| 18 | + fetch-depth: 0 |
| 19 | + |
| 20 | + - name: Get PR diff |
| 21 | + id: diff |
| 22 | + env: |
| 23 | + GH_TOKEN: ${{ github.token }} |
| 24 | + 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" |
| 30 | +
|
| 31 | + - name: Get PR info |
| 32 | + id: pr |
| 33 | + env: |
| 34 | + GH_TOKEN: ${{ github.token }} |
| 35 | + run: | |
| 36 | + gh pr view ${{ github.event.pull_request.number }} --json title,body > /tmp/pr-info.json |
| 37 | +
|
| 38 | + - name: Check for relevant proposals |
| 39 | + id: proposals |
| 40 | + 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) |
| 54 | + if [ -f "CLAUDE.md" ]; then |
| 55 | + head -100 CLAUDE.md > /tmp/project-context.txt |
| 56 | + else |
| 57 | + echo "No CLAUDE.md found" > /tmp/project-context.txt |
| 58 | + fi |
| 59 | +
|
| 60 | + - name: AI Review |
| 61 | + id: review |
| 62 | + env: |
| 63 | + GITHUB_TOKEN: ${{ github.token }} |
| 64 | + 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 | +
|
| 72 | + TRUNCATION_NOTE="" |
| 73 | + 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." |
| 75 | + fi |
| 76 | +
|
| 77 | + # Call GitHub Models API |
| 78 | + RESPONSE=$(curl -s -X POST \ |
| 79 | + -H "Authorization: Bearer $GITHUB_TOKEN" \ |
| 80 | + -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') |
| 106 | +
|
| 107 | + if [ -z "$REVIEW" ]; then |
| 108 | + echo "::warning::AI review failed. Response: $(echo "$RESPONSE" | head -c 500)" |
| 109 | + exit 0 |
| 110 | + fi |
| 111 | +
|
| 112 | + # Post as PR comment |
| 113 | + gh pr comment ${{ github.event.pull_request.number }} --body "$(cat <<EOF |
| 114 | + ## AI Code Review |
| 115 | +
|
| 116 | + $REVIEW |
| 117 | +
|
| 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 | + )" |
| 122 | +
|
0 commit comments