Website Guardian: 1 issue(s) found #990
Workflow file for this run
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 PR Auto-Review & Merge | ||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, ready_for_review] | ||
| workflow_dispatch: | ||
| inputs: | ||
| pr_number: | ||
| description: 'PR number to review' | ||
| required: true | ||
| jobs: | ||
| review: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha || github.ref }} | ||
| - name: Get PR details | ||
| id: pr | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| PR_NUMBER="${{ github.event.pull_request.number || inputs.pr_number }}" | ||
| if [ -z "$PR_NUMBER" ]; then | ||
| echo "No PR number" && exit 0 | ||
| fi | ||
| echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT | ||
| PR_DIFF=$(gh pr diff "$PR_NUMBER" 2>/dev/null | head -500) | ||
| PR_TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title' 2>/dev/null) | ||
| PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null) | ||
| PR_AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login' 2>/dev/null) | ||
| PR_FILES=$(gh pr view "$PR_NUMBER" --json files --jq '.files[].path' 2>/dev/null | head -20) | ||
| echo "$PR_DIFF" > /tmp/pr-diff.txt | ||
| echo "title<<EOF" >> $GITHUB_OUTPUT | ||
| echo "$PR_TITLE" >> $GITHUB_OUTPUT | ||
| echo "EOF" >> $GITHUB_OUTPUT | ||
| echo "author=$PR_AUTHOR" >> $GITHUB_OUTPUT | ||
| echo "files=$PR_FILES" >> $GITHUB_OUTPUT | ||
| - name: AI Code Review | ||
| id: ai_review | ||
| if: steps.pr.outputs.number | ||
| env: | ||
| OPENROUTER_KEY: ${{ secrets.OPENROUTER_API_KEY }} | ||
| GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| PR_NUM="${{ steps.pr.outputs.number }}" | ||
| PR_TITLE="${{ steps.pr.outputs.title }}" | ||
| PR_AUTHOR="${{ steps.pr.outputs.author }}" | ||
| PR_FILES="${{ steps.pr.outputs.files }}" | ||
| AI_MODEL="meta-llama/llama-3.2-3b-instruct:free" | ||
| if [ -z "$OPENROUTER_KEY" ]; then | ||
| echo "No OpenRouter key — cannot review" | ||
| exit 0 | ||
| fi | ||
| DIFF=$(cat /tmp/pr-diff.txt 2>/dev/null) | ||
| if [ -z "$DIFF" ]; then | ||
| echo "Empty diff — auto-approving" | ||
| gh pr review "$PR_NUM" --approve \ | ||
| --body "_🤖 AI Review (${AI_MODEL})_\n\n**Verdict:** ✅ APPROVE\n**Reason:** No code changes detected (empty diff)." 2>/dev/null || true | ||
| gh pr merge "$PR_NUM" --merge --auto --delete-branch 2>gh pr merge "$PR_NUM" --merge --auto 2>&11 || true | ||
| exit 0 | ||
| fi | ||
| echo "Reviewing PR #$PR_NUM by @$PR_AUTHOR: $PR_TITLE" | ||
| echo "Files changed: $PR_FILES" | ||
| # AI decides: approve, reject, or request changes | ||
| REVIEW_JSON=$(curl -s "https://openrouter.ai/api/v1/chat/completions" \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "Authorization: Bearer $OPENROUTER_KEY" \ | ||
| -H "HTTP-Referer: https://acreetionos.org" \ | ||
| -H "X-Title: AcreetionOS AI PR Reviewer" \ | ||
| -d "{ | ||
| \"model\": \"$AI_MODEL\", | ||
| \"messages\": [ | ||
| {\"role\": \"system\", \"content\": \"You are an AI code reviewer for AcreetionOS, an Arch Linux distro. Review PRs carefully.\n\nRules:\n- Check for: broken HTML, XSS vulnerabilities, exposed secrets, JS errors, broken links, typos\n- APPROVE if changes are safe and correct\n- REJECT if changes would break the site or introduce security issues\n- REQUEST_CHANGES for minor issues that need fixing\n\nRespond with valid JSON only:\n{\\\"verdict\\\": \\\"APPROVE|REJECT|REQUEST_CHANGES\\\", \\\"summary\\\": \\\"one-line reason\\\", \\\"details\\\": \\\"detailed explanation\\\"}\"}, | ||
| {\"role\": \"user\", \"content\": $(echo "PR Title: $PR_TITLE\nAuthor: @$PR_AUTHOR\nFiles: $PR_FILES\n\nDiff:\n$DIFF" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")} | ||
| ], | ||
| \"max_tokens\": 500 | ||
| }" 2>/dev/null) | ||
| # Save AI response to file for Python to read | ||
| printf '%s' "$REVIEW_JSON" > /tmp/ai-response.json | ||
| PARSE_AI=$(python3 << 'PYEOF' | ||
| import json, re, sys | ||
| try: | ||
| with open('/tmp/ai-response.json') as f: | ||
| raw = f.read().strip() | ||
| if not raw: | ||
| print("VERDICT=APPROVE") | ||
| print("SUMMARY=Empty AI response") | ||
| sys.exit(0) | ||
| # Try to parse as JSON | ||
| try: | ||
| data = json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| # Might be shell wrapping - try to extract JSON from the raw text | ||
| data = json.loads(json.dumps({"choices":[{"message":{"content":raw}}]})) | ||
| content = data.get('choices', [{}])[0].get('message', {}).get('content', '{}') | ||
| # Remove markdown code fences if present | ||
| content = re.sub(r'^```(?:json)?\s*', '', content.strip()) | ||
| content = re.sub(r'\s*```$', '', content) | ||
| # Try to find and parse JSON object in the content | ||
| json_match = re.search(r'\{[^{}]*\}', content) | ||
| if json_match: | ||
| result = json.loads(json_match.group()) | ||
| print(f"VERDICT={result.get('verdict', 'APPROVE')}") | ||
| print(f"SUMMARY={result.get('summary', 'No reason given')}") | ||
| print(f"DETAILS={result.get('details', '')}") | ||
| else: | ||
| verdict = 'APPROVE' | ||
| if 'REJECT' in content.upper(): verdict = 'REJECT' | ||
| elif 'REQUEST_CHANGES' in content.upper(): verdict = 'REQUEST_CHANGES' | ||
| print(f"VERDICT={verdict}") | ||
| print(f"SUMMARY={content[:200].strip()}") | ||
| except Exception as e: | ||
| print(f"VERDICT=APPROVE") | ||
| print(f"SUMMARY=AI parse fallback, error: {str(e)[:100]}") | ||
| PYEOF | ||
| ) | ||
| # Extract values from single Python run | ||
| VERDICT=$(echo "$PARSE_AI" | grep '^VERDICT=' | cut -d= -f2) | ||
| SUMMARY=$(echo "$PARSE_AI" | grep '^SUMMARY=' | cut -d= -f2-) | ||
| DETAILS=$(echo "$PARSE_AI" | grep '^DETAILS=' | cut -d= -f2-) | ||
| VERDICT="${VERDICT:-APPROVE}" | ||
| SUMMARY="${SUMMARY:-AI review completed}" | ||
| echo "AI Verdict: $VERDICT" | ||
| echo "AI Summary: $SUMMARY" | ||
| echo "model=$AI_MODEL" >> $GITHUB_OUTPUT | ||
| echo "verdict=$VERDICT" >> $GITHUB_OUTPUT | ||
| echo "summary=$SUMMARY" >> $GITHUB_OUTPUT | ||
| # Post review on the PR with AI attribution | ||
| COMMENT="_🤖 AI Review by \`${AI_MODEL}\`_\n\n**Verdict:** \`$VERDICT\`\n**Summary:** $SUMMARY" | ||
| if [ -n "$DETAILS" ]; then | ||
| COMMENT="$COMMENT\n\n**Details:**\n$DETAILS" | ||
| fi | ||
| COMMENT="$COMMENT\n\n---\n_This review was performed automatically by an AI system. The model used is $AI_MODEL._" | ||
| if [ "$VERDICT" = "APPROVE" ]; then | ||
| gh pr review "$PR_NUM" --approve --body "$(echo -e "$COMMENT")" 2>/dev/null || true | ||
| echo "✅ PR approved by AI" | ||
| # Auto-merge | ||
| gh pr merge "$PR_NUM" --merge --auto --delete-branch 2>gh pr merge "$PR_NUM" --merge --auto 2>&11 || true | ||
| elif [ "$VERDICT" = "REJECT" ]; then | ||
| gh pr review "$PR_NUM" --request-changes --body "$(echo -e "$COMMENT")" 2>/dev/null || true | ||
| echo "❌ PR rejected by AI" | ||
| else | ||
| gh pr review "$PR_NUM" --comment --body "$(echo -e "$COMMENT")" 2>/dev/null || true | ||
| echo "⏸ Changes requested by AI — pending review" | ||
| fi | ||
| - name: Log review for audit | ||
| if: always() | ||
| run: | | ||
| { | ||
| echo "## 🤖 AI PR Review Log" | ||
| echo "" | ||
| echo "**PR:** #${{ steps.pr.outputs.number }}" | ||
| echo "**Author:** @${{ steps.pr.outputs.author }}" | ||
| echo "**Title:** ${{ steps.pr.outputs.title }}" | ||
| echo "**Review Model:** ${{ steps.ai_review.outputs.model }}" | ||
| echo "**Verdict:** ${{ steps.ai_review.outputs.verdict }}" | ||
| echo "**Summary:** ${{ steps.ai_review.outputs.summary }}" | ||
| echo "**Reviewer:** AI System (autonomous)" | ||
| echo "**Timestamp:** $(date -u '+%Y-%m-%dT%H:%M:%SZ')" | ||
| } >> $GITHUB_STEP_SUMMARY | ||