Files
Claude-code-review/action.js
2025-02-10 17:25:49 -05:00

197 lines
5.2 KiB
JavaScript

// 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']);
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']);
}
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();
}
}
});
return diffContent;
} 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.
- 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:
\`\`\`
${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-5-sonnet-20241022',
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();