diff --git a/.github/workflows/ai-code-review.yml b/.github/workflows/ai-code-review.yml new file mode 100644 index 0000000..f2154c2 --- /dev/null +++ b/.github/workflows/ai-code-review.yml @@ -0,0 +1,143 @@ +name: AI Code Review + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + pull-requests: write + contents: read + +jobs: + code-review: + runs-on: ubuntu-latest + steps: + - name: Checkout PR code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get PR diff + id: diff + run: | + git diff origin/${{ github.base_ref }}...HEAD > pr.diff + MAX_SIZE=50000 + if [ $(wc -c < pr.diff) -gt $MAX_SIZE ]; then + head -c $MAX_SIZE pr.diff > pr_truncated.diff + mv pr_truncated.diff pr.diff + echo "warning=Diff truncated to ${MAX_SIZE} bytes" >> $GITHUB_OUTPUT + fi + FILES_CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | wc -l) + echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT + + - name: Code Review + id: review + env: + API_KEY: ${{ secrets.AI_PROVIDER_API_KEY }} + API_ENDPOINT: ${{ secrets.AI_PROVIDER_API_ENDPOINT }} + MODEL: ${{ secrets.AI_PROVIDER_MODEL }} + run: | + set +x # Disable verbose mode to prevent secret leakage in logs + DIFF=$(cat pr.diff | jq -Rs .) + + # Retry logic with exponential backoff + MAX_RETRIES=3 + RETRY_COUNT=0 + HTTP_CODE=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + HTTP_CODE=$(curl -s -o response.json -w "%{http_code}" $API_ENDPOINT \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept-Language: en-US,en" \ + -d "{ + \"model\": \"$MODEL\", + \"messages\": [ + { + \"role\": \"system\", + \"content\": \"You are an expert code reviewer. Analyze the PR diff and provide:\\n\\n1. **Summary**: Brief overview\\n2. **Issues**: Bugs or problems (if any)\\n3. **Security**: Security concerns (if any)\\n4. **Performance**: Performance considerations (if any)\\n5. **Best Practices**: Swift 6, concurrency, architecture (if any)\\n6. **Suggestions**: Optional improvements\\n\\nBe concise but thorough. Use markdown formatting.\" + }, + { + \"role\": \"user\", + \"content\": ${DIFF} + } + ], + \"temperature\": 0.3, + \"max_tokens\": 4096 + }") + + if [ "$HTTP_CODE" -eq 200 ]; then + break + fi + + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then + WAIT_TIME=$((2 ** RETRY_COUNT)) + echo "API returned $HTTP_CODE. Retrying in ${WAIT_TIME}s... (Attempt $RETRY_COUNT/$MAX_RETRIES)" + sleep $WAIT_TIME + fi + done + + if [ "$HTTP_CODE" -ne 200 ]; then + echo "Error: API returned $HTTP_CODE after $MAX_RETRIES attempts" + cat response.json + exit 1 + fi + + RESPONSE=$(cat response.json) + REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content') + + # Check if review was extracted successfully + if [ -z "$REVIEW" ] || [ "$REVIEW" == "null" ]; then + echo "Error: Failed to extract review content from API response" + echo "Response: $RESPONSE" + exit 1 + fi + + echo "$REVIEW" > review.md + + cat > review_with_header.md << EOF + ### 🧠 AI Code Review + + $REVIEW + EOF + + if [ -n "${{ steps.diff.outputs.warning }}" ]; then + echo "" >> review_with_header.md + echo "⚠️ **Note**: ${{ steps.diff.outputs.warning }}" >> review_with_header.md + fi + + - name: Post review as PR comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const review = fs.readFileSync('review_with_header.md', 'utf8'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('AI Code Review') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: review + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: review + }); + }