mirror of
https://github.com/pacnpal/Claude-code-review.git
synced 2025-12-20 12:11:09 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa1ffd4d99 | ||
|
|
b516288da6 | ||
|
|
7fae28e1ff | ||
|
|
5258f1bd84 | ||
|
|
0c9c65f6d5 | ||
|
|
9014acfb36 | ||
|
|
3d441dcd1d | ||
|
|
183d49dd6a | ||
|
|
4c895868e7 | ||
|
|
47ead35362 | ||
|
|
785bb103c6 | ||
|
|
d45693ae1c | ||
|
|
a9173964c2 | ||
|
|
acd58d30ee | ||
|
|
d1c6a98994 | ||
|
|
e6777037d5 | ||
|
|
c6c00d8c95 | ||
|
|
45e3331bcb | ||
|
|
dd1b18e57a | ||
|
|
1b84aff159 | ||
|
|
5fb151d172 | ||
|
|
4e1bd4b9e5 | ||
|
|
7276f1e2c5 | ||
|
|
2c57b7a5ba | ||
|
|
096a52cda5 | ||
|
|
0356960aa8 | ||
|
|
a5bd0e4bcf | ||
|
|
e49cf9d908 | ||
|
|
a4b8566177 | ||
|
|
fc5d87701c | ||
|
|
eb556a6d23 | ||
|
|
7e5914e90c | ||
|
|
6c6cbaba84 | ||
|
|
9f9953664f | ||
|
|
ada4f6b4ed | ||
|
|
a5a4083f00 |
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -25,6 +25,14 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run 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 commit -m 'Add built files'
|
||||||
|
git push origin HEAD:main
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Claude Code Review Action
|
# Claude Code Review Action
|
||||||
|
|
||||||
A GitHub Action that performs automated code reviews using Claude AI.
|
A GitHub Action that performs automated code reviews using Claude Sonnet 3.5, an AI assistant from Anthropic.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Analyzes code changes in pull requests
|
- Analyzes code changes in pull requests
|
||||||
@@ -35,14 +35,14 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
code-review:
|
code-review:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: development_environment
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Run Claude Review
|
- name: Run Claude Review
|
||||||
uses: pacnpal/claude-code-review@v1.0.6
|
uses: pacnpal/claude-code-review@main
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|||||||
444
action.js
444
action.js
@@ -3,17 +3,110 @@ const core = require('@actions/core');
|
|||||||
const github = require('@actions/github');
|
const github = require('@actions/github');
|
||||||
const { exec } = require('@actions/exec');
|
const { exec } = require('@actions/exec');
|
||||||
|
|
||||||
async function getPRDetails(octokit, context, 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
try {
|
||||||
console.log(`Getting details for PR #${prNumber}`);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Get PR info
|
return await fn();
|
||||||
const { data: pr } = await octokit.rest.pulls.get({
|
} 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,
|
...context.repo,
|
||||||
pull_number: parseInt(prNumber)
|
pull_number: prNumber
|
||||||
});
|
}),
|
||||||
|
'Get PR details'
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
base: {
|
base: {
|
||||||
sha: pr.base.sha,
|
sha: pr.base.sha,
|
||||||
@@ -22,59 +115,154 @@ async function getPRDetails(octokit, context, prNumber) {
|
|||||||
head: {
|
head: {
|
||||||
sha: pr.head.sha,
|
sha: pr.head.sha,
|
||||||
ref: pr.head.ref
|
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) {
|
} 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() {
|
async function setupGitConfig() {
|
||||||
// Configure git to fetch PR refs
|
core.startGroup('Setting up Git configuration');
|
||||||
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 {
|
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
|
// Get the full diff with context
|
||||||
await exec('git', ['diff', '-U10', baseSha, headSha], {
|
await exec('git', ['diff', '-U10', baseSha, headSha], {
|
||||||
listeners: {
|
listeners: {
|
||||||
stdout: (data) => {
|
stdout: (data) => {
|
||||||
diffContent += data.toString();
|
diffContent += data.toString();
|
||||||
|
},
|
||||||
|
stderr: (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter for relevant files
|
if (stderr) {
|
||||||
const lines = diffContent.split('\n');
|
core.debug(`Git diff stderr: ${stderr}`);
|
||||||
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;
|
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) {
|
} 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}`);
|
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) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +282,9 @@ For each issue found:
|
|||||||
- Provide specific recommendations for fixes
|
- Provide specific recommendations for fixes
|
||||||
- Include code examples where helpful
|
- 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:
|
Here is the code diff to review:
|
||||||
|
|
||||||
@@ -103,7 +293,11 @@ ${diffContent}
|
|||||||
\`\`\``;
|
\`\`\``;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
core.info('Sending request to Claude API...');
|
||||||
|
core.debug(`Prompt length: ${prompt.length} characters`);
|
||||||
|
|
||||||
|
const review = await retryWithBackoff(async () => {
|
||||||
|
const response = await fetchWithTimeout('https://api.anthropic.com/v1/messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -111,7 +305,7 @@ ${diffContent}
|
|||||||
'anthropic-version': '2023-06-01'
|
'anthropic-version': '2023-06-01'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'claude-3-sonnet-20240229',
|
model: 'claude-sonnet-4-5',
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
messages: [{
|
messages: [{
|
||||||
@@ -119,93 +313,207 @@ ${diffContent}
|
|||||||
content: prompt
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!data.content?.[0]?.text) {
|
if (!data.content?.[0]?.text) {
|
||||||
throw new Error(`API Error: ${JSON.stringify(data)}`);
|
throw new Error(`Invalid API response: missing content. Response: ${JSON.stringify(data).substring(0, 200)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.content[0].text;
|
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) {
|
} catch (error) {
|
||||||
throw new Error(`Claude API error: ${error.message}`);
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
async function postReview(octokit, context, review, prNumber) {
|
||||||
try {
|
core.startGroup('Posting review comment');
|
||||||
// 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({
|
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,
|
...context.repo,
|
||||||
issue_number: prNumber,
|
issue_number: prNumber,
|
||||||
body: `# Claude Code Review\n\n${escapedReview}`
|
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}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
core.error(`Failed to post review comment: ${error.message}`);
|
||||||
throw new Error(`Failed to post review: ${error.message}`);
|
throw new Error(`Failed to post review: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
core.endGroup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execution function
|
||||||
|
*/
|
||||||
async function run() {
|
async function run() {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
core.info('🚀 Starting Claude Code Review Action');
|
||||||
|
core.info(`Node version: ${process.version}`);
|
||||||
|
core.info(`Platform: ${process.platform}`);
|
||||||
|
|
||||||
// Get inputs
|
// Get inputs
|
||||||
const token = core.getInput('github-token', { required: true });
|
const token = core.getInput('github-token', { required: true });
|
||||||
const anthropicKey = core.getInput('anthropic-key', { required: true });
|
const anthropicKey = core.getInput('anthropic-key', { required: true });
|
||||||
|
let prNumber = core.getInput('pr-number');
|
||||||
|
|
||||||
|
// 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
|
// Initialize GitHub client
|
||||||
|
core.info('Initializing GitHub client...');
|
||||||
const octokit = github.getOctokit(token);
|
const octokit = github.getOctokit(token);
|
||||||
const context = github.context;
|
core.info(`Repository: ${context.repo.owner}/${context.repo.repo}`);
|
||||||
|
core.info(`Event: ${context.eventName}`);
|
||||||
// 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
|
// Set up git configuration
|
||||||
await setupGitConfig();
|
await setupGitConfig();
|
||||||
|
|
||||||
// Get PR details
|
// Get PR details
|
||||||
const pr = await getPRDetails(octokit, context, prNumber);
|
const pr = await getPRDetails(octokit, context, validatedPrNumber);
|
||||||
console.log(`Retrieved details for PR #${pr.number}`);
|
|
||||||
|
// Validate PR state
|
||||||
|
if (pr.state === 'closed') {
|
||||||
|
core.warning(`PR #${pr.number} is closed. Review will still be posted.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate diff
|
// Generate diff
|
||||||
console.log('Generating diff...');
|
|
||||||
const diff = await getDiff(pr.base.sha, pr.head.sha);
|
const diff = await getDiff(pr.base.sha, pr.head.sha);
|
||||||
|
|
||||||
if (!diff) {
|
if (!diff || diff.trim() === '') {
|
||||||
console.log('No relevant changes found');
|
core.warning('No changes found in diff');
|
||||||
core.setOutput('diff_size', '0');
|
core.setOutput('diff_size', '0');
|
||||||
|
core.setOutput('review', 'No changes to review');
|
||||||
|
core.info('✓ Action completed (no changes to review)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
core.setOutput('diff_size', diff.length.toString());
|
const diffSize = Buffer.byteLength(diff, 'utf8');
|
||||||
|
core.setOutput('diff_size', diffSize.toString());
|
||||||
|
|
||||||
// Analyze with Claude
|
// Analyze with Claude
|
||||||
console.log('Analyzing with Claude...');
|
|
||||||
const review = await analyzeWithClaude(diff, anthropicKey);
|
const review = await analyzeWithClaude(diff, anthropicKey);
|
||||||
|
|
||||||
if (!review) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post review
|
// Post review
|
||||||
console.log('Posting review...');
|
|
||||||
await postReview(octokit, context, review, pr.number);
|
await postReview(octokit, context, review, pr.number);
|
||||||
|
|
||||||
// Set outputs
|
// Set outputs
|
||||||
core.setOutput('review', review);
|
core.setOutput('review', review);
|
||||||
|
|
||||||
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
core.info(`✓ Claude Code Review completed successfully in ${duration}s`);
|
||||||
|
|
||||||
} catch (error) {
|
} 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);
|
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();
|
run();
|
||||||
543
dist/index.js
vendored
543
dist/index.js
vendored
@@ -3736,7 +3736,7 @@ module.exports = __toCommonJS(dist_src_exports);
|
|||||||
var import_universal_user_agent = __nccwpck_require__(3843);
|
var import_universal_user_agent = __nccwpck_require__(3843);
|
||||||
|
|
||||||
// pkg/dist-src/version.js
|
// pkg/dist-src/version.js
|
||||||
var VERSION = "9.0.5";
|
var VERSION = "9.0.6";
|
||||||
|
|
||||||
// pkg/dist-src/defaults.js
|
// pkg/dist-src/defaults.js
|
||||||
var userAgent = `octokit-endpoint.js/${VERSION} ${(0, import_universal_user_agent.getUserAgent)()}`;
|
var userAgent = `octokit-endpoint.js/${VERSION} ${(0, import_universal_user_agent.getUserAgent)()}`;
|
||||||
@@ -3841,9 +3841,9 @@ function addQueryParameters(url, parameters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pkg/dist-src/util/extract-url-variable-names.js
|
// pkg/dist-src/util/extract-url-variable-names.js
|
||||||
var urlVariableRegex = /\{[^}]+\}/g;
|
var urlVariableRegex = /\{[^{}}]+\}/g;
|
||||||
function removeNonChars(variableName) {
|
function removeNonChars(variableName) {
|
||||||
return variableName.replace(/^\W+|\W+$/g, "").split(/,/);
|
return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
|
||||||
}
|
}
|
||||||
function extractUrlVariableNames(url) {
|
function extractUrlVariableNames(url) {
|
||||||
const matches = url.match(urlVariableRegex);
|
const matches = url.match(urlVariableRegex);
|
||||||
@@ -4029,7 +4029,7 @@ function parse(options) {
|
|||||||
}
|
}
|
||||||
if (url.endsWith("/graphql")) {
|
if (url.endsWith("/graphql")) {
|
||||||
if (options.mediaType.previews?.length) {
|
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) => {
|
headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
|
||||||
const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
|
const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
|
||||||
return `application/vnd.github.${preview}-preview${format}`;
|
return `application/vnd.github.${preview}-preview${format}`;
|
||||||
@@ -4278,7 +4278,7 @@ __export(dist_src_exports, {
|
|||||||
module.exports = __toCommonJS(dist_src_exports);
|
module.exports = __toCommonJS(dist_src_exports);
|
||||||
|
|
||||||
// pkg/dist-src/version.js
|
// pkg/dist-src/version.js
|
||||||
var VERSION = "9.2.1";
|
var VERSION = "9.2.2";
|
||||||
|
|
||||||
// pkg/dist-src/normalize-paginated-list-response.js
|
// pkg/dist-src/normalize-paginated-list-response.js
|
||||||
function normalizePaginatedListResponse(response) {
|
function normalizePaginatedListResponse(response) {
|
||||||
@@ -4326,7 +4326,7 @@ function iterator(octokit, route, parameters) {
|
|||||||
const response = await requestMethod({ method, url, headers });
|
const response = await requestMethod({ method, url, headers });
|
||||||
const normalizedResponse = normalizePaginatedListResponse(response);
|
const normalizedResponse = normalizePaginatedListResponse(response);
|
||||||
url = ((normalizedResponse.headers.link || "").match(
|
url = ((normalizedResponse.headers.link || "").match(
|
||||||
/<([^>]+)>;\s*rel="next"/
|
/<([^<>]+)>;\s*rel="next"/
|
||||||
) || [])[1];
|
) || [])[1];
|
||||||
return { value: normalizedResponse };
|
return { value: normalizedResponse };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -6878,7 +6878,7 @@ var RequestError = class extends Error {
|
|||||||
if (options.request.headers.authorization) {
|
if (options.request.headers.authorization) {
|
||||||
requestCopy.headers = Object.assign({}, options.request.headers, {
|
requestCopy.headers = Object.assign({}, options.request.headers, {
|
||||||
authorization: options.request.headers.authorization.replace(
|
authorization: options.request.headers.authorization.replace(
|
||||||
/ .*$/,
|
/(?<! ) .*$/,
|
||||||
" [REDACTED]"
|
" [REDACTED]"
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -6946,7 +6946,7 @@ var import_endpoint = __nccwpck_require__(4471);
|
|||||||
var import_universal_user_agent = __nccwpck_require__(3843);
|
var import_universal_user_agent = __nccwpck_require__(3843);
|
||||||
|
|
||||||
// pkg/dist-src/version.js
|
// pkg/dist-src/version.js
|
||||||
var VERSION = "8.4.0";
|
var VERSION = "8.4.1";
|
||||||
|
|
||||||
// pkg/dist-src/is-plain-object.js
|
// pkg/dist-src/is-plain-object.js
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value) {
|
||||||
@@ -7005,7 +7005,7 @@ function fetchWrapper(requestOptions) {
|
|||||||
headers[keyAndValue[0]] = keyAndValue[1];
|
headers[keyAndValue[0]] = keyAndValue[1];
|
||||||
}
|
}
|
||||||
if ("deprecation" in headers) {
|
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();
|
const deprecationLink = matches && matches.pop();
|
||||||
log.warn(
|
log.warn(
|
||||||
`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
|
`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
|
||||||
@@ -13009,7 +13009,7 @@ module.exports = {
|
|||||||
|
|
||||||
|
|
||||||
const { parseSetCookie } = __nccwpck_require__(8915)
|
const { parseSetCookie } = __nccwpck_require__(8915)
|
||||||
const { stringify, getHeadersList } = __nccwpck_require__(3834)
|
const { stringify } = __nccwpck_require__(3834)
|
||||||
const { webidl } = __nccwpck_require__(4222)
|
const { webidl } = __nccwpck_require__(4222)
|
||||||
const { Headers } = __nccwpck_require__(6349)
|
const { Headers } = __nccwpck_require__(6349)
|
||||||
|
|
||||||
@@ -13085,14 +13085,13 @@ function getSetCookies (headers) {
|
|||||||
|
|
||||||
webidl.brandCheck(headers, Headers, { strict: false })
|
webidl.brandCheck(headers, Headers, { strict: false })
|
||||||
|
|
||||||
const cookies = getHeadersList(headers).cookies
|
const cookies = headers.getSetCookie()
|
||||||
|
|
||||||
if (!cookies) {
|
if (!cookies) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// In older versions of undici, cookies is a list of name:value.
|
return cookies.map((pair) => parseSetCookie(pair))
|
||||||
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13520,14 +13519,15 @@ module.exports = {
|
|||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 3834:
|
/***/ 3834:
|
||||||
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
|
/***/ ((module) => {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
const assert = __nccwpck_require__(2613)
|
/**
|
||||||
const { kHeadersList } = __nccwpck_require__(6443)
|
* @param {string} value
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isCTLExcludingHtab (value) {
|
function isCTLExcludingHtab (value) {
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
return false
|
return false
|
||||||
@@ -13788,31 +13788,13 @@ function stringify (cookie) {
|
|||||||
return out.join('; ')
|
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 = {
|
module.exports = {
|
||||||
isCTLExcludingHtab,
|
isCTLExcludingHtab,
|
||||||
stringify,
|
validateCookieName,
|
||||||
getHeadersList
|
validateCookiePath,
|
||||||
|
validateCookieValue,
|
||||||
|
toIMFDate,
|
||||||
|
stringify
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -15741,6 +15723,14 @@ const { isUint8Array, isArrayBuffer } = __nccwpck_require__(8253)
|
|||||||
const { File: UndiciFile } = __nccwpck_require__(3041)
|
const { File: UndiciFile } = __nccwpck_require__(3041)
|
||||||
const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(4322)
|
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
|
let ReadableStream = globalThis.ReadableStream
|
||||||
|
|
||||||
/** @type {globalThis['File']} */
|
/** @type {globalThis['File']} */
|
||||||
@@ -15826,7 +15816,7 @@ function extractBody (object, keepalive = false) {
|
|||||||
// Set source to a copy of the bytes held by object.
|
// Set source to a copy of the bytes held by object.
|
||||||
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
|
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
|
||||||
} else if (util.isFormDataLike(object)) {
|
} 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`
|
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
|
||||||
|
|
||||||
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
|
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
|
||||||
@@ -17808,6 +17798,7 @@ const {
|
|||||||
isValidHeaderName,
|
isValidHeaderName,
|
||||||
isValidHeaderValue
|
isValidHeaderValue
|
||||||
} = __nccwpck_require__(5523)
|
} = __nccwpck_require__(5523)
|
||||||
|
const util = __nccwpck_require__(9023)
|
||||||
const { webidl } = __nccwpck_require__(4222)
|
const { webidl } = __nccwpck_require__(4222)
|
||||||
const assert = __nccwpck_require__(2613)
|
const assert = __nccwpck_require__(2613)
|
||||||
|
|
||||||
@@ -18361,6 +18352,9 @@ Object.defineProperties(Headers.prototype, {
|
|||||||
[Symbol.toStringTag]: {
|
[Symbol.toStringTag]: {
|
||||||
value: 'Headers',
|
value: 'Headers',
|
||||||
configurable: true
|
configurable: true
|
||||||
|
},
|
||||||
|
[util.inspect.custom]: {
|
||||||
|
enumerable: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -27537,6 +27531,20 @@ class Pool extends PoolBase {
|
|||||||
? { ...options.interceptors }
|
? { ...options.interceptors }
|
||||||
: undefined
|
: undefined
|
||||||
this[kFactory] = factory
|
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] () {
|
[kGetDispatcher] () {
|
||||||
@@ -30011,6 +30019,14 @@ module.exports = require("net");
|
|||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 7598:
|
||||||
|
/***/ ((module) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
module.exports = require("node:crypto");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ 8474:
|
/***/ 8474:
|
||||||
/***/ ((module) => {
|
/***/ ((module) => {
|
||||||
|
|
||||||
@@ -31816,17 +31832,110 @@ const core = __nccwpck_require__(7484);
|
|||||||
const github = __nccwpck_require__(3228);
|
const github = __nccwpck_require__(3228);
|
||||||
const { exec } = __nccwpck_require__(5236);
|
const { exec } = __nccwpck_require__(5236);
|
||||||
|
|
||||||
async function getPRDetails(octokit, context, 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
try {
|
||||||
console.log(`Getting details for PR #${prNumber}`);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Get PR info
|
return await fn();
|
||||||
const { data: pr } = await octokit.rest.pulls.get({
|
} 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,
|
...context.repo,
|
||||||
pull_number: parseInt(prNumber)
|
pull_number: prNumber
|
||||||
});
|
}),
|
||||||
|
'Get PR details'
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
base: {
|
base: {
|
||||||
sha: pr.base.sha,
|
sha: pr.base.sha,
|
||||||
@@ -31835,59 +31944,154 @@ async function getPRDetails(octokit, context, prNumber) {
|
|||||||
head: {
|
head: {
|
||||||
sha: pr.head.sha,
|
sha: pr.head.sha,
|
||||||
ref: pr.head.ref
|
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) {
|
} 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() {
|
async function setupGitConfig() {
|
||||||
// Configure git to fetch PR refs
|
core.startGroup('Setting up Git configuration');
|
||||||
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 {
|
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
|
// Get the full diff with context
|
||||||
await exec('git', ['diff', '-U10', baseSha, headSha], {
|
await exec('git', ['diff', '-U10', baseSha, headSha], {
|
||||||
listeners: {
|
listeners: {
|
||||||
stdout: (data) => {
|
stdout: (data) => {
|
||||||
diffContent += data.toString();
|
diffContent += data.toString();
|
||||||
|
},
|
||||||
|
stderr: (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter for relevant files
|
if (stderr) {
|
||||||
const lines = diffContent.split('\n');
|
core.debug(`Git diff stderr: ${stderr}`);
|
||||||
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;
|
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) {
|
} 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}`);
|
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) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31907,7 +32111,9 @@ For each issue found:
|
|||||||
- Provide specific recommendations for fixes
|
- Provide specific recommendations for fixes
|
||||||
- Include code examples where helpful
|
- 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:
|
Here is the code diff to review:
|
||||||
|
|
||||||
@@ -31916,7 +32122,11 @@ ${diffContent}
|
|||||||
\`\`\``;
|
\`\`\``;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
core.info('Sending request to Claude API...');
|
||||||
|
core.debug(`Prompt length: ${prompt.length} characters`);
|
||||||
|
|
||||||
|
const review = await retryWithBackoff(async () => {
|
||||||
|
const response = await fetchWithTimeout('https://api.anthropic.com/v1/messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -31924,7 +32134,7 @@ ${diffContent}
|
|||||||
'anthropic-version': '2023-06-01'
|
'anthropic-version': '2023-06-01'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'claude-3-sonnet-20240229',
|
model: 'claude-sonnet-4-5',
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
messages: [{
|
messages: [{
|
||||||
@@ -31932,96 +32142,211 @@ ${diffContent}
|
|||||||
content: prompt
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!data.content?.[0]?.text) {
|
if (!data.content?.[0]?.text) {
|
||||||
throw new Error(`API Error: ${JSON.stringify(data)}`);
|
throw new Error(`Invalid API response: missing content. Response: ${JSON.stringify(data).substring(0, 200)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.content[0].text;
|
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) {
|
} catch (error) {
|
||||||
throw new Error(`Claude API error: ${error.message}`);
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
async function postReview(octokit, context, review, prNumber) {
|
||||||
try {
|
core.startGroup('Posting review comment');
|
||||||
// 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({
|
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,
|
...context.repo,
|
||||||
issue_number: prNumber,
|
issue_number: prNumber,
|
||||||
body: `# Claude Code Review\n\n${escapedReview}`
|
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}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
core.error(`Failed to post review comment: ${error.message}`);
|
||||||
throw new Error(`Failed to post review: ${error.message}`);
|
throw new Error(`Failed to post review: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
core.endGroup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execution function
|
||||||
|
*/
|
||||||
async function run() {
|
async function run() {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
core.info('🚀 Starting Claude Code Review Action');
|
||||||
|
core.info(`Node version: ${process.version}`);
|
||||||
|
core.info(`Platform: ${process.platform}`);
|
||||||
|
|
||||||
// Get inputs
|
// Get inputs
|
||||||
const token = core.getInput('github-token', { required: true });
|
const token = core.getInput('github-token', { required: true });
|
||||||
const anthropicKey = core.getInput('anthropic-key', { required: true });
|
const anthropicKey = core.getInput('anthropic-key', { required: true });
|
||||||
|
let prNumber = core.getInput('pr-number');
|
||||||
|
|
||||||
|
// 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
|
// Initialize GitHub client
|
||||||
|
core.info('Initializing GitHub client...');
|
||||||
const octokit = github.getOctokit(token);
|
const octokit = github.getOctokit(token);
|
||||||
const context = github.context;
|
core.info(`Repository: ${context.repo.owner}/${context.repo.repo}`);
|
||||||
|
core.info(`Event: ${context.eventName}`);
|
||||||
// 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
|
// Set up git configuration
|
||||||
await setupGitConfig();
|
await setupGitConfig();
|
||||||
|
|
||||||
// Get PR details
|
// Get PR details
|
||||||
const pr = await getPRDetails(octokit, context, prNumber);
|
const pr = await getPRDetails(octokit, context, validatedPrNumber);
|
||||||
console.log(`Retrieved details for PR #${pr.number}`);
|
|
||||||
|
// Validate PR state
|
||||||
|
if (pr.state === 'closed') {
|
||||||
|
core.warning(`PR #${pr.number} is closed. Review will still be posted.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate diff
|
// Generate diff
|
||||||
console.log('Generating diff...');
|
|
||||||
const diff = await getDiff(pr.base.sha, pr.head.sha);
|
const diff = await getDiff(pr.base.sha, pr.head.sha);
|
||||||
|
|
||||||
if (!diff) {
|
if (!diff || diff.trim() === '') {
|
||||||
console.log('No relevant changes found');
|
core.warning('No changes found in diff');
|
||||||
core.setOutput('diff_size', '0');
|
core.setOutput('diff_size', '0');
|
||||||
|
core.setOutput('review', 'No changes to review');
|
||||||
|
core.info('✓ Action completed (no changes to review)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
core.setOutput('diff_size', diff.length.toString());
|
const diffSize = Buffer.byteLength(diff, 'utf8');
|
||||||
|
core.setOutput('diff_size', diffSize.toString());
|
||||||
|
|
||||||
// Analyze with Claude
|
// Analyze with Claude
|
||||||
console.log('Analyzing with Claude...');
|
|
||||||
const review = await analyzeWithClaude(diff, anthropicKey);
|
const review = await analyzeWithClaude(diff, anthropicKey);
|
||||||
|
|
||||||
if (!review) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post review
|
// Post review
|
||||||
console.log('Posting review...');
|
|
||||||
await postReview(octokit, context, review, pr.number);
|
await postReview(octokit, context, review, pr.number);
|
||||||
|
|
||||||
// Set outputs
|
// Set outputs
|
||||||
core.setOutput('review', review);
|
core.setOutput('review', review);
|
||||||
|
|
||||||
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
core.info(`✓ Claude Code Review completed successfully in ${duration}s`);
|
||||||
|
|
||||||
} catch (error) {
|
} 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);
|
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();
|
run();
|
||||||
|
|
||||||
module.exports = __webpack_exports__;
|
module.exports = __webpack_exports__;
|
||||||
/******/ })()
|
/******/ })()
|
||||||
;
|
;
|
||||||
3193
package-lock.json
generated
3193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -18,11 +18,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.11.1",
|
"@actions/core": "^1.11.1",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.1",
|
||||||
"node-fetch": "^3.3.2"
|
"glob": "^11.0.3",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"js-yaml": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.38.3",
|
"@vercel/ncc": "^0.38.4",
|
||||||
"jest": "^29.7.0"
|
"jest": "^30.2.0",
|
||||||
|
"js-yaml": "^4.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user