Skip to content

docs: Update AI.md to use docs/ subdirectories #30

docs: Update AI.md to use docs/ subdirectories

docs: Update AI.md to use docs/ subdirectories #30

Workflow file for this run

name: AI PR Review
# Centralized timeout configuration
env:
AI_EXECUTION_TIMEOUT_MINUTES: ${{ vars.AI_EXECUTION_TIMEOUT_MINUTES || '10' }}
# Triggers:
# - When a PR is first opened or moves from draft to ready for review
# - When ai-review-needed label is added to a PR
# - When someone comments "/review" on a PR
# Note: Removed 'synchronize' trigger to avoid running on every commit
# Note: Bot comments are explicitly filtered to prevent infinite loops
on:
pull_request:
types: [opened, labeled]
permissions:
contents: read
pull-requests: write
issues: write
checks: read
actions: read
# NOTE: This workflow uses GITHUB_TOKEN for GitHub API operations
# Uses declared permissions above for proper access control
concurrency:
group: ai-workflows-${{ github.event.pull_request.head.ref }}
cancel-in-progress: true
jobs:
review:
# Only run on PRs when opened, when ai-review-needed label is added, and not a draft, skip AI-generated branches
if: >
github.event_name == 'pull_request' &&
(github.event.action == 'opened' ||
(github.event.action == 'labeled' && github.event.label.name == 'ai-review-needed')) &&
github.event.pull_request.draft == false &&
!startsWith(github.head_ref, 'fix/ai-') &&
!startsWith(github.head_ref, 'feature/ai-task-')
runs-on: ubuntu-latest
steps:
- name: Get PR data when triggered by comment
if: github.event_name == 'issue_comment'
id: get_pr
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
const pr = await github.rest.pulls.get({
...context.repo,
pull_number: context.issue.number
});
// Set outputs for later steps
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('body', pr.data.body || '');
core.setOutput('draft', pr.data.draft);
// Store PR data for later use
const prData = {
base: { ref: pr.data.base.ref },
head: { ref: pr.data.head.ref, sha: pr.data.head.sha },
body: pr.data.body || '',
draft: pr.data.draft
};
// Export as environment variable for other steps
core.exportVariable('PR_DATA', JSON.stringify(prData));
- name: Check for recent AI review
id: check_recent_review
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
// Get recent comments
const comments = await github.rest.issues.listComments({
...context.repo,
issue_number: context.issue.number,
per_page: 100
});
// Check if there's a recent AI review (within last 5 minutes)
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const recentAIReview = comments.data.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('🤖 AI Review by Claude') &&
new Date(comment.created_at) > fiveMinutesAgo
);
if (recentAIReview) {
console.log('Recent AI review found, skipping to prevent spam');
core.setOutput('skip', 'true');
} else {
core.setOutput('skip', 'false');
}
- name: Check test status
id: check_tests
if: github.event_name == 'pull_request' && steps.check_recent_review.outputs.skip != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
// Get the latest commit SHA
const sha = context.payload.pull_request.head.sha;
// Get all check runs for this commit
const { data: checkRuns } = await github.rest.checks.listForRef({
...context.repo,
ref: sha
});
// Also get workflow runs for this commit
const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({
...context.repo,
head_sha: sha
});
// Look for test-related workflows or checks
const testChecks = checkRuns.check_runs.filter(check =>
check.name.toLowerCase().includes('test') ||
check.name.toLowerCase().includes('ci') ||
check.name.toLowerCase().includes('build')
);
const testWorkflows = workflowRuns.workflow_runs.filter(run =>
run.name.toLowerCase().includes('test') ||
run.name.toLowerCase().includes('ci') ||
run.name.toLowerCase().includes('build')
);
// Check if any tests are failing
const failedChecks = testChecks.filter(check => check.conclusion === 'failure');
const failedWorkflows = testWorkflows.filter(run => run.conclusion === 'failure');
const hasFailingTests = failedChecks.length > 0 || failedWorkflows.length > 0;
if (hasFailingTests) {
console.log('Tests are failing. AI review will be skipped.');
console.log('Failed checks:', failedChecks.map(c => c.name));
console.log('Failed workflows:', failedWorkflows.map(w => w.name));
// Create a comment explaining why review is skipped
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: `## 🚨 AI Review Skipped - Tests Failing\n\n` +
`The AI review has been skipped because one or more tests are currently failing.\n\n` +
`**Failed tests:**\n` +
`${failedChecks.map(c => `- ${c.name}: ${c.conclusion}`).join('\n')}\n` +
`${failedWorkflows.map(w => `- ${w.name}: ${w.conclusion}`).join('\n')}\n\n` +
`Please fix the failing tests first, then the AI review will run automatically.`
});
core.setOutput('result', 'false');
return false;
}
core.setOutput('result', 'true');
return true;
- name: Checkout code
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
uses: actions/checkout@v4
with:
# Fetch the PR branch - use get_pr outputs for comment triggers
ref: ${{ github.event.pull_request.head.sha || steps.get_pr.outputs.head_sha || github.event.pull_request.head.ref || steps.get_pr.outputs.head_ref }}
fetch-depth: 0
- name: Setup Python
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install OpenRouter dependencies
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
run: |
pip install openai==1.54.3 httpx==0.27.0
- name: Get PR diff
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
id: diff
run: |
# Get the base branch - use get_pr outputs for comment triggers
BASE_BRANCH="${{ github.event.pull_request.base.ref || steps.get_pr.outputs.base_ref || 'main' }}"
# Get the diff
git diff origin/${BASE_BRANCH}...HEAD > pr_diff.txt
# Also get list of changed files
git diff --name-only origin/${BASE_BRANCH}...HEAD > changed_files.txt
# Get PR description - use safe method to avoid shell interpretation
cat > pr_description.txt << 'EOF'
${{ github.event.pull_request.body || steps.get_pr.outputs.body || '' }}
EOF
- name: Validate input files
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
run: |
# Check if required files exist
if [ ! -f pr_diff.txt ]; then
echo "Error: pr_diff.txt not found"
exit 1
fi
if [ ! -f changed_files.txt ]; then
echo "Error: changed_files.txt not found"
exit 1
fi
if [ ! -f pr_description.txt ]; then
echo "Error: pr_description.txt not found"
exit 1
fi
echo "All required files found"
- name: Run AI Review via OpenRouter
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
id: ai_review
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} # pragma: allowlist secret
AI_MODEL: ${{ vars.AI_MODEL || 'anthropic/claude-sonnet-4' }}
run: | # pragma: allowlist secret
# Validate API key exists # pragma: allowlist secret
if [[ -z "$OPENROUTER_API_KEY" ]]; then # pragma: allowlist secret
echo "## ⚠️ AI Review Failed" > review_output.md
echo "OPENROUTER_API_KEY not configured in repository secrets." >> review_output.md
exit 0
fi
# Create review prompt
cat > review_prompt.txt << EOF
Please review this pull request and provide feedback. Focus on:
1. Code quality and best practices
2. Potential bugs or issues
3. Security concerns
4. Performance implications
5. Test coverage
6. Documentation updates needed
PR Description:
$(cat pr_description.txt)
Changed files:
$(cat changed_files.txt)
Diff (truncated to first 5000 lines if longer):
$(cat pr_diff.txt | head -5000)
Please provide a structured review with:
- Summary of changes
- Strengths of the implementation
- Issues or concerns (if any)
- Suggestions for improvement
- Overall recommendation (approve, request changes, or comment)
EOF
# Create OpenRouter API script
cat > ai_review.py << 'EOF'
import os
import sys
import json
from openai import OpenAI
def main():
api_key = os.environ.get('OPENROUTER_API_KEY')
model = os.environ.get('AI_MODEL', 'anthropic/claude-sonnet-4')
if not api_key:
print("## ⚠️ AI Review Failed")
print("OPENROUTER_API_KEY not configured.")
return 1
# Read the prompt
with open('review_prompt.txt', 'r') as f:
prompt = f.read()
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=api_key
)
try:
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": prompt
}
],
extra_headers={
"HTTP-Referer": "https://github.com",
"X-Title": "AI PR Review"
}
)
# Save the model used to a file
with open('review_model.txt', 'w') as f:
f.write(response.model)
print(response.choices[0].message.content)
return 0
except Exception as e:
print("## ⚠️ AI Review Failed")
print(f"Error: {str(e)}")
print("")
print("This could be due to:")
print("- API rate limiting")
print("- Large diff size")
print("- Temporary service issues")
print("")
print("Please retry the review later or request manual review.")
return 1
if __name__ == "__main__":
sys.exit(main())
EOF
# Run AI review with timeout
TIMEOUT_SECONDS=$((${AI_EXECUTION_TIMEOUT_MINUTES} * 60))
echo "🤖 Using $AI_MODEL via OpenRouter for PR review (timeout: ${AI_EXECUTION_TIMEOUT_MINUTES} minutes)..." # pragma: allowlist secret
if ! timeout $TIMEOUT_SECONDS python ai_review.py > review_output.md; then
echo "## ⚠️ AI Review Failed" > review_output.md
echo "AI review could not be completed (timeout or error)." >> review_output.md
echo "" >> review_output.md
echo "This could be due to:" >> review_output.md
echo "- API rate limiting" >> review_output.md
echo "- Large diff size" >> review_output.md
echo "- Temporary service issues" >> review_output.md
echo "" >> review_output.md
echo "Please retry the review later or request manual review." >> review_output.md
echo "ai_review_failed=true" >> $GITHUB_OUTPUT
else
echo "ai_review_failed=false" >> $GITHUB_OUTPUT
fi
- name: Post review comment
if: (steps.check_tests.outputs.result == 'true' || github.event_name != 'pull_request') && steps.check_recent_review.outputs.skip != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
const fs = require('fs');
const review = fs.readFileSync('review_output.md', 'utf8');
let model = 'unknown';
try {
model = fs.readFileSync('review_model.txt', 'utf8').trim();
} catch (e) {
console.log("Could not read model file, using 'unknown'.");
}
const comment = `## 🤖 AI Review\n\n${review}\n\n---\n` +
`*This review was automatically generated by \`${model}\` via OpenRouter. Please consider it as supplementary feedback alongside human review.*`;
// For PR events
if (context.eventName === 'pull_request') {
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
}
// For issue comments on PRs (when /review is used)
else if (context.eventName === 'issue_comment') {
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: comment
});
}
- name: Handle AI review failure
if: steps.ai_review.outputs.ai_review_failed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
// Add a label to indicate AI review failed (but not ai-review-needed to avoid loops)
await github.rest.issues.addLabels({
...context.repo,
issue_number: context.issue.number,
labels: ['ai-review-failed']
});
// Fail the workflow step to indicate the review failure
core.setFailed('AI review failed - manual review needed');
- name: Add labels based on review
if: >
github.event_name == 'pull_request' &&
steps.check_tests.outputs.result == 'true' &&
steps.check_recent_review.outputs.skip != 'true' &&
steps.ai_review.outputs.ai_review_failed != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
const fs = require('fs');
const review = fs.readFileSync('review_output.md', 'utf8').toLowerCase();
const labels = [];
// Helper function to check if a phrase indicates actual concern vs no concern
const hasActualConcern = (text, keywords) => {
for (const keyword of keywords) {
const regex = new RegExp(`(no|zero|minimal|none|without|negligible)\\s+${keyword}`, 'i');
if (regex.test(text)) {
// Found negation before the keyword, so no actual concern
return false;
}
if (text.includes(keyword)) {
// Found keyword without negation
return true;
}
}
return false;
};
// Add labels based on review content (avoid triggering AI fix workflows)
if (hasActualConcern(review, ['security concern', 'security issue', 'security vulnerability', 'security risk'])) {
labels.push('security-review-needed');
}
if (hasActualConcern(review, ['performance impact', 'performance implication', 'performance degradation', 'performance issue'])) {
labels.push('performance-impact');
}
if (hasActualConcern(review, ['breaking change', 'backward incompatible', 'backwards incompatible'])) {
labels.push('breaking-change');
}
if (review.includes('needs documentation') || review.includes('documentation needed') || review.includes('missing documentation')) {
labels.push('needs-docs');
}
labels.push('ai-reviewed');
// Filter out any labels that could trigger AI fix workflows to prevent loops
const filteredLabels = labels.filter(label =>
!label.startsWith('ai-fix-') &&
label !== 'ai-fix-lint' &&
label !== 'ai-fix-security' &&
label !== 'ai-fix-tests'
);
if (filteredLabels.length > 0) {
await github.rest.issues.addLabels({
...context.repo,
issue_number: context.issue.number,
labels: filteredLabels
});
}
- name: Remove ai-review-needed label
if: >
github.event.action == 'labeled' &&
github.event.label.name == 'ai-review-needed' &&
steps.check_tests.outputs.result == 'true' &&
steps.check_recent_review.outputs.skip != 'true' &&
steps.ai_review.outputs.ai_review_failed != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
await github.rest.issues.removeLabel({
...context.repo,
issue_number: context.issue.number,
name: 'ai-review-needed'
});