diff --git a/.github/workflows/ai-review-reusable.yml b/.github/workflows/ai-review-reusable.yml new file mode 100644 index 0000000..b097194 --- /dev/null +++ b/.github/workflows/ai-review-reusable.yml @@ -0,0 +1,183 @@ +# Reusable AI Code Review Workflow +# Called by other repos via: uses: BLEND360/code-sage-code-review/.github/workflows/ai-review-reusable.yml@main + +name: AI Code Review (Reusable) + +on: + workflow_call: + inputs: + ai_provider: + description: 'Primary AI provider (openai or claude)' + required: false + default: 'openai' + type: string + review_guidelines_path: + description: 'Path to review guidelines file in your repo' + required: false + default: '.github/review-guidelines.md' + type: string + enable_fallback: + description: 'If primary fails, try the other provider' + required: false + default: true + type: boolean + secrets: + OPENAI_API_KEY: + required: false + ANTHROPIC_API_KEY: + required: false + +jobs: + review: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + env: + AI_PROVIDER: ${{ inputs.ai_provider }} + + steps: + - name: Checkout caller repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get PR diff + run: | + BASE_REF="${{ github.base_ref || 'main' }}" + git fetch origin "$BASE_REF" + git diff "origin/$BASE_REF" > diff.txt + + - name: Build prompt + env: + PR_BODY: ${{ github.event.pull_request.body }} + GUIDELINES_PATH: ${{ inputs.review_guidelines_path }} + run: | + printf '%s\n' "$PR_BODY" > pr_body.txt + + # Use repo's guidelines if exists, otherwise use default + if [ -f "$GUIDELINES_PATH" ]; then + RULES=$(cat "$GUIDELINES_PATH") + else + RULES="- Follow language best practices + - Avoid console.log in production code + - All API calls must have error handling + - Validate inputs before processing + - Use async/await over raw promises + - Avoid duplicate logic + - Ensure proper null/undefined checks" + fi + + python3 -c " + import sys + rules = sys.argv[1] + pr_body = open('pr_body.txt').read() + diff = open('diff.txt').read() + prompt = f'''You are a senior code reviewer. + + GLOBAL RULES: + {rules} + + PR CONTEXT: + {pr_body} + + CODE CHANGES: + {diff} + + Review the changes above for: + - Bugs and logic errors + - Security vulnerabilities (OWASP Top 10) + - Code quality and style violations + - Missing input validation or error handling + - Violations of the global rules listed above + + Give concise, actionable feedback. Reference specific line numbers where possible.''' + open('prompt.txt', 'w').write(prompt) + " "$RULES" + + - name: Call primary provider + id: primary + continue-on-error: true + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + if [ "$AI_PROVIDER" = "claude" ]; then + PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ + '{model:"claude-sonnet-4-6",max_tokens:1024,messages:[{role:"user",content:$prompt}]}') + RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + REVIEW=$(echo "$RESPONSE" | jq -re '.content[0].text') + USED="Claude Sonnet 4.6 (Anthropic)" + else + PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ + '{model:"gpt-4o-mini",temperature:0.3,messages:[{role:"user",content:$prompt}]}') + RESPONSE=$(curl -sf https://api.openai.com/v1/chat/completions \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + REVIEW=$(echo "$RESPONSE" | jq -re '.choices[0].message.content') + USED="GPT-4o-mini (OpenAI)" + fi + echo "REVIEW<> "$GITHUB_OUTPUT" + printf '%s\n' "$REVIEW" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + echo "USED=$USED" >> "$GITHUB_OUTPUT" + + - name: Call fallback provider + id: fallback + if: steps.primary.outcome == 'failure' && inputs.enable_fallback + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + echo "::warning::Primary provider ($AI_PROVIDER) failed — switching to fallback." + if [ "$AI_PROVIDER" = "claude" ]; then + PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ + '{model:"gpt-4o-mini",temperature:0.3,messages:[{role:"user",content:$prompt}]}') + RESPONSE=$(curl -sf https://api.openai.com/v1/chat/completions \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") || { echo "::error::Both Claude and OpenAI failed."; exit 1; } + REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ("OpenAI error: " + (.error.message // "unknown"))') + USED="GPT-4o-mini (OpenAI) [fallback]" + else + PAYLOAD=$(jq -n --rawfile prompt prompt.txt \ + '{model:"claude-sonnet-4-6",max_tokens:1024,messages:[{role:"user",content:$prompt}]}') + RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") || { echo "::error::Both OpenAI and Claude failed."; exit 1; } + REVIEW=$(echo "$RESPONSE" | jq -r '.content[0].text // ("Claude error: " + (.error.message // "unknown"))') + USED="Claude Sonnet 4.6 (Anthropic) [fallback]" + fi + echo "REVIEW<> "$GITHUB_OUTPUT" + printf '%s\n' "$REVIEW" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + echo "USED=$USED" >> "$GITHUB_OUTPUT" + + - name: Post review comment + uses: actions/github-script@v7 + env: + PRIMARY_REVIEW: ${{ steps.primary.outputs.REVIEW }} + PRIMARY_USED: ${{ steps.primary.outputs.USED }} + FALLBACK_REVIEW: ${{ steps.fallback.outputs.REVIEW }} + FALLBACK_USED: ${{ steps.fallback.outputs.USED }} + with: + script: | + const review = process.env.PRIMARY_REVIEW || process.env.FALLBACK_REVIEW; + const label = process.env.PRIMARY_USED || process.env.FALLBACK_USED; + if (!review) { + core.setFailed('No review generated — both providers failed.'); + return; + } + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## 🤖 AI Code Review — ${label}\n\n${review}\n\n---\nPowered by [CodeSage](https://github.com/BLEND360/code-sage-code-review)` + }); diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 5ab4a16..46b84db 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -1,3 +1,5 @@ +# CodeSage's own AI review — uses the reusable workflow we provide to others + name: AI Code Review on: @@ -6,113 +8,21 @@ on: workflow_dispatch: inputs: ai_provider: - description: 'AI provider to use for this run' + description: 'Primary AI provider for this run' required: false - default: 'claude' + default: 'openai' type: choice options: - - claude - openai + - claude jobs: review: - runs-on: ubuntu-latest - # Priority: manual dispatch input > repo variable (vars.AI_PROVIDER) > default 'claude' - env: - AI_PROVIDER: ${{ github.event_name == 'workflow_dispatch' && inputs.ai_provider || vars.AI_PROVIDER || 'claude' }} - - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get PR diff - run: | - BASE_REF="${{ github.base_ref || 'main' }}" - git fetch origin "$BASE_REF" - git diff "origin/$BASE_REF" > diff.txt - - - name: Build prompt - env: - PR_BODY: ${{ github.event.pull_request.body }} - run: | - printf '%s\n' "$PR_BODY" > pr_body.txt - python3 -c " - template = open('.github/prompt-template.txt').read() - rules = open('.github/review-guidelines.md').read() - pr_body = open('pr_body.txt').read() - diff = open('diff.txt').read() - open('prompt.txt', 'w').write(template.format(rules=rules, pr_body=pr_body, diff=diff)) - " - - - name: Call Claude API - id: claude - if: env.AI_PROVIDER == 'claude' - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - run: | - PAYLOAD=$(jq -n --rawfile prompt prompt.txt '{ - model: "claude-sonnet-4-6", - max_tokens: 1024, - messages: [{role: "user", content: $prompt}] - }') - RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \ - -H "x-api-key: $ANTHROPIC_API_KEY" \ - -H "anthropic-version: 2023-06-01" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") || { echo "::error::Claude API call failed"; exit 1; } - REVIEW=$(echo "$RESPONSE" | jq -r '.content[0].text // ("Claude error: " + (.error.message // "unknown"))') - echo "REVIEW<> "$GITHUB_OUTPUT" - printf '%s\n' "$REVIEW" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - - name: Call OpenAI API - id: openai - if: env.AI_PROVIDER == 'openai' - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - PAYLOAD=$(jq -n --rawfile prompt prompt.txt '{ - model: "gpt-4o-mini", - temperature: 0.3, - messages: [{role: "user", content: $prompt}] - }') - RESPONSE=$(curl -sf https://api.openai.com/v1/chat/completions \ - -H "Authorization: Bearer $OPENAI_API_KEY" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") || { echo "::error::OpenAI API call failed"; exit 1; } - REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ("OpenAI error: " + (.error.message // "unknown"))') - echo "REVIEW<> "$GITHUB_OUTPUT" - printf '%s\n' "$REVIEW" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - - name: Post review comment - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - env: - CLAUDE_REVIEW: ${{ steps.claude.outputs.REVIEW }} - OPENAI_REVIEW: ${{ steps.openai.outputs.REVIEW }} - AI_PROVIDER: ${{ env.AI_PROVIDER }} - with: - script: | - const provider = process.env.AI_PROVIDER; - const review = process.env.CLAUDE_REVIEW || process.env.OPENAI_REVIEW; - const label = provider === 'claude' - ? 'Claude Sonnet 4.6 (Anthropic)' - : 'GPT-4o-mini (OpenAI)'; - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `## 🤖 AI Code Review — ${label}\n\n${review}` - }); - - - name: Print review (manual run) - if: github.event_name == 'workflow_dispatch' - env: - CLAUDE_REVIEW: ${{ steps.claude.outputs.REVIEW }} - OPENAI_REVIEW: ${{ steps.openai.outputs.REVIEW }} - run: | - echo "=== AI Review Output (provider: $AI_PROVIDER) ===" - printf '%s\n' "${CLAUDE_REVIEW:-$OPENAI_REVIEW}" + uses: ./.github/workflows/ai-review-reusable.yml + with: + ai_provider: ${{ github.event_name == 'workflow_dispatch' && inputs.ai_provider || 'openai' }} + review_guidelines_path: '.github/review-guidelines.md' + enable_fallback: true + secrets: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..7cf00f6 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,90 @@ +# CodeSage Setup Guide + +Add AI-powered code reviews to your repo in 3 steps. + +--- + +## Step 1: Add the Workflow + +Copy [`templates/ai-review-starter.yml`](templates/ai-review-starter.yml) to your repo: + +``` +your-repo/ +└── .github/ + └── workflows/ + └── ai-review.yml ← paste it here +``` + +Or use this one-liner in your repo: + +```bash +mkdir -p .github/workflows && curl -o .github/workflows/ai-review.yml \ + https://raw.githubusercontent.com/BLEND360/code-sage-code-review/main/templates/ai-review-starter.yml +``` + +--- + +## Step 2: Add API Keys + +Go to your repo: **Settings → Secrets and variables → Actions → New repository secret** + +| Secret Name | Required | Get it from | +|-------------|----------|-------------| +| `OPENAI_API_KEY` | If using OpenAI | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) | +| `ANTHROPIC_API_KEY` | If using Claude | [console.anthropic.com](https://console.anthropic.com/) | + +> **Org-wide secrets**: Ask your org admin to add these at the org level so all repos can use them. + +--- + +## Step 3: (Optional) Customize Review Rules + +Create `.github/review-guidelines.md` in your repo with your team's rules. + +See [`templates/review-guidelines.md`](templates/review-guidelines.md) for an example. + +If you don't add this file, the reviewer uses sensible defaults. + +--- + +## Configuration Options + +Edit your `.github/workflows/ai-review.yml`: + +```yaml +with: + # Choose provider: 'openai' (default) or 'claude' + ai_provider: 'openai' + + # Path to your team's review guidelines + review_guidelines_path: '.github/review-guidelines.md' + + # Fallback to other provider if primary fails (default: true) + enable_fallback: true +``` + +--- + +## Test It + +1. Create a branch with any code change +2. Open a Pull Request +3. Wait ~30 seconds +4. See the 🤖 AI Code Review comment appear! + +--- + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| No comment appears | Check Actions tab for errors | +| 403 error | Add `pull-requests: write` permission (already in template) | +| API error | Verify your secret name matches exactly | +| Wrong provider | Check `ai_provider` in your workflow | + +--- + +## Questions? + +Open an issue at [BLEND360/code-sage-code-review](https://github.com/BLEND360/code-sage-code-review/issues) diff --git a/templates/ai-review-starter.yml b/templates/ai-review-starter.yml new file mode 100644 index 0000000..b5560cc --- /dev/null +++ b/templates/ai-review-starter.yml @@ -0,0 +1,32 @@ +# AI Code Review — Starter Template +# Copy this file to your repo: .github/workflows/ai-review.yml +# +# Setup: +# 1. Copy this file to .github/workflows/ai-review.yml +# 2. Add secrets: Settings → Secrets → Actions +# - OPENAI_API_KEY (required if using openai) +# - ANTHROPIC_API_KEY (required if using claude) +# 3. (Optional) Add .github/review-guidelines.md with your team's rules +# 4. Open a PR — review comment will appear automatically! + +name: AI Code Review + +on: + pull_request: + types: [opened, synchronize] + +jobs: + review: + uses: BLEND360/code-sage-code-review/.github/workflows/ai-review-reusable.yml@main + with: + # Choose your provider: 'openai' or 'claude' + ai_provider: 'openai' + + # Path to your review guidelines (optional) + # review_guidelines_path: '.github/review-guidelines.md' + + # Enable fallback to other provider if primary fails (default: true) + # enable_fallback: true + secrets: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/templates/review-guidelines.md b/templates/review-guidelines.md new file mode 100644 index 0000000..d188067 --- /dev/null +++ b/templates/review-guidelines.md @@ -0,0 +1,28 @@ +## Code Review Guidelines + + + +### General +- Follow ESLint and Prettier rules +- Avoid console.log in production code +- No hardcoded secrets or API keys + +### Error Handling +- All API calls must have error handling (try/catch) +- Validate inputs before processing +- Use async/await instead of raw promises + +### Code Quality +- Avoid duplicate logic — extract to functions +- Ensure proper null/undefined checks +- Keep functions under 50 lines +- Use meaningful variable names + +### Security +- Sanitize user inputs +- Use parameterized queries for databases +- No sensitive data in logs + +### Testing +- New features must include unit tests +- Bug fixes should add regression tests