From 7ba8e02de88baebc72a572e8be2fcc065e5a1c86 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:42:08 -0500 Subject: [PATCH] Initial commit --- .github/workflows/release.yml | 31 +++++ .gitignore | 2 + README.md | 137 ++++++++++++++++++++++ action.js | 211 ++++++++++++++++++++++++++++++++++ action.yml | 21 ++++ index.js | 1 + jsconfig.json | 27 +++++ package.json | 23 ++++ 8 files changed, 453 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 README.md create mode 100644 action.js create mode 100644 action.yml create mode 100644 index.js create mode 100644 jsconfig.json create mode 100644 package.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..799a381 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +# .github/workflows/release.yml +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6bba59..f568581 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* +.DS_Store +*.lockb diff --git a/README.md b/README.md new file mode 100644 index 0000000..98fb975 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# Claude Code Review Action + +A GitHub Action that performs automated code reviews using Claude AI. + +## 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 + +## Usage + +Add this to your GitHub workflow file (e.g. `.github/workflows/review.yml`): + +```yaml +name: Code Review + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: your-username/claude-code-review-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }} + pr-number: ${{ github.event.pull_request.number }} +``` + +## 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 + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `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 | + +## Outputs + +| Output | Description | +|--------|-------------| +| `diff_size` | Size of the relevant code changes | +| `review` | Generated code review content | + +## Review Format + +The action provides detailed code reviews covering: + +1. Potential conflicts with existing codebase +2. Code correctness and potential bugs +3. Security vulnerabilities and risks +4. Performance implications +5. Maintainability and readability issues +6. Adherence to best practices +7. Suggestions for improvements + +Each issue found includes: +- Clear problem explanation +- Severity rating (Critical/High/Medium/Low) +- Specific recommendations +- Code examples where helpful + +## Example Review + +```markdown +# Claude Code Review + +1. **Potential conflicts with existing codebase**: + - No apparent conflicts identified + +2. **Code correctness and potential bugs**: + - **Medium Severity**: Potential null pointer in user handling + - Recommendation: Add null check before accessing user object + +3. **Security vulnerabilities and risks**: + - **High Severity**: SQL injection vulnerability in query construction + - Recommendation: Use parameterized queries +``` + +## Development + +1. Clone the repository +2. Install dependencies: +```bash +npm install +``` + +3. Make changes to `action.js` + +4. Build the action: +```bash +npm run build +``` + +5. Run tests: +```bash +npm test +``` + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run tests +5. Submit a pull request + +## 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 + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for release history \ No newline at end of file diff --git a/action.js b/action.js new file mode 100644 index 0000000..e25b701 --- /dev/null +++ b/action.js @@ -0,0 +1,211 @@ +// action.js +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) + }); + + return { + number: pr.number, + base: { + sha: pr.base.sha, + ref: pr.base.ref + }, + head: { + sha: pr.head.sha, + ref: pr.head.ref + } + }; + } catch (error) { + throw new Error(`Failed to get PR details: ${error.message}`); + } +} + +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']); +} + +async function getDiff(baseSha, headSha) { + let diffContent = ''; + + try { + // Get the full diff with context + await exec('git', ['diff', '-U10', baseSha, headSha], { + listeners: { + stdout: (data) => { + diffContent += 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'; + } + } + + return filtered; + } catch (error) { + throw new Error(`Failed to generate diff: ${error.message}`); + } +} + +async function analyzeWithClaude(diffContent, anthropicKey) { + if (!diffContent.trim()) { + return null; + } + + const prompt = `You are performing a code review. Please analyze this code diff and provide a thorough review that covers: + +1. Potential conflicts with existing codebase +2. Code correctness and potential bugs +3. Security vulnerabilities or risks +4. Performance implications +5. Maintainability and readability issues +6. Adherence to best practices and coding standards +7. Suggestions for improvements + +For each issue found: +- Explain the problem clearly +- Rate the severity (Critical/High/Medium/Low) +- Provide specific recommendations for fixes +- Include code examples where helpful + +If no issues are found in a particular area, explicitly state that. + +Here is the code diff to review: + +\`\`\` +${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 + }] + }) + }); + + const data = await response.json(); + if (!data.content?.[0]?.text) { + throw new Error(`API Error: ${JSON.stringify(data)}`); + } + + return data.content[0].text; + } catch (error) { + throw new Error(`Claude API error: ${error.message}`); + } +} + +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, '\\${'); + + await octokit.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: `# Claude Code Review\n\n${escapedReview}` + }); + } catch (error) { + throw new Error(`Failed to post review: ${error.message}`); + } +} + +async function run() { + try { + // 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; + + // 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 }); + } + + // Set up git configuration + await setupGitConfig(); + + // Get PR details + const pr = await getPRDetails(octokit, context, prNumber); + console.log(`Retrieved details for PR #${pr.number}`); + + // Generate diff + console.log('Generating diff...'); + const diff = await getDiff(pr.base.sha, pr.head.sha); + + if (!diff) { + console.log('No relevant changes found'); + core.setOutput('diff_size', '0'); + return; + } + + core.setOutput('diff_size', diff.length.toString()); + + // Analyze with Claude + console.log('Analyzing with Claude...'); + const review = await analyzeWithClaude(diff, anthropicKey); + + if (!review) { + console.log('No review generated'); + return; + } + + // Post review + console.log('Posting review...'); + await postReview(octokit, context, review, pr.number); + + // Set outputs + core.setOutput('review', review); + + } catch (error) { + core.setFailed(error.message); + } +} + +run(); \ No newline at end of file diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..3b03f60 --- /dev/null +++ b/action.yml @@ -0,0 +1,21 @@ +# action.yml +name: 'Claude Code Review' +description: 'Automated code review using Claude' +inputs: + github-token: + description: 'GitHub token' + required: true + anthropic-key: + description: 'Anthropic API key' + required: true + pr-number: + description: 'Pull request number' + required: true +outputs: + diff_size: + description: 'Size of the relevant code changes' + review: + description: 'Generated code review' +runs: + using: 'node20' + main: 'action.js' \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..faa7a6a --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "claude-code-review-action", + "version": "1.0.0", + "description": "GitHub Action for code review using Claude", + "main": "action.js", + "scripts": { + "test": "jest", + "build": "ncc build action.js -o dist" + }, + "keywords": ["github", "action", "code-review", "claude"], + "author": "PacNPal", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "@actions/exec": "^1.1.1", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.3", + "jest": "^29.7.0" + } +} \ No newline at end of file