Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions .github/workflows/ai-review-reusable.yml
Original file line number Diff line number Diff line change
@@ -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<<EOF" >> "$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<<EOF" >> "$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---\n<sub>Powered by [CodeSage](https://github.com/BLEND360/code-sage-code-review)</sub>`
});
116 changes: 13 additions & 103 deletions .github/workflows/ai-review.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# CodeSage's own AI review — uses the reusable workflow we provide to others

name: AI Code Review

on:
Expand All @@ -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<<EOF" >> "$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<<EOF" >> "$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 }}
Loading