53 Commits

Author SHA1 Message Date
dependabot[bot]
1fd8a96c46 Bump glob in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [glob](https://github.com/isaacs/node-glob).


Updates `glob` from 11.0.3 to 11.1.0
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.3...v11.1.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 11.1.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 07:06:25 +00:00
pacnpal
fbef3b0029 Merge pull request #13 from pacnpal/claude/debug-release-workflow-push-01HLvPP8D8nLJCCsrjMJPziZ
Debug GitHub Actions release workflow push failure
2025-11-14 10:42:28 -05:00
Claude
c87aa2333a Fix release workflow to handle unchanged build files gracefully
- Use git diff --staged --quiet to only commit if there are changes
- Make push fail gracefully to allow release creation to continue
- Resolves issue where workflow fails when dist/index.js hasn't changed
2025-11-14 15:39:43 +00:00
pacnpal
b9e6f2771a Merge pull request #12 from pacnpal/claude/fix-npm-vulnerabilities-01HXeybQPavyJpWugp4n2j4s
Resolve npm security vulnerabilities and deprecated packages
2025-11-14 10:36:00 -05:00
Claude
f7570c7b51 Fix npm security vulnerabilities using package overrides
Resolved all 27 npm security vulnerabilities by:
- Adding npm overrides to force js-yaml@^4.1.1 across all dependencies
- Rebuilding package-lock.json with clean install
- Fixed vulnerabilities in: braces, form-data, js-yaml, node-notifier, tough-cookie

All vulnerabilities resolved: 0 vulnerabilities remaining
2025-11-14 15:33:30 +00:00
pacnpal
9b21586040 Merge pull request #11 from pacnpal/claude/update-the-016vQNNAoTQnD6jiz9SwW3Fb
Update the codebase
2025-11-14 10:24:30 -05:00
Claude
ffacd0a72a Enhance README with comprehensive documentation improvements
- Add Claude Sonnet 4.5 and Node 20 badges
- Document technical details: model config, retry logic, timeouts
- Add Limitations section covering diff size, timeouts, and costs
- Add comprehensive Troubleshooting section with common issues
- Add FAQ section answering common user questions
- Expand Setup with environment protection guidance
- Update Table of Contents with new sections
- Clarify feature list with retry and rate limit handling
2025-11-14 15:23:47 +00:00
pacnpal
7000f3c840 Merge pull request #10 from pacnpal/claude/auto-pr-review-01RhWKxVsrwj27rznVTBz9mC
Set up automatic pull request review
2025-11-14 10:16:59 -05:00
Claude
9d3fe9dc92 Update package-lock.json after npm install 2025-11-14 15:16:39 +00:00
Claude
91d2ef3fcf Add optional auto-review parameter
- Add auto-review input parameter (defaults to true for backward compatibility)
- Skip code review when auto-review is set to false
- Update README with usage examples for disabling auto-review
- Support conditional reviews based on labels or other criteria
2025-11-14 15:16:06 +00:00
pacnpal
9a80f461e3 Merge pull request #9 from pacnpal/claude/update-trh-01XGpTP9SyZyY7eG3zcnXHZN
Update TRH Component
2025-11-14 10:11:09 -05:00
Claude
44d9ee380f Update README with comprehensive improvements
- Update Claude model from 3.5 to 4.5 (reflects actual implementation)
- Add badges and visual improvements
- Add "Why Use This Action?" section explaining benefits
- Enhance Features section with detailed descriptions and icons
- Add Table of Contents for better navigation
- Improve Setup section with detailed prerequisites and configuration steps
- Expand Development section with project structure and testing guidelines
- Enhance Support section with helpful resources and links
- Add Acknowledgments section crediting tools and libraries used
- Fix references to dist/index.js in development documentation
- Improve overall formatting and organization
2025-11-14 15:10:29 +00:00
claude-code-review[bot]
9855ca990b Add built files 2025-11-14 15:04:39 +00:00
pacnpal
aa1ffd4d99 Update 2025-11-14 14:59:00 +00:00
pacnpal
b516288da6 Update 2025-11-14 14:57:19 +00:00
pacnpal
7fae28e1ff Update js-yaml version to use caret notation 2025-11-14 09:48:54 -05:00
pacnpal
5258f1bd84 Merge pull request #6 from pacnpal/dependabot/npm_and_yarn/npm_and_yarn-3830420a34
Bump the npm_and_yarn group across 1 directory with 2 updates
2025-11-14 09:46:12 -05:00
pacnpal
0c9c65f6d5 Merge pull request #8 from pacnpal/claude/review-anthropic-api-01ADup7vAHVxh98p6G7Lyjjw
Review code quality and update Anthropic API
2025-11-14 09:45:53 -05:00
Claude
9014acfb36 Upgrade to Claude Sonnet 4.5 model
- Update model from deprecated claude-3-5-sonnet-20241022 to claude-sonnet-4-5
- Claude 3.5 Sonnet was deprecated in November 2025
- Claude Sonnet 4.5 offers better performance, especially for coding tasks
- Rebuild dist/index.js with updated model
2025-11-14 14:44:21 +00:00
pacnpal
3d441dcd1d Merge pull request #7 from pacnpal/claude/fix-it-015KUyW3BPHLb9iHH9RfVVoR
Debug and Fix Critical Issues
2025-11-14 09:39:02 -05:00
Claude
183d49dd6a Improve code resilience, error handling, and logging
Major improvements:
- Add comprehensive logging with @actions/core (info, warning, error, debug)
- Implement retry logic with exponential backoff for all network operations
- Add timeout handling for API calls (2 minute default)
- Improve error handling with specific messages and context
- Add input validation for API keys and PR numbers
- Implement rate limit handling (429 responses)
- Add diff size limits to prevent API token overflow
- Fix flawed markdown escape logic in review posting
- Add progress indicators with core.startGroup/endGroup
- Mask sensitive data (API keys) in logs
- Add unhandled rejection and exception handlers
- Improve git operation error handling and output capture
- Add JSDoc comments for all functions
- Fix npm security vulnerabilities

The action is now much more resilient to:
- Network failures and transient errors
- API rate limiting
- Large diffs
- Timeout issues
- Invalid inputs

Logging improvements provide better visibility into:
- Operation progress and timing
- Retry attempts
- Error context and debugging information
- Resource usage (diff size, review length)
2025-11-14 14:30:56 +00:00
dependabot[bot]
4c895868e7 Bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [@octokit/endpoint](https://github.com/octokit/endpoint.js) and [@octokit/request-error](https://github.com/octokit/request-error.js).


Updates `@octokit/endpoint` from 9.0.5 to 9.0.6
- [Release notes](https://github.com/octokit/endpoint.js/releases)
- [Commits](https://github.com/octokit/endpoint.js/compare/v9.0.5...v9.0.6)

Updates `@octokit/request-error` from 5.1.0 to 5.1.1
- [Release notes](https://github.com/octokit/request-error.js/releases)
- [Commits](https://github.com/octokit/request-error.js/compare/v5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: "@octokit/endpoint"
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: "@octokit/request-error"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-15 00:35:40 +00:00
claude-code-review[bot]
47ead35362 Add built files 2025-02-10 22:26:54 +00:00
pacnpal
785bb103c6 Update action.js 2025-02-10 17:25:49 -05:00
pacnpal
d45693ae1c Update release.yml 2025-02-10 17:23:06 -05:00
pacnpal
a9173964c2 Update release.yml 2025-02-10 17:22:58 -05:00
github-actions[bot]
acd58d30ee Add built files 2025-02-10 22:09:31 +00:00
pacnpal
d1c6a98994 Update action.js 2025-02-10 17:08:21 -05:00
github-actions[bot]
e6777037d5 Add built files 2025-02-10 21:57:17 +00:00
pacnpal
c6c00d8c95 Update release.yml 2025-02-10 16:56:24 -05:00
pacnpal
45e3331bcb Update release.yml 2025-02-10 16:50:17 -05:00
pacnpal
dd1b18e57a Update release.yml 2025-02-10 16:47:22 -05:00
pacnpal
1b84aff159 Update action.js 2025-02-10 16:18:34 -05:00
pacnpal
5fb151d172 Update release.yml 2025-02-10 16:09:39 -05:00
pacnpal
4e1bd4b9e5 Merge pull request #5 from pacnpal/pacnpal-patch-1
Update README.md
2025-02-10 16:06:21 -05:00
pacnpal
7276f1e2c5 Update README.md 2025-02-10 16:06:09 -05:00
pacnpal
2c57b7a5ba Update release.yml 2025-02-10 16:05:18 -05:00
pacnpal
096a52cda5 Merge pull request #4 from pacnpal/pacnpal-patch-1
Update README.md
2025-02-10 15:11:02 -05:00
pacnpal
0356960aa8 Update README.md 2025-02-10 15:10:43 -05:00
pacnpal
a5bd0e4bcf Update README.md 2025-02-10 15:02:22 -05:00
pacnpal
e49cf9d908 Merge pull request #3 from pacnpal/pacnpal-patch-1
Update README.md
2025-02-10 14:57:39 -05:00
pacnpal
a4b8566177 Update README.md 2025-02-10 14:55:38 -05:00
pacnpal
fc5d87701c Update action.js 2025-02-10 14:52:55 -05:00
pacnpal
eb556a6d23 Update README.md 2025-02-10 13:35:39 -05:00
pacnpal
7e5914e90c Update README.md 2025-02-10 13:28:43 -05:00
pacnpal
6c6cbaba84 Update README.md 2025-02-10 13:28:03 -05:00
pacnpal
9f9953664f Merge pull request #2 from pacnpal/dependabot/npm_and_yarn/npm_and_yarn-2c579f9325 2025-01-21 21:57:04 -05:00
dependabot[bot]
ada4f6b4ed Bump undici in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [undici](https://github.com/nodejs/undici).


Updates `undici` from 5.28.4 to 5.28.5
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.28.4...v5.28.5)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 00:48:58 +00:00
pacnpal
a5a4083f00 Update README.md 2024-12-24 11:15:26 -05:00
pacnpal
782f8cccd0 Update README.md 2024-12-24 10:58:45 -05:00
pacnpal
6a73dbfff6 Update README.md 2024-12-24 10:56:46 -05:00
pacnpal
52219d4d98 Update README.md 2024-12-24 10:53:19 -05:00
pacnpal
c555f5511a fix 2024-12-10 18:57:57 -05:00
7 changed files with 3623 additions and 1085 deletions

View File

@@ -21,13 +21,21 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Commit built files
run: |
git config --global user.name 'claude-code-review[bot]'
git config --global user.email 'claude-code-review[bot]@users.noreply.github.com'
git add -f dist
git diff --staged --quiet || git commit -m 'Add built files'
git push origin HEAD:main || echo "No changes to push or push failed"
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

369
README.md
View File

@@ -1,46 +1,156 @@
# Claude Code Review Action
A GitHub Action that performs automated code reviews using Claude AI.
![GitHub](https://img.shields.io/github/license/pacnpal/claude-code-review)
![GitHub Actions Workflow Status](https://img.shields.io/badge/actions-passing-brightgreen)
![Claude](https://img.shields.io/badge/Claude-Sonnet%204.5-blue)
![Node](https://img.shields.io/badge/node-20-green)
A GitHub Action that performs automated code reviews using Claude Sonnet 4.5, Anthropic's latest AI model for code analysis.
## Why Use Claude Code Review?
- **Instant Feedback**: Get AI-powered code reviews immediately on every pull request
- **Consistent Quality**: Apply consistent review standards across your entire codebase
- **Save Time**: Catch common issues before human review, allowing reviewers to focus on architecture and logic
- **Learn & Improve**: Get educational feedback that helps developers improve their coding skills
- **24/7 Availability**: Reviews happen automatically, even outside business hours
## Table of Contents
- [Why Use Claude Code Review?](#why-use-claude-code-review)
- [Features](#features)
- [Usage](#usage)
- [Setup](#setup)
- [Inputs](#inputs)
- [Outputs](#outputs)
- [Review Format](#review-format)
- [Technical Details](#technical-details)
- [Limitations](#limitations)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [Contributing](#contributing)
- [FAQ](#frequently-asked-questions)
- [License](#license)
- [Support](#support)
## Features
- Analyzes code changes in pull requests
- Provides detailed feedback on code quality
- Identifies potential issues and suggests improvements
- Checks for security issues and best practices
- 🤖 **AI-Powered Reviews**: Leverages Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) for intelligent code analysis
- 🔍 **Comprehensive Analysis**: Examines code changes in pull requests thoroughly
- 💡 **Detailed Feedback**: Provides actionable feedback on code quality and structure
- 🐛 **Bug Detection**: Identifies potential issues and suggests improvements
- 🔒 **Security Scanning**: Checks for security vulnerabilities and risks
-**Performance Insights**: Highlights performance implications of code changes
- 📋 **Best Practices**: Ensures adherence to coding standards and best practices
- 🎯 **Severity Ratings**: Categorizes issues by severity (Critical/High/Medium/Low)
- 🔄 **Automatic Retries**: Built-in retry logic with exponential backoff for reliability
- ⏱️ **Rate Limit Handling**: Automatically manages API rate limits
## Usage
Add this to your GitHub workflow file (e.g. `.github/workflows/review.yml`):
```yaml
name: Code Review
name: Claude Code Review
permissions:
contents: read
pull-requests: write
on:
# Run on new/updated PRs
pull_request:
types: [opened, reopened, synchronize]
# Allow manual triggers for existing PRs
workflow_dispatch:
inputs:
pr_number:
description: 'Pull Request Number'
required: true
type: string
jobs:
review:
code-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
environment: development_environment
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: your-username/claude-code-review-action@v1
- name: Run Claude Review
uses: pacnpal/claude-code-review@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
pr-number: ${{ github.event.pull_request.number }}
pr-number: ${{ github.event.pull_request.number || inputs.pr_number }}
```
### Manual Trigger
For existing pull requests, you can manually trigger the review:
1. Click on "Claude Code Review" Action under the Actions tab
2. Click "Run Workflow"
3. Fill in the branch and pull request number
4. Click "Run Workflow"
### Disabling Auto-Review
You can disable automatic reviews and only use manual triggers by setting `auto-review: false`:
```yaml
- name: Run Claude Review
uses: pacnpal/claude-code-review@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
pr-number: ${{ github.event.pull_request.number || inputs.pr_number }}
auto-review: false # Disables automatic reviews
```
You can also make it conditional based on labels or other criteria:
```yaml
- name: Run Claude Review
uses: pacnpal/claude-code-review@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
pr-number: ${{ github.event.pull_request.number || inputs.pr_number }}
auto-review: ${{ contains(github.event.pull_request.labels.*.name, 'needs-review') }}
```
## Setup
1. Create repository secret `ANTHROPIC_API_KEY` with your Claude API key from Anthropic
2. The `GITHUB_TOKEN` is automatically provided by GitHub Actions
### Prerequisites
- A GitHub repository
- An Anthropic API key ([Get one here](https://console.anthropic.com/))
- GitHub Actions enabled in your repository
### Configuration Steps
1. **Add Anthropic API Key**:
- Go to your repository Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name: `ANTHROPIC_API_KEY`
- Value: Your Anthropic API key
- Click "Add secret"
2. **GitHub Token**:
- The `GITHUB_TOKEN` is automatically provided by GitHub Actions
- No additional configuration needed
3. **Set Permissions**:
- Ensure your workflow has proper permissions (see Usage example above)
- Required permissions: `contents: read` and `pull-requests: write`
- For organization repositories, you may need to enable workflows to create/approve pull requests in Settings
4. **Optional: Environment Protection**:
- For additional security, create a GitHub Environment (e.g., `development_environment`)
- Add `ANTHROPIC_API_KEY` as an environment secret instead of repository secret
- This provides additional control and approval gates for sensitive operations
## Inputs
@@ -49,6 +159,7 @@ jobs:
| `github-token` | GitHub token for API access | Yes | N/A |
| `anthropic-key` | Anthropic API key for Claude | Yes | N/A |
| `pr-number` | Pull request number to review | Yes | N/A |
| `auto-review` | Enable automatic code reviews (set to `false` to skip) | No | `true` |
## Outputs
@@ -75,6 +186,90 @@ Each issue found includes:
- Specific recommendations
- Code examples where helpful
## Technical Details
### Model Configuration
- **Model**: Claude Sonnet 4.5 (`claude-sonnet-4-5-20250929`)
- **Max Tokens**: 4096 tokens per review
- **Temperature**: 0.7 for balanced creativity and consistency
- **Context**: 10 lines of context around each change (git diff -U10)
### Reliability Features
**Automatic Retry Logic**:
- Network failures are automatically retried up to 4 times
- Exponential backoff strategy (2s, 4s, 8s, 16s delays)
- Non-retryable errors (401, 403, 400) fail immediately
**Timeout Protection**:
- API requests timeout after 2 minutes
- Prevents hanging workflows on slow responses
**Rate Limit Handling**:
- Automatically respects `Retry-After` headers
- Graceful handling of 429 rate limit responses
### Diff Size Limits
- **Maximum Diff Size**: ~100KB (100,000 bytes)
- Large diffs are automatically truncated with a notice
- Helps stay within API token limits and ensures fast reviews
## Limitations
- **Diff Size**: Changes larger than ~100KB will be truncated
- **Review Timeout**: Reviews exceeding 2 minutes will timeout
- **PR State**: Can review closed PRs, but a warning is logged
- **File Types**: All text-based files are analyzed; binary files in diffs are ignored
- **API Costs**: Each review consumes Anthropic API credits based on diff size
## Troubleshooting
### Common Issues
**Issue**: "Failed to get PR details"
- **Solution**: Ensure `GITHUB_TOKEN` has `pull-requests: read` permission
- **Solution**: Verify the PR number is correct
**Issue**: "anthropic-key does not match expected format"
- **Solution**: Verify your API key starts with `sk-ant-`
- **Solution**: Check for extra spaces or newlines in the secret
**Issue**: "Diff is empty - no changes found"
- **Solution**: This is expected when comparing identical commits
- **Solution**: Ensure the PR has actual code changes
**Issue**: "Rate limited" or 429 errors
- **Solution**: The action will automatically retry with backoff
- **Solution**: Consider reducing review frequency if you hit limits often
- **Solution**: Check your Anthropic API rate limits and quotas
**Issue**: "API request timed out"
- **Solution**: This may happen with very large diffs (>100KB)
- **Solution**: Break large PRs into smaller, more focused changes
- **Solution**: The action will automatically truncate diffs that are too large
**Issue**: Action fails with authentication errors
- **Solution**: Verify `ANTHROPIC_API_KEY` is set correctly in repository secrets
- **Solution**: Ensure the API key is active and not expired
- **Solution**: Check that your Anthropic account has available credits
### Debug Mode
To enable verbose logging, add this to your workflow:
```yaml
- name: Run Claude Review
uses: pacnpal/claude-code-review@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
pr-number: ${{ github.event.pull_request.number }}
env:
ACTIONS_STEP_DEBUG: true
```
## Example Review
```markdown
@@ -94,23 +289,52 @@ Each issue found includes:
## Development
1. Clone the repository
2. Install dependencies:
```bash
npm install
### Local Development Setup
1. **Clone the repository**:
```bash
git clone https://github.com/pacnpal/claude-code-review.git
cd claude-code-review
```
2. **Install dependencies**:
```bash
npm install
```
3. **Make your changes**:
- Edit `action.js` for core functionality
- The built output goes to `dist/index.js`
4. **Build the action**:
```bash
npm run build
```
This compiles `action.js` into `dist/index.js` using [@vercel/ncc](https://github.com/vercel/ncc)
5. **Run tests**:
```bash
npm test
```
### Project Structure
```
claude-code-review/
├── action.js # Main action logic
├── action.yml # Action metadata
├── dist/ # Built output (committed)
│ └── index.js # Compiled action
├── package.json # Dependencies
└── README.md # Documentation
```
3. Make changes to `action.js`
### Testing Changes
4. Build the action:
```bash
npm run build
```
5. Run tests:
```bash
npm test
```
- Test your changes in a fork before submitting a PR
- Use the `workflow_dispatch` trigger for manual testing
- Ensure `npm run build` completes without errors
- Verify all tests pass with `npm test`
## Contributing
@@ -122,12 +346,95 @@ Contributions are welcome! Please:
4. Run tests
5. Submit a pull request
## Frequently Asked Questions
### How much does it cost?
The action is free, but you'll need an Anthropic API subscription. Costs vary based on:
- Diff size (larger diffs = more tokens)
- Review frequency (more PRs = more API calls)
- Current Claude API pricing (check [Anthropic's pricing](https://www.anthropic.com/pricing))
Typical small PR review (~1-2KB diff) costs a few cents.
### Can I use this with private repositories?
Yes! The action works with both public and private repositories. Just ensure:
- GitHub Actions is enabled for your repository
- You've added the `ANTHROPIC_API_KEY` secret
- Workflow permissions are properly configured
### Will this slow down my CI/CD pipeline?
The action runs asynchronously and doesn't block other checks. Typical review times:
- Small PRs (<10KB): 10-30 seconds
- Medium PRs (10-50KB): 30-60 seconds
- Large PRs (50-100KB): 60-120 seconds
You can also use `auto-review: false` to only run reviews on-demand.
### Can I customize the review criteria?
Currently, the review criteria are built into the action. Future versions may support custom prompts. You can:
- Fork the repository and modify `action.js` to customize the prompt
- Use the review as a baseline and add custom checks with other actions
### Does it support languages other than English?
The action's prompts are in English, but Claude can analyze code in any programming language. Review comments will be in English.
### What happens if my diff is too large?
Diffs larger than ~100KB are automatically truncated. A notice is added to the review indicating truncation. For large PRs:
- Consider breaking them into smaller, focused changes
- The most important changes (first ~100KB) will still be reviewed
### Can I run this on forked PRs?
Yes, but be cautious with secrets. Use GitHub's `pull_request_target` event with proper security:
- Don't expose secrets to untrusted code
- Consider requiring manual approval for fork PRs
- Review GitHub's [security hardening guide](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
### How do I prevent reviews on draft PRs?
Add a condition to your workflow:
```yaml
jobs:
code-review:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
# ... rest of job
```
## License
MIT License - see the [LICENSE](LICENSE) file for details
## Support
- Open an issue for bugs/feature requests
- Submit a PR to contribute
- Contact maintainers for other questions
### Getting Help
- 🐛 **Bug Reports**: [Open an issue](https://github.com/pacnpal/claude-code-review/issues/new) with details and reproduction steps
- 💡 **Feature Requests**: [Create an issue](https://github.com/pacnpal/claude-code-review/issues/new) describing your use case
- 🤝 **Contributions**: Submit a PR following our contribution guidelines
- 📧 **Questions**: Open a discussion or contact the maintainers
### Resources
- [Anthropic Claude Documentation](https://docs.anthropic.com/)
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [Action Marketplace Listing](https://github.com/marketplace/actions/claude-code-review)
## Acknowledgments
Built with:
- [Anthropic Claude API](https://www.anthropic.com/claude) - AI-powered code analysis
- [GitHub Actions](https://github.com/features/actions) - CI/CD automation platform
- [@actions/core](https://github.com/actions/toolkit/tree/main/packages/core) - GitHub Actions toolkit
- [@vercel/ncc](https://github.com/vercel/ncc) - Node.js bundler
---
**Made with ❤️ by PacNPal** | [Report Bug](https://github.com/pacnpal/claude-code-review/issues) | [Request Feature](https://github.com/pacnpal/claude-code-review/issues)

501
action.js
View File

@@ -3,17 +3,110 @@ const core = require('@actions/core');
const github = require('@actions/github');
const { exec } = require('@actions/exec');
async function getPRDetails(octokit, context, prNumber) {
try {
console.log(`Getting details for PR #${prNumber}`);
// Get PR info
const { data: pr } = await octokit.rest.pulls.get({
...context.repo,
pull_number: parseInt(prNumber)
});
// Constants for retry logic
const MAX_RETRIES = 4;
const INITIAL_BACKOFF_MS = 2000;
const MAX_DIFF_SIZE = 100000; // ~100KB to stay well under API limits
const API_TIMEOUT_MS = 120000; // 2 minutes
return {
/**
* Sleep for specified milliseconds
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry a function with exponential backoff
* @param {Function} fn - Async function to retry
* @param {string} operation - Description of operation for logging
* @param {number} maxRetries - Maximum number of retries
* @returns {Promise<any>} Result of the function
*/
async function retryWithBackoff(fn, operation, maxRetries = MAX_RETRIES) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
core.info(`Retry attempt ${attempt}/${maxRetries} for ${operation} after ${backoffMs}ms`);
await sleep(backoffMs);
}
return await fn();
} catch (error) {
lastError = error;
// Don't retry on authentication or validation errors
if (error.status === 401 || error.status === 403 || error.status === 400) {
core.error(`${operation} failed with non-retryable error: ${error.message}`);
throw error;
}
if (attempt < maxRetries) {
core.warning(`${operation} failed (attempt ${attempt + 1}/${maxRetries + 1}): ${error.message}`);
} else {
core.error(`${operation} failed after ${maxRetries + 1} attempts`);
}
}
}
throw lastError;
}
/**
* Validate input parameters
* @param {string} token - GitHub token
* @param {string} anthropicKey - Anthropic API key
* @param {string|number} prNumber - PR number
*/
function validateInputs(token, anthropicKey, prNumber) {
if (!token || token.trim() === '') {
throw new Error('github-token is required and cannot be empty');
}
if (!anthropicKey || anthropicKey.trim() === '') {
throw new Error('anthropic-key is required and cannot be empty');
}
if (!anthropicKey.startsWith('sk-ant-')) {
core.warning('anthropic-key does not match expected format (should start with sk-ant-)');
}
const prNum = parseInt(prNumber);
if (isNaN(prNum) || prNum <= 0) {
throw new Error(`Invalid PR number: ${prNumber}. Must be a positive integer.`);
}
core.info('✓ Input validation passed');
return prNum;
}
/**
* Get PR details from GitHub API with retry logic
* @param {Object} octokit - GitHub API client
* @param {Object} context - GitHub context
* @param {number} prNumber - PR number
* @returns {Promise<Object>} PR details
*/
async function getPRDetails(octokit, context, prNumber) {
core.startGroup('Fetching PR details');
try {
core.info(`Getting details for PR #${prNumber}`);
const { data: pr } = await retryWithBackoff(
async () => await octokit.rest.pulls.get({
...context.repo,
pull_number: prNumber
}),
'Get PR details'
);
const result = {
number: pr.number,
base: {
sha: pr.base.sha,
@@ -22,59 +115,154 @@ async function getPRDetails(octokit, context, prNumber) {
head: {
sha: pr.head.sha,
ref: pr.head.ref
}
},
title: pr.title,
state: pr.state
};
core.info(`✓ Retrieved PR #${result.number}: "${result.title}"`);
core.info(` Base: ${result.base.ref} (${result.base.sha.substring(0, 7)})`);
core.info(` Head: ${result.head.ref} (${result.head.sha.substring(0, 7)})`);
core.debug(`PR state: ${result.state}`);
return result;
} catch (error) {
throw new Error(`Failed to get PR details: ${error.message}`);
core.error(`Failed to get PR details: ${error.message}`);
throw new Error(`Failed to get PR details for #${prNumber}: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Setup git configuration with retry logic
* @returns {Promise<void>}
*/
async function setupGitConfig() {
// Configure git to fetch PR refs
await exec('git', ['config', '--local', '--add', 'remote.origin.fetch', '+refs/pull/*/head:refs/remotes/origin/pr/*']);
await exec('git', ['fetch', 'origin']);
core.startGroup('Setting up Git configuration');
try {
core.info('Configuring git to fetch PR refs...');
await retryWithBackoff(
async () => await exec('git', ['config', '--local', '--add', 'remote.origin.fetch', '+refs/pull/*/head:refs/remotes/origin/pr/*']),
'Git config fetch refs'
);
core.info('Fetching from origin...');
await retryWithBackoff(
async () => await exec('git', ['fetch', 'origin']),
'Git fetch origin'
);
core.info('Setting git user identity...');
await exec('git', ['config', '--global', 'user.name', 'claude-code-review[bot]']);
await exec('git', ['config', '--global', 'user.email', 'claude-code-review[bot]@users.noreply.github.com']);
core.info('✓ Git configuration completed');
} catch (error) {
core.error(`Git configuration failed: ${error.message}`);
throw new Error(`Failed to configure git: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Generate diff between two commits with size validation
* @param {string} baseSha - Base commit SHA
* @param {string} headSha - Head commit SHA
* @returns {Promise<string>} Diff content
*/
async function getDiff(baseSha, headSha) {
core.startGroup('Generating diff');
let diffContent = '';
let stderr = '';
try {
core.info(`Generating diff between ${baseSha.substring(0, 7)} and ${headSha.substring(0, 7)}`);
// Get the full diff with context
await exec('git', ['diff', '-U10', baseSha, headSha], {
listeners: {
stdout: (data) => {
diffContent += data.toString();
},
stderr: (data) => {
stderr += data.toString();
}
}
});
// Filter for relevant files
const lines = diffContent.split('\n');
let filtered = '';
let keep = false;
for (const line of lines) {
if (line.startsWith('diff --git')) {
keep = false;
// Check if file type should be included
if (line.match(/\.(js|ts|py|cpp|h|java|cs)$/) &&
!line.match(/(package-lock\.json|yarn\.lock|\.md|\.json)/)) {
keep = true;
}
}
if (keep) {
filtered += line + '\n';
}
if (stderr) {
core.debug(`Git diff stderr: ${stderr}`);
}
return filtered;
const diffSize = Buffer.byteLength(diffContent, 'utf8');
core.info(`✓ Diff generated: ${diffSize} bytes`);
if (diffSize === 0) {
core.warning('Diff is empty - no changes found');
return '';
}
if (diffSize > MAX_DIFF_SIZE) {
core.warning(`Diff size (${diffSize} bytes) exceeds maximum (${MAX_DIFF_SIZE} bytes)`);
const truncated = diffContent.substring(0, MAX_DIFF_SIZE);
const lines = truncated.split('\n').length;
core.warning(`Diff truncated to ${MAX_DIFF_SIZE} bytes (~${lines} lines)`);
return truncated + '\n\n[... diff truncated due to size ...]';
}
const lines = diffContent.split('\n').length;
core.info(`Diff contains ${lines} lines`);
return diffContent;
} catch (error) {
core.error(`Failed to generate diff: ${error.message}`);
if (stderr) {
core.error(`Git stderr: ${stderr}`);
}
throw new Error(`Failed to generate diff: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Create fetch with timeout
* @param {string} url - URL to fetch
* @param {Object} options - Fetch options
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<Response>} Fetch response
*/
async function fetchWithTimeout(url, options, timeout = API_TIMEOUT_MS) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Analyze code diff with Claude API including retry logic and proper error handling
* @param {string} diffContent - Code diff to analyze
* @param {string} anthropicKey - Anthropic API key
* @returns {Promise<string|null>} Code review text
*/
async function analyzeWithClaude(diffContent, anthropicKey) {
if (!diffContent.trim()) {
core.startGroup('Analyzing with Claude AI');
if (!diffContent || !diffContent.trim()) {
core.warning('Diff content is empty, skipping analysis');
core.endGroup();
return null;
}
@@ -94,7 +282,9 @@ For each issue found:
- Provide specific recommendations for fixes
- Include code examples where helpful
If no issues are found in a particular area, explicitly state that.
- If no issues are found in a particular area, explicitly state that.
- If it's a dependency update, evaluate with strict scrutiny the implications of the change.
- No matter your findings, give a summary of the pull request.
Here is the code diff to review:
@@ -103,109 +293,236 @@ ${diffContent}
\`\`\``;
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': anthropicKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-3-sonnet-20240229',
max_tokens: 4096,
temperature: 0.7,
messages: [{
role: 'user',
content: prompt
}]
})
});
core.info('Sending request to Claude API...');
core.debug(`Prompt length: ${prompt.length} characters`);
const data = await response.json();
if (!data.content?.[0]?.text) {
throw new Error(`API Error: ${JSON.stringify(data)}`);
const review = await retryWithBackoff(async () => {
const response = await fetchWithTimeout('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': anthropicKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 4096,
temperature: 0.7,
messages: [{
role: 'user',
content: prompt
}]
})
}, API_TIMEOUT_MS);
// Check HTTP status
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error?.message || errorData.message || errorText;
} catch {
errorMessage = errorText;
}
// Create error with status for retry logic
const error = new Error(`API returned ${response.status}: ${errorMessage}`);
error.status = response.status;
// Handle rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('retry-after');
if (retryAfter) {
core.warning(`Rate limited. Retry after ${retryAfter} seconds`);
error.retryAfter = parseInt(retryAfter) * 1000;
}
}
throw error;
}
const data = await response.json();
if (!data.content?.[0]?.text) {
throw new Error(`Invalid API response: missing content. Response: ${JSON.stringify(data).substring(0, 200)}`);
}
return data.content[0].text;
}, 'Claude API request', 3); // Use fewer retries for API calls
const reviewLength = review.length;
core.info(`✓ Received review from Claude: ${reviewLength} characters`);
return review;
} catch (error) {
if (error.name === 'AbortError') {
core.error(`Claude API request timed out after ${API_TIMEOUT_MS}ms`);
throw new Error(`Claude API request timed out after ${API_TIMEOUT_MS / 1000} seconds`);
}
return data.content[0].text;
} catch (error) {
throw new Error(`Claude API error: ${error.message}`);
core.error(`Claude API error: ${error.message}`);
throw new Error(`Failed to analyze with Claude: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Post code review as a comment on the PR with retry logic
* @param {Object} octokit - GitHub API client
* @param {Object} context - GitHub context
* @param {string} review - Review text
* @param {number} prNumber - PR number
* @returns {Promise<void>}
*/
async function postReview(octokit, context, review, prNumber) {
try {
// Escape special characters for proper formatting
const escapedReview = review
.replace(/(?<=[\s\n])`([^`]+)`(?=[\s\n])/g, '\\`$1\\`')
.replace(/```/g, '\\`\\`\\`')
.replace(/\${/g, '\\${');
core.startGroup('Posting review comment');
try {
core.info(`Posting review to PR #${prNumber}...`);
core.debug(`Review length: ${review.length} characters`);
// GitHub markdown handles most content correctly without escaping
// Only ensure the review doesn't break the comment
const body = `# 🤖 Claude Code Review\n\n${review}`;
const comment = await retryWithBackoff(
async () => await octokit.rest.issues.createComment({
...context.repo,
issue_number: prNumber,
body: body
}),
'Post review comment'
);
core.info(`✓ Review posted successfully`);
core.debug(`Comment ID: ${comment.data.id}`);
core.debug(`Comment URL: ${comment.data.html_url}`);
await octokit.rest.issues.createComment({
...context.repo,
issue_number: prNumber,
body: `# Claude Code Review\n\n${escapedReview}`
});
} catch (error) {
core.error(`Failed to post review comment: ${error.message}`);
throw new Error(`Failed to post review: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Main execution function
*/
async function run() {
const startTime = Date.now();
try {
core.info('🚀 Starting Claude Code Review Action');
core.info(`Node version: ${process.version}`);
core.info(`Platform: ${process.platform}`);
// Get inputs
const token = core.getInput('github-token', { required: true });
const anthropicKey = core.getInput('anthropic-key', { required: true });
// Initialize GitHub client
const octokit = github.getOctokit(token);
const context = github.context;
let prNumber = core.getInput('pr-number');
const autoReview = core.getInput('auto-review', { required: false }) || 'true';
// Get PR number from event or input
let prNumber;
if (context.eventName === 'pull_request') {
prNumber = context.payload.pull_request.number;
} else {
prNumber = core.getInput('pr-number', { required: true });
// Check if auto-review is disabled
if (autoReview.toLowerCase() === 'false') {
core.info('⏭️ Auto-review is disabled, skipping code review');
core.setOutput('diff_size', '0');
core.setOutput('review', 'Auto-review disabled');
return;
}
// Get PR number from event if not provided
const context = github.context;
if (!prNumber && context.eventName === 'pull_request') {
prNumber = context.payload.pull_request?.number;
}
// Validate inputs
const validatedPrNumber = validateInputs(token, anthropicKey, prNumber);
// Mask sensitive data in logs
core.setSecret(anthropicKey);
// Initialize GitHub client
core.info('Initializing GitHub client...');
const octokit = github.getOctokit(token);
core.info(`Repository: ${context.repo.owner}/${context.repo.repo}`);
core.info(`Event: ${context.eventName}`);
// Set up git configuration
await setupGitConfig();
// Get PR details
const pr = await getPRDetails(octokit, context, prNumber);
console.log(`Retrieved details for PR #${pr.number}`);
const pr = await getPRDetails(octokit, context, validatedPrNumber);
// Validate PR state
if (pr.state === 'closed') {
core.warning(`PR #${pr.number} is closed. Review will still be posted.`);
}
// Generate diff
console.log('Generating diff...');
const diff = await getDiff(pr.base.sha, pr.head.sha);
if (!diff) {
console.log('No relevant changes found');
if (!diff || diff.trim() === '') {
core.warning('No changes found in diff');
core.setOutput('diff_size', '0');
core.setOutput('review', 'No changes to review');
core.info('✓ Action completed (no changes to review)');
return;
}
core.setOutput('diff_size', diff.length.toString());
const diffSize = Buffer.byteLength(diff, 'utf8');
core.setOutput('diff_size', diffSize.toString());
// Analyze with Claude
console.log('Analyzing with Claude...');
const review = await analyzeWithClaude(diff, anthropicKey);
if (!review) {
console.log('No review generated');
core.warning('No review generated by Claude');
core.setOutput('review', '');
core.info('✓ Action completed (no review generated)');
return;
}
// Post review
console.log('Posting review...');
await postReview(octokit, context, review, pr.number);
// Set outputs
core.setOutput('review', review);
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
core.info(`✓ Claude Code Review completed successfully in ${duration}s`);
} catch (error) {
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
core.error(`✗ Action failed after ${duration}s`);
core.error(`Error: ${error.message}`);
if (error.stack) {
core.debug(`Stack trace: ${error.stack}`);
}
core.setFailed(error.message);
process.exit(1);
}
}
run();
// Handle unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
core.error('Unhandled Rejection at:', promise);
core.error('Reason:', reason);
core.setFailed(`Unhandled rejection: ${reason}`);
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
core.error('Uncaught Exception:', error);
core.setFailed(`Uncaught exception: ${error.message}`);
process.exit(1);
});
run();

View File

@@ -11,6 +11,10 @@ inputs:
pr-number:
description: 'Pull request number'
required: true
auto-review:
description: 'Enable automatic code reviews (set to false to skip review)'
required: false
default: 'true'
outputs:
diff_size:
description: 'Size of the relevant code changes'
@@ -18,4 +22,4 @@ outputs:
description: 'Generated code review'
runs:
using: 'node20'
main: 'action.js'
main: 'dist/index.js'

643
dist/index.js vendored
View File

@@ -1882,6 +1882,7 @@ class Context {
this.action = process.env.GITHUB_ACTION;
this.actor = process.env.GITHUB_ACTOR;
this.job = process.env.GITHUB_JOB;
this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT, 10);
this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER, 10);
this.runId = parseInt(process.env.GITHUB_RUN_ID, 10);
this.apiUrl = (_a = process.env.GITHUB_API_URL) !== null && _a !== void 0 ? _a : `https://api.github.com`;
@@ -3557,11 +3558,11 @@ var __copyProps = (to, from, except, desc) => {
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// pkg/dist-src/index.js
var dist_src_exports = {};
__export(dist_src_exports, {
var index_exports = {};
__export(index_exports, {
Octokit: () => Octokit
});
module.exports = __toCommonJS(dist_src_exports);
module.exports = __toCommonJS(index_exports);
var import_universal_user_agent = __nccwpck_require__(3843);
var import_before_after_hook = __nccwpck_require__(2732);
var import_request = __nccwpck_require__(8636);
@@ -3569,13 +3570,28 @@ var import_graphql = __nccwpck_require__(7);
var import_auth_token = __nccwpck_require__(7864);
// pkg/dist-src/version.js
var VERSION = "5.2.0";
var VERSION = "5.2.2";
// pkg/dist-src/index.js
var noop = () => {
};
var consoleWarn = console.warn.bind(console);
var consoleError = console.error.bind(console);
function createLogger(logger = {}) {
if (typeof logger.debug !== "function") {
logger.debug = noop;
}
if (typeof logger.info !== "function") {
logger.info = noop;
}
if (typeof logger.warn !== "function") {
logger.warn = consoleWarn;
}
if (typeof logger.error !== "function") {
logger.error = consoleError;
}
return logger;
}
var userAgentTrail = `octokit-core.js/${VERSION} ${(0, import_universal_user_agent.getUserAgent)()}`;
var Octokit = class {
static {
@@ -3649,15 +3665,7 @@ var Octokit = class {
}
this.request = import_request.request.defaults(requestDefaults);
this.graphql = (0, import_graphql.withCustomRequest)(this.request).defaults(requestDefaults);
this.log = Object.assign(
{
debug: noop,
info: noop,
warn: consoleWarn,
error: consoleError
},
options.log
);
this.log = createLogger(options.log);
this.hook = hook;
if (!options.authStrategy) {
if (!options.auth) {
@@ -3736,7 +3744,7 @@ module.exports = __toCommonJS(dist_src_exports);
var import_universal_user_agent = __nccwpck_require__(3843);
// pkg/dist-src/version.js
var VERSION = "9.0.5";
var VERSION = "9.0.6";
// pkg/dist-src/defaults.js
var userAgent = `octokit-endpoint.js/${VERSION} ${(0, import_universal_user_agent.getUserAgent)()}`;
@@ -3841,9 +3849,9 @@ function addQueryParameters(url, parameters) {
}
// pkg/dist-src/util/extract-url-variable-names.js
var urlVariableRegex = /\{[^}]+\}/g;
var urlVariableRegex = /\{[^{}}]+\}/g;
function removeNonChars(variableName) {
return variableName.replace(/^\W+|\W+$/g, "").split(/,/);
return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
}
function extractUrlVariableNames(url) {
const matches = url.match(urlVariableRegex);
@@ -4029,7 +4037,7 @@ function parse(options) {
}
if (url.endsWith("/graphql")) {
if (options.mediaType.previews?.length) {
const previewsFromAcceptHeader = headers.accept.match(/[\w-]+(?=-preview)/g) || [];
const previewsFromAcceptHeader = headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || [];
headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
return `application/vnd.github.${preview}-preview${format}`;
@@ -4110,18 +4118,18 @@ var __copyProps = (to, from, except, desc) => {
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// pkg/dist-src/index.js
var dist_src_exports = {};
__export(dist_src_exports, {
var index_exports = {};
__export(index_exports, {
GraphqlResponseError: () => GraphqlResponseError,
graphql: () => graphql2,
withCustomRequest: () => withCustomRequest
});
module.exports = __toCommonJS(dist_src_exports);
module.exports = __toCommonJS(index_exports);
var import_request3 = __nccwpck_require__(8636);
var import_universal_user_agent = __nccwpck_require__(3843);
// pkg/dist-src/version.js
var VERSION = "7.1.0";
var VERSION = "7.1.1";
// pkg/dist-src/with-defaults.js
var import_request2 = __nccwpck_require__(8636);
@@ -4169,8 +4177,7 @@ function graphql(request2, query, options) {
);
}
for (const key in options) {
if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key))
continue;
if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key)) continue;
return Promise.reject(
new Error(
`[@octokit/graphql] "${key}" cannot be used as variable name`
@@ -4278,7 +4285,7 @@ __export(dist_src_exports, {
module.exports = __toCommonJS(dist_src_exports);
// pkg/dist-src/version.js
var VERSION = "9.2.1";
var VERSION = "9.2.2";
// pkg/dist-src/normalize-paginated-list-response.js
function normalizePaginatedListResponse(response) {
@@ -4326,7 +4333,7 @@ function iterator(octokit, route, parameters) {
const response = await requestMethod({ method, url, headers });
const normalizedResponse = normalizePaginatedListResponse(response);
url = ((normalizedResponse.headers.link || "").match(
/<([^>]+)>;\s*rel="next"/
/<([^<>]+)>;\s*rel="next"/
) || [])[1];
return { value: normalizedResponse };
} catch (error) {
@@ -6878,7 +6885,7 @@ var RequestError = class extends Error {
if (options.request.headers.authorization) {
requestCopy.headers = Object.assign({}, options.request.headers, {
authorization: options.request.headers.authorization.replace(
/ .*$/,
/(?<! ) .*$/,
" [REDACTED]"
)
});
@@ -6946,7 +6953,7 @@ var import_endpoint = __nccwpck_require__(4471);
var import_universal_user_agent = __nccwpck_require__(3843);
// pkg/dist-src/version.js
var VERSION = "8.4.0";
var VERSION = "8.4.1";
// pkg/dist-src/is-plain-object.js
function isPlainObject(value) {
@@ -7005,7 +7012,7 @@ function fetchWrapper(requestOptions) {
headers[keyAndValue[0]] = keyAndValue[1];
}
if ("deprecation" in headers) {
const matches = headers.link && headers.link.match(/<([^>]+)>; rel="deprecation"/);
const matches = headers.link && headers.link.match(/<([^<>]+)>; rel="deprecation"/);
const deprecationLink = matches && matches.pop();
log.warn(
`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
@@ -13009,7 +13016,7 @@ module.exports = {
const { parseSetCookie } = __nccwpck_require__(8915)
const { stringify, getHeadersList } = __nccwpck_require__(3834)
const { stringify } = __nccwpck_require__(3834)
const { webidl } = __nccwpck_require__(4222)
const { Headers } = __nccwpck_require__(6349)
@@ -13085,14 +13092,13 @@ function getSetCookies (headers) {
webidl.brandCheck(headers, Headers, { strict: false })
const cookies = getHeadersList(headers).cookies
const cookies = headers.getSetCookie()
if (!cookies) {
return []
}
// In older versions of undici, cookies is a list of name:value.
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
return cookies.map((pair) => parseSetCookie(pair))
}
/**
@@ -13520,14 +13526,15 @@ module.exports = {
/***/ }),
/***/ 3834:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
/***/ ((module) => {
"use strict";
const assert = __nccwpck_require__(2613)
const { kHeadersList } = __nccwpck_require__(6443)
/**
* @param {string} value
* @returns {boolean}
*/
function isCTLExcludingHtab (value) {
if (value.length === 0) {
return false
@@ -13788,31 +13795,13 @@ function stringify (cookie) {
return out.join('; ')
}
let kHeadersListNode
function getHeadersList (headers) {
if (headers[kHeadersList]) {
return headers[kHeadersList]
}
if (!kHeadersListNode) {
kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
(symbol) => symbol.description === 'headers list'
)
assert(kHeadersListNode, 'Headers cannot be parsed')
}
const headersList = headers[kHeadersListNode]
assert(headersList)
return headersList
}
module.exports = {
isCTLExcludingHtab,
stringify,
getHeadersList
validateCookieName,
validateCookiePath,
validateCookieValue,
toIMFDate,
stringify
}
@@ -15741,6 +15730,14 @@ const { isUint8Array, isArrayBuffer } = __nccwpck_require__(8253)
const { File: UndiciFile } = __nccwpck_require__(3041)
const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(4322)
let random
try {
const crypto = __nccwpck_require__(7598)
random = (max) => crypto.randomInt(0, max)
} catch {
random = (max) => Math.floor(Math.random(max))
}
let ReadableStream = globalThis.ReadableStream
/** @type {globalThis['File']} */
@@ -15826,7 +15823,7 @@ function extractBody (object, keepalive = false) {
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) {
const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
@@ -17808,6 +17805,7 @@ const {
isValidHeaderName,
isValidHeaderValue
} = __nccwpck_require__(5523)
const util = __nccwpck_require__(9023)
const { webidl } = __nccwpck_require__(4222)
const assert = __nccwpck_require__(2613)
@@ -18361,6 +18359,9 @@ Object.defineProperties(Headers.prototype, {
[Symbol.toStringTag]: {
value: 'Headers',
configurable: true
},
[util.inspect.custom]: {
enumerable: false
}
})
@@ -27537,6 +27538,20 @@ class Pool extends PoolBase {
? { ...options.interceptors }
: undefined
this[kFactory] = factory
this.on('connectionError', (origin, targets, error) => {
// If a connection error occurs, we remove the client from the pool,
// and emit a connectionError event. They will not be re-used.
// Fixes https://github.com/nodejs/undici/issues/3895
for (const target of targets) {
// Do not use kRemoveClient here, as it will close the client,
// but the client cannot be closed in this state.
const idx = this[kClients].indexOf(target)
if (idx !== -1) {
this[kClients].splice(idx, 1)
}
}
})
}
[kGetDispatcher] () {
@@ -30011,6 +30026,14 @@ module.exports = require("net");
/***/ }),
/***/ 7598:
/***/ ((module) => {
"use strict";
module.exports = require("node:crypto");
/***/ }),
/***/ 8474:
/***/ ((module) => {
@@ -31816,17 +31839,110 @@ const core = __nccwpck_require__(7484);
const github = __nccwpck_require__(3228);
const { exec } = __nccwpck_require__(5236);
async function getPRDetails(octokit, context, prNumber) {
try {
console.log(`Getting details for PR #${prNumber}`);
// Get PR info
const { data: pr } = await octokit.rest.pulls.get({
...context.repo,
pull_number: parseInt(prNumber)
});
// Constants for retry logic
const MAX_RETRIES = 4;
const INITIAL_BACKOFF_MS = 2000;
const MAX_DIFF_SIZE = 100000; // ~100KB to stay well under API limits
const API_TIMEOUT_MS = 120000; // 2 minutes
return {
/**
* Sleep for specified milliseconds
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry a function with exponential backoff
* @param {Function} fn - Async function to retry
* @param {string} operation - Description of operation for logging
* @param {number} maxRetries - Maximum number of retries
* @returns {Promise<any>} Result of the function
*/
async function retryWithBackoff(fn, operation, maxRetries = MAX_RETRIES) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
core.info(`Retry attempt ${attempt}/${maxRetries} for ${operation} after ${backoffMs}ms`);
await sleep(backoffMs);
}
return await fn();
} catch (error) {
lastError = error;
// Don't retry on authentication or validation errors
if (error.status === 401 || error.status === 403 || error.status === 400) {
core.error(`${operation} failed with non-retryable error: ${error.message}`);
throw error;
}
if (attempt < maxRetries) {
core.warning(`${operation} failed (attempt ${attempt + 1}/${maxRetries + 1}): ${error.message}`);
} else {
core.error(`${operation} failed after ${maxRetries + 1} attempts`);
}
}
}
throw lastError;
}
/**
* Validate input parameters
* @param {string} token - GitHub token
* @param {string} anthropicKey - Anthropic API key
* @param {string|number} prNumber - PR number
*/
function validateInputs(token, anthropicKey, prNumber) {
if (!token || token.trim() === '') {
throw new Error('github-token is required and cannot be empty');
}
if (!anthropicKey || anthropicKey.trim() === '') {
throw new Error('anthropic-key is required and cannot be empty');
}
if (!anthropicKey.startsWith('sk-ant-')) {
core.warning('anthropic-key does not match expected format (should start with sk-ant-)');
}
const prNum = parseInt(prNumber);
if (isNaN(prNum) || prNum <= 0) {
throw new Error(`Invalid PR number: ${prNumber}. Must be a positive integer.`);
}
core.info('✓ Input validation passed');
return prNum;
}
/**
* Get PR details from GitHub API with retry logic
* @param {Object} octokit - GitHub API client
* @param {Object} context - GitHub context
* @param {number} prNumber - PR number
* @returns {Promise<Object>} PR details
*/
async function getPRDetails(octokit, context, prNumber) {
core.startGroup('Fetching PR details');
try {
core.info(`Getting details for PR #${prNumber}`);
const { data: pr } = await retryWithBackoff(
async () => await octokit.rest.pulls.get({
...context.repo,
pull_number: prNumber
}),
'Get PR details'
);
const result = {
number: pr.number,
base: {
sha: pr.base.sha,
@@ -31835,59 +31951,154 @@ async function getPRDetails(octokit, context, prNumber) {
head: {
sha: pr.head.sha,
ref: pr.head.ref
}
},
title: pr.title,
state: pr.state
};
core.info(`✓ Retrieved PR #${result.number}: "${result.title}"`);
core.info(` Base: ${result.base.ref} (${result.base.sha.substring(0, 7)})`);
core.info(` Head: ${result.head.ref} (${result.head.sha.substring(0, 7)})`);
core.debug(`PR state: ${result.state}`);
return result;
} catch (error) {
throw new Error(`Failed to get PR details: ${error.message}`);
core.error(`Failed to get PR details: ${error.message}`);
throw new Error(`Failed to get PR details for #${prNumber}: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Setup git configuration with retry logic
* @returns {Promise<void>}
*/
async function setupGitConfig() {
// Configure git to fetch PR refs
await exec('git', ['config', '--local', '--add', 'remote.origin.fetch', '+refs/pull/*/head:refs/remotes/origin/pr/*']);
await exec('git', ['fetch', 'origin']);
core.startGroup('Setting up Git configuration');
try {
core.info('Configuring git to fetch PR refs...');
await retryWithBackoff(
async () => await exec('git', ['config', '--local', '--add', 'remote.origin.fetch', '+refs/pull/*/head:refs/remotes/origin/pr/*']),
'Git config fetch refs'
);
core.info('Fetching from origin...');
await retryWithBackoff(
async () => await exec('git', ['fetch', 'origin']),
'Git fetch origin'
);
core.info('Setting git user identity...');
await exec('git', ['config', '--global', 'user.name', 'claude-code-review[bot]']);
await exec('git', ['config', '--global', 'user.email', 'claude-code-review[bot]@users.noreply.github.com']);
core.info('✓ Git configuration completed');
} catch (error) {
core.error(`Git configuration failed: ${error.message}`);
throw new Error(`Failed to configure git: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Generate diff between two commits with size validation
* @param {string} baseSha - Base commit SHA
* @param {string} headSha - Head commit SHA
* @returns {Promise<string>} Diff content
*/
async function getDiff(baseSha, headSha) {
core.startGroup('Generating diff');
let diffContent = '';
let stderr = '';
try {
core.info(`Generating diff between ${baseSha.substring(0, 7)} and ${headSha.substring(0, 7)}`);
// Get the full diff with context
await exec('git', ['diff', '-U10', baseSha, headSha], {
listeners: {
stdout: (data) => {
diffContent += data.toString();
},
stderr: (data) => {
stderr += data.toString();
}
}
});
// Filter for relevant files
const lines = diffContent.split('\n');
let filtered = '';
let keep = false;
for (const line of lines) {
if (line.startsWith('diff --git')) {
keep = false;
// Check if file type should be included
if (line.match(/\.(js|ts|py|cpp|h|java|cs)$/) &&
!line.match(/(package-lock\.json|yarn\.lock|\.md|\.json)/)) {
keep = true;
}
}
if (keep) {
filtered += line + '\n';
}
if (stderr) {
core.debug(`Git diff stderr: ${stderr}`);
}
return filtered;
const diffSize = Buffer.byteLength(diffContent, 'utf8');
core.info(`✓ Diff generated: ${diffSize} bytes`);
if (diffSize === 0) {
core.warning('Diff is empty - no changes found');
return '';
}
if (diffSize > MAX_DIFF_SIZE) {
core.warning(`Diff size (${diffSize} bytes) exceeds maximum (${MAX_DIFF_SIZE} bytes)`);
const truncated = diffContent.substring(0, MAX_DIFF_SIZE);
const lines = truncated.split('\n').length;
core.warning(`Diff truncated to ${MAX_DIFF_SIZE} bytes (~${lines} lines)`);
return truncated + '\n\n[... diff truncated due to size ...]';
}
const lines = diffContent.split('\n').length;
core.info(`Diff contains ${lines} lines`);
return diffContent;
} catch (error) {
core.error(`Failed to generate diff: ${error.message}`);
if (stderr) {
core.error(`Git stderr: ${stderr}`);
}
throw new Error(`Failed to generate diff: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Create fetch with timeout
* @param {string} url - URL to fetch
* @param {Object} options - Fetch options
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<Response>} Fetch response
*/
async function fetchWithTimeout(url, options, timeout = API_TIMEOUT_MS) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Analyze code diff with Claude API including retry logic and proper error handling
* @param {string} diffContent - Code diff to analyze
* @param {string} anthropicKey - Anthropic API key
* @returns {Promise<string|null>} Code review text
*/
async function analyzeWithClaude(diffContent, anthropicKey) {
if (!diffContent.trim()) {
core.startGroup('Analyzing with Claude AI');
if (!diffContent || !diffContent.trim()) {
core.warning('Diff content is empty, skipping analysis');
core.endGroup();
return null;
}
@@ -31907,7 +32118,9 @@ For each issue found:
- Provide specific recommendations for fixes
- Include code examples where helpful
If no issues are found in a particular area, explicitly state that.
- If no issues are found in a particular area, explicitly state that.
- If it's a dependency update, evaluate with strict scrutiny the implications of the change.
- No matter your findings, give a summary of the pull request.
Here is the code diff to review:
@@ -31916,112 +32129,240 @@ ${diffContent}
\`\`\``;
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': anthropicKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-3-sonnet-20240229',
max_tokens: 4096,
temperature: 0.7,
messages: [{
role: 'user',
content: prompt
}]
})
});
core.info('Sending request to Claude API...');
core.debug(`Prompt length: ${prompt.length} characters`);
const data = await response.json();
if (!data.content?.[0]?.text) {
throw new Error(`API Error: ${JSON.stringify(data)}`);
const review = await retryWithBackoff(async () => {
const response = await fetchWithTimeout('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': anthropicKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 4096,
temperature: 0.7,
messages: [{
role: 'user',
content: prompt
}]
})
}, API_TIMEOUT_MS);
// Check HTTP status
if (!response.ok) {
const errorText = await response.text();
let errorMessage;
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error?.message || errorData.message || errorText;
} catch {
errorMessage = errorText;
}
// Create error with status for retry logic
const error = new Error(`API returned ${response.status}: ${errorMessage}`);
error.status = response.status;
// Handle rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('retry-after');
if (retryAfter) {
core.warning(`Rate limited. Retry after ${retryAfter} seconds`);
error.retryAfter = parseInt(retryAfter) * 1000;
}
}
throw error;
}
const data = await response.json();
if (!data.content?.[0]?.text) {
throw new Error(`Invalid API response: missing content. Response: ${JSON.stringify(data).substring(0, 200)}`);
}
return data.content[0].text;
}, 'Claude API request', 3); // Use fewer retries for API calls
const reviewLength = review.length;
core.info(`✓ Received review from Claude: ${reviewLength} characters`);
return review;
} catch (error) {
if (error.name === 'AbortError') {
core.error(`Claude API request timed out after ${API_TIMEOUT_MS}ms`);
throw new Error(`Claude API request timed out after ${API_TIMEOUT_MS / 1000} seconds`);
}
return data.content[0].text;
} catch (error) {
throw new Error(`Claude API error: ${error.message}`);
core.error(`Claude API error: ${error.message}`);
throw new Error(`Failed to analyze with Claude: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Post code review as a comment on the PR with retry logic
* @param {Object} octokit - GitHub API client
* @param {Object} context - GitHub context
* @param {string} review - Review text
* @param {number} prNumber - PR number
* @returns {Promise<void>}
*/
async function postReview(octokit, context, review, prNumber) {
try {
// Escape special characters for proper formatting
const escapedReview = review
.replace(/(?<=[\s\n])`([^`]+)`(?=[\s\n])/g, '\\`$1\\`')
.replace(/```/g, '\\`\\`\\`')
.replace(/\${/g, '\\${');
core.startGroup('Posting review comment');
try {
core.info(`Posting review to PR #${prNumber}...`);
core.debug(`Review length: ${review.length} characters`);
// GitHub markdown handles most content correctly without escaping
// Only ensure the review doesn't break the comment
const body = `# 🤖 Claude Code Review\n\n${review}`;
const comment = await retryWithBackoff(
async () => await octokit.rest.issues.createComment({
...context.repo,
issue_number: prNumber,
body: body
}),
'Post review comment'
);
core.info(`✓ Review posted successfully`);
core.debug(`Comment ID: ${comment.data.id}`);
core.debug(`Comment URL: ${comment.data.html_url}`);
await octokit.rest.issues.createComment({
...context.repo,
issue_number: prNumber,
body: `# Claude Code Review\n\n${escapedReview}`
});
} catch (error) {
core.error(`Failed to post review comment: ${error.message}`);
throw new Error(`Failed to post review: ${error.message}`);
} finally {
core.endGroup();
}
}
/**
* Main execution function
*/
async function run() {
const startTime = Date.now();
try {
core.info('🚀 Starting Claude Code Review Action');
core.info(`Node version: ${process.version}`);
core.info(`Platform: ${process.platform}`);
// Get inputs
const token = core.getInput('github-token', { required: true });
const anthropicKey = core.getInput('anthropic-key', { required: true });
// Initialize GitHub client
const octokit = github.getOctokit(token);
const context = github.context;
let prNumber = core.getInput('pr-number');
const autoReview = core.getInput('auto-review', { required: false }) || 'true';
// Get PR number from event or input
let prNumber;
if (context.eventName === 'pull_request') {
prNumber = context.payload.pull_request.number;
} else {
prNumber = core.getInput('pr-number', { required: true });
// Check if auto-review is disabled
if (autoReview.toLowerCase() === 'false') {
core.info('⏭️ Auto-review is disabled, skipping code review');
core.setOutput('diff_size', '0');
core.setOutput('review', 'Auto-review disabled');
return;
}
// Get PR number from event if not provided
const context = github.context;
if (!prNumber && context.eventName === 'pull_request') {
prNumber = context.payload.pull_request?.number;
}
// Validate inputs
const validatedPrNumber = validateInputs(token, anthropicKey, prNumber);
// Mask sensitive data in logs
core.setSecret(anthropicKey);
// Initialize GitHub client
core.info('Initializing GitHub client...');
const octokit = github.getOctokit(token);
core.info(`Repository: ${context.repo.owner}/${context.repo.repo}`);
core.info(`Event: ${context.eventName}`);
// Set up git configuration
await setupGitConfig();
// Get PR details
const pr = await getPRDetails(octokit, context, prNumber);
console.log(`Retrieved details for PR #${pr.number}`);
const pr = await getPRDetails(octokit, context, validatedPrNumber);
// Validate PR state
if (pr.state === 'closed') {
core.warning(`PR #${pr.number} is closed. Review will still be posted.`);
}
// Generate diff
console.log('Generating diff...');
const diff = await getDiff(pr.base.sha, pr.head.sha);
if (!diff) {
console.log('No relevant changes found');
if (!diff || diff.trim() === '') {
core.warning('No changes found in diff');
core.setOutput('diff_size', '0');
core.setOutput('review', 'No changes to review');
core.info('✓ Action completed (no changes to review)');
return;
}
core.setOutput('diff_size', diff.length.toString());
const diffSize = Buffer.byteLength(diff, 'utf8');
core.setOutput('diff_size', diffSize.toString());
// Analyze with Claude
console.log('Analyzing with Claude...');
const review = await analyzeWithClaude(diff, anthropicKey);
if (!review) {
console.log('No review generated');
core.warning('No review generated by Claude');
core.setOutput('review', '');
core.info('✓ Action completed (no review generated)');
return;
}
// Post review
console.log('Posting review...');
await postReview(octokit, context, review, pr.number);
// Set outputs
core.setOutput('review', review);
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
core.info(`✓ Claude Code Review completed successfully in ${duration}s`);
} catch (error) {
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
core.error(`✗ Action failed after ${duration}s`);
core.error(`Error: ${error.message}`);
if (error.stack) {
core.debug(`Stack trace: ${error.stack}`);
}
core.setFailed(error.message);
process.exit(1);
}
}
// Handle unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
core.error('Unhandled Rejection at:', promise);
core.error('Reason:', reason);
core.setFailed(`Unhandled rejection: ${reason}`);
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
core.error('Uncaught Exception:', error);
core.setFailed(`Uncaught exception: ${error.message}`);
process.exit(1);
});
run();
module.exports = __webpack_exports__;
/******/ })()
;

3163
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,17 @@
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"node-fetch": "^3.3.2"
"@actions/github": "^6.0.1",
"glob": "^11.1.0",
"node-fetch": "^3.3.2",
"js-yaml": "^4.1.1"
},
"devDependencies": {
"@vercel/ncc": "^0.38.3",
"jest": "^29.7.0"
"@vercel/ncc": "^0.38.4",
"jest": "^30.2.0",
"js-yaml": "^4.1.1"
},
"overrides": {
"js-yaml": "^4.1.1"
}
}