Skip to content
Open
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
90 changes: 90 additions & 0 deletions .github/workflows/code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Code Review

on:
pull_request:
types: [opened, ready_for_review, synchronize]

permissions:
contents: read
pull-requests: write
issues: write
id-token: write

jobs:
code-review:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: anthropics/claude-code-action@v1
id: claude-review
continue-on-error: true
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
use_sticky_comment: true
prompt: |
Review the changes introduced by this PR. Follow these steps:

1. Use `gh pr diff` or read the changed files to understand what changed.
2. For each changed file, check for:
- Correctness: logic errors, edge cases, off-by-ones
- Security: injection risks, auth bypasses, exposed secrets
- Elixir/Phoenix patterns: proper use of changesets, Ecto queries scoped by project, policy checks via Bodyguard
- React/TypeScript patterns: correct hook usage, prop types, no unnecessary re-renders
- Test coverage: are the changes tested?
3. Check CLAUDE.md for project-specific conventions.
4. Post a structured review comment with sections:
- **Summary** of what the PR does
- **Issues** (if any): numbered list with file:line references
- **Suggestions** (optional): non-blocking improvements
- **Verdict**: APPROVE / REQUEST CHANGES / COMMENT
claude_args: --max-turns 50 --model claude-opus-4-7

- name: Post status comment if Claude did not comment
if: always()
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const outcome = '${{ steps.claude-review.outcome }}';
const hasClaudeComment = comments.some(c =>
c.body && c.body.includes('Code Review') && c.user.type === 'Bot'
);
if (!hasClaudeComment) {
const isFailure = outcome === 'failure' || outcome === 'cancelled';
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: isFailure
? [
'## Code Review',
'',
'⚠️ **Automated code review did not complete.**',
'',
'Claude hit the max-turns limit or encountered an error before posting findings.',
'',
'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.',
].join('\n')
: [
'## Code Review',
'',
'⚠️ The review completed but no findings comment was posted.',
'',
'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for the raw Claude output.',
].join('\n'),
});
}

- name: Fail the check if review failed
if: steps.claude-review.outcome == 'failure'
run: |
echo "::error::Code review did not complete successfully. See PR comments for details."
exit 1
45 changes: 27 additions & 18 deletions .github/workflows/security-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
permissions:
contents: read
pull-requests: write
issues: write
id-token: write

jobs:
Expand All @@ -31,40 +32,48 @@ jobs:
Read that file first, then follow its instructions exactly.
Review only the changes introduced by this PR.
Post your findings as a structured review comment.
claude_args: |
--max-turns 50
--model claude-opus-4-6
claude_args: --max-turns 50 --model claude-opus-4-7

- name: Post fallback comment on failure
if: steps.claude-review.outcome == 'failure'
- name: Post status comment if Claude did not comment
if: always()
uses: actions/github-script@v7
with:
script: |
// Check if there's already a sticky comment from Claude
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const outcome = '${{ steps.claude-review.outcome }}';
// Claude posts its own sticky comment when successful; only intervene if it didn't.
const hasClaudeComment = comments.some(c =>
c.body && c.body.includes('Security Review')
c.body && c.body.includes('Security Review') && c.user.type === 'Bot'
);
if (!hasClaudeComment) {
const isFailure = outcome === 'failure' || outcome === 'cancelled';
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: [
'## Security Review',
'',
'⚠️ **Automated security review did not complete.**',
'',
'Claude hit the max-turns limit or encountered an error before posting findings.',
'A manual review of S0 (project-scoped data access), S1 (authorization policies),',
'and S2 (audit trail coverage) is recommended for this PR.',
'',
'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.',
].join('\n'),
body: isFailure
? [
'## Security Review',
'',
'⚠️ **Automated security review did not complete.**',
'',
'Claude hit the max-turns limit or encountered an error before posting findings.',
'A manual review of S0 (project-scoped data access), S1 (authorization policies),',
'and S2 (audit trail coverage) is recommended for this PR.',
'',
'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for details.',
].join('\n')
: [
'## Security Review',
'',
'⚠️ The review completed but no findings comment was posted.',
'',
'See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + ') for the raw Claude output.',
].join('\n'),
});
}

Expand Down
Loading