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
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Claude Code Reviewer
# Nutrient Code Reviewer

An AI-powered code review GitHub Action using Claude to analyze code changes. Uses a unified multi-agent approach for both code quality (correctness, reliability, performance, maintainability, testing) and security in a single pass. This action provides intelligent, context-aware review for pull requests using Anthropic's Claude Code tool for deep semantic analysis.

Expand Down Expand Up @@ -27,6 +27,7 @@ permissions:

on:
pull_request:
types: [opened, synchronize, reopened, labeled]

jobs:
review:
Expand Down Expand Up @@ -63,6 +64,9 @@ This action is not hardened against prompt injection attacks and should only be
| `false-positive-filtering-instructions` | Path to custom false positive filtering instructions text file | None | No |
| `custom-review-instructions` | Path to custom code review instructions text file to append to the audit prompt | None | No |
| `custom-security-scan-instructions` | Path to custom security scan instructions text file to append to the security section | None | No |
| `dismiss-stale-reviews` | Dismiss previous bot reviews when posting a new review (useful for follow-up commits) | `true` | No |
| `skip-draft-prs` | Skip code review on draft pull requests | `true` | No |
| `require-label` | Only run review if this label is present. Leave empty to review all PRs. Add `labeled` to your workflow `pull_request` types to trigger on label addition. | None | No |

### Action Outputs

Expand Down Expand Up @@ -150,6 +154,37 @@ The default command is designed to work well in most cases, but it can also be c

It is also possible to configure custom scanning and false positive filtering instructions, see the [`docs/`](docs/) folder for more details.

## Using a Custom GitHub App

By default, reviews are posted as "github-actions[bot]". To use a custom name and avatar:

1. **Create a GitHub App** at `https://github.com/settings/apps/new`
- Set your desired name and avatar
- Permissions: Pull requests (Read & Write), Contents (Read)
- Uncheck "Webhook > Active"

2. **Store secrets** in your repository:
- `APP_ID` - The App ID from settings
- `APP_PRIVATE_KEY` - Generated private key

3. **Update your workflow**:
```yaml
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- uses: PSPDFKit-labs/claude-code-review@main
with:
claude-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
```

Review dismissal works automatically with custom apps since reviews are identified by content, not bot username.

## Testing

Run the test suite to validate functionality:
Expand Down
24 changes: 17 additions & 7 deletions scripts/comment-pr-findings.bun.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ describe('comment-pr-findings.js', () => {
});

describe('Stale Review Handling', () => {
test('should dismiss stale reviews when DISMISS_STALE_REVIEWS is true', async () => {
test('should only dismiss own bot reviews when DISMISS_STALE_REVIEWS is true', async () => {
process.env.DISMISS_STALE_REVIEWS = 'true';

const mockFindings = [{
Expand All @@ -863,10 +863,16 @@ describe('comment-pr-findings.js', () => {
const mockPrFiles = [{ filename: 'test.py', patch: '@@ -10,1 +10,1 @@' }];

const mockReviews = [
{ id: 101, state: 'CHANGES_REQUESTED', user: { type: 'Bot' } },
{ id: 102, state: 'APPROVED', user: { type: 'Bot' } },
{ id: 103, state: 'COMMENTED', user: { type: 'Bot' } }, // Should not be dismissed
{ id: 104, state: 'CHANGES_REQUESTED', user: { type: 'User' } } // Should not be dismissed
// Our reviews - should be dismissed
{ id: 101, state: 'CHANGES_REQUESTED', user: { type: 'Bot' }, body: 'Found 3 security issues. Please address the high-severity issues before merging.' },
{ id: 102, state: 'APPROVED', user: { type: 'Bot' }, body: 'No issues found. Changes look good.' },
// Other bot reviews - should NOT be dismissed
{ id: 103, state: 'APPROVED', user: { type: 'Bot' }, body: 'Dependabot has approved this PR.' },
{ id: 104, state: 'CHANGES_REQUESTED', user: { type: 'Bot' }, body: 'Renovate: This PR has conflicts.' },
// COMMENTED state - should NOT be dismissed
{ id: 105, state: 'COMMENTED', user: { type: 'Bot' }, body: 'Found 1 security issue.' },
// User review - should NOT be dismissed
{ id: 106, state: 'CHANGES_REQUESTED', user: { type: 'User' }, body: 'Please fix the typo.' }
];

readFileSyncSpy.mockImplementation((path) => {
Expand Down Expand Up @@ -907,10 +913,14 @@ describe('comment-pr-findings.js', () => {

await import('./comment-pr-findings.js');

// Only our reviews should be dismissed
expect(dismissedReviews).toContain(101);
expect(dismissedReviews).toContain(102);
expect(dismissedReviews).not.toContain(103); // COMMENTED state
expect(dismissedReviews).not.toContain(104); // User review
// Other bot reviews should NOT be dismissed
expect(dismissedReviews).not.toContain(103); // Dependabot
expect(dismissedReviews).not.toContain(104); // Renovate
expect(dismissedReviews).not.toContain(105); // COMMENTED state
expect(dismissedReviews).not.toContain(106); // User review
expect(consoleLogSpy).toHaveBeenCalledWith('Dismissed stale review 101');
expect(consoleLogSpy).toHaveBeenCalledWith('Dismissed stale review 102');
});
Expand Down
29 changes: 27 additions & 2 deletions scripts/comment-pr-findings.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,31 @@ function addReactionsToReview(reviewId) {
}
}

// Helper function to dismiss stale bot reviews
// Check if a review was posted by this action
function isOwnReview(review) {
if (!review.body) return false;

// Check for our review summary patterns
const ownPatterns = [
'No issues found. Changes look good.',
/^Found \d+ .+ issues?\./,
'Please address the high-severity issues before merging.',
'Consider addressing the suggestions in the comments.',
'Minor suggestions noted in comments.'
];

for (const pattern of ownPatterns) {
if (pattern instanceof RegExp) {
if (pattern.test(review.body)) return true;
} else {
if (review.body.includes(pattern)) return true;
}
}

return false;
}

// Helper function to dismiss stale bot reviews from this action only
function dismissStaleReviews() {
try {
const reviews = ghApi(`/repos/${context.repo.owner}/${context.repo.repo}/pulls/${context.issue.number}/reviews`);
Expand All @@ -101,8 +125,9 @@ function dismissStaleReviews() {
for (const review of reviews) {
const isDismissible = review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED';
const isBot = review.user && review.user.type === 'Bot';
const isOwn = isOwnReview(review);

if (isBot && isDismissible) {
if (isBot && isDismissible && isOwn) {
try {
ghApi(
`/repos/${context.repo.owner}/${context.repo.repo}/pulls/${context.issue.number}/reviews/${review.id}/dismissals`,
Expand Down