diff --git a/README.md b/README.md index cfa2676..407f930 100644 --- a/README.md +++ b/README.md @@ -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. @@ -27,6 +27,7 @@ permissions: on: pull_request: + types: [opened, synchronize, reopened, labeled] jobs: review: @@ -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 @@ -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: diff --git a/scripts/comment-pr-findings.bun.test.js b/scripts/comment-pr-findings.bun.test.js index 0409d9b..03afced 100644 --- a/scripts/comment-pr-findings.bun.test.js +++ b/scripts/comment-pr-findings.bun.test.js @@ -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 = [{ @@ -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) => { @@ -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'); }); diff --git a/scripts/comment-pr-findings.js b/scripts/comment-pr-findings.js index d594e31..1774954 100755 --- a/scripts/comment-pr-findings.js +++ b/scripts/comment-pr-findings.js @@ -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`); @@ -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`,