forked from claude-did-this/claude-hub
* feat: Add @botaccount review command for manual PR reviews (#131) - Add detection for 'review' command in PR and issue comments - Implement handleManualPRReview function with authorization checks - Reuse existing PR review logic with manual-pr-review operation type - Configure PR review tools with broad research access and controlled write access - Support manual triggering of comprehensive PR reviews on demand 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * style: Apply pre-commit formatting changes * test: Update test expectation for new operationType parameter * fix: Improve PR detection for manual review command - Add pull_request property to GitHubIssue interface for PR comments - Handle both direct PR objects and issues with pull_request metadata - Fix TypeScript compilation errors and linting issues * fix: Improve pre-commit hook to fail on issues instead of auto-fixing - Use format:check instead of format to detect issues without auto-fixing - Use proper error handling with clear error messages - Provide helpful instructions on how to fix issues - Make commit behavior more predictable and transparent * style: Fix whitespace formatting --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,25 @@
|
||||
# Run formatting with auto-fix
|
||||
echo "🎨 Running Prettier..."
|
||||
npm run format
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Run linting for code quality (not formatting)
|
||||
echo "🔍 Running ESLint..."
|
||||
npm run lint:check
|
||||
echo "🎨 Running Prettier check..."
|
||||
if ! npm run format:check; then
|
||||
echo "❌ Prettier formatting issues found!"
|
||||
echo "💡 Run 'npm run format' to fix formatting issues, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔍 Running ESLint check..."
|
||||
if ! npm run lint:check; then
|
||||
echo "❌ ESLint issues found!"
|
||||
echo "💡 Run 'npm run lint' to fix linting issues, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run TypeScript type checking
|
||||
echo "📝 Running TypeScript check..."
|
||||
npm run typecheck
|
||||
if ! npm run typecheck; then
|
||||
echo "❌ TypeScript errors found!"
|
||||
echo "💡 Fix TypeScript errors, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All pre-commit checks passed!"
|
||||
@@ -119,6 +119,12 @@ chown node:node "${RESPONSE_FILE}"
|
||||
if [ "${OPERATION_TYPE}" = "auto-tagging" ]; then
|
||||
ALLOWED_TOOLS="Read,GitHub,Bash(gh issue edit:*),Bash(gh issue view:*),Bash(gh label list:*)" # Minimal tools for auto-tagging (security)
|
||||
echo "Running Claude Code for auto-tagging with minimal tools..." >&2
|
||||
elif [ "${OPERATION_TYPE}" = "pr-review" ] || [ "${OPERATION_TYPE}" = "manual-pr-review" ]; then
|
||||
# PR Review: Broad research access + controlled write access
|
||||
# Read access: Full file system, git history, GitHub data
|
||||
# Write access: GitHub comments/reviews, PR labels, but no file deletion/modification
|
||||
ALLOWED_TOOLS="Read,GitHub,Bash(gh *),Bash(git log*),Bash(git show*),Bash(git diff*),Bash(git blame*),Bash(find*),Bash(grep*),Bash(rg*),Bash(cat*),Bash(head*),Bash(tail*),Bash(ls*),Bash(tree*)"
|
||||
echo "Running Claude Code for PR review with broad research access..." >&2
|
||||
else
|
||||
ALLOWED_TOOLS="Bash,Create,Edit,Read,Write,GitHub" # Full tools for general operations
|
||||
echo "Running Claude Code with full tool access..." >&2
|
||||
|
||||
@@ -385,6 +385,11 @@ async function handlePullRequestComment(
|
||||
if (commandMatch?.[1]) {
|
||||
const command = commandMatch[1].trim();
|
||||
|
||||
// Check for manual review command
|
||||
if (command.toLowerCase() === 'review') {
|
||||
return await handleManualPRReview(pr, repo, payload.sender, res);
|
||||
}
|
||||
|
||||
try {
|
||||
// Process the command with Claude
|
||||
logger.info('Sending command to Claude service');
|
||||
@@ -490,6 +495,30 @@ async function processBotMention(
|
||||
if (commandMatch?.[1]) {
|
||||
const command = commandMatch[1].trim();
|
||||
|
||||
// Check if this is a PR and the command is "review"
|
||||
if (command.toLowerCase() === 'review') {
|
||||
// Check if this is already a PR object
|
||||
if ('head' in issue && 'base' in issue) {
|
||||
return await handleManualPRReview(issue, repo, comment.user, res);
|
||||
}
|
||||
|
||||
// Check if this issue is actually a PR (GitHub includes pull_request property for PR comments)
|
||||
const issueWithPR = issue;
|
||||
if (issueWithPR.pull_request) {
|
||||
// Create a mock PR object from the issue data for the review
|
||||
const mockPR: GitHubPullRequest = {
|
||||
...issue,
|
||||
head: {
|
||||
ref: issueWithPR.pull_request.head?.ref ?? 'unknown',
|
||||
sha: issueWithPR.pull_request.head?.sha ?? 'unknown'
|
||||
},
|
||||
base: issueWithPR.pull_request.base ?? { ref: 'main' }
|
||||
} as GitHubPullRequest;
|
||||
|
||||
return await handleManualPRReview(mockPR, repo, comment.user, res);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Process the command with Claude
|
||||
logger.info('Sending command to Claude service');
|
||||
@@ -530,6 +559,211 @@ async function processBotMention(
|
||||
return res.status(200).json({ message: 'Webhook processed successfully' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle manual PR review requests via @botaccount review command
|
||||
*/
|
||||
async function handleManualPRReview(
|
||||
pr: GitHubPullRequest,
|
||||
repo: GitHubRepository,
|
||||
sender: { login: string },
|
||||
res: Response<WebhookResponse | ErrorResponse>
|
||||
): Promise<Response<WebhookResponse | ErrorResponse>> {
|
||||
try {
|
||||
// Check if the sender is authorized to trigger reviews
|
||||
const authorizedUsers = process.env.AUTHORIZED_USERS
|
||||
? process.env.AUTHORIZED_USERS.split(',').map(user => user.trim())
|
||||
: [process.env.DEFAULT_AUTHORIZED_USER ?? 'admin'];
|
||||
|
||||
if (!authorizedUsers.includes(sender.login)) {
|
||||
logger.info(
|
||||
{
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login
|
||||
},
|
||||
'Unauthorized user attempted to trigger manual PR review'
|
||||
);
|
||||
|
||||
try {
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`❌ Sorry @${sender.login}, only authorized users can trigger PR reviews.`
|
||||
);
|
||||
|
||||
await postComment({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
issueNumber: pr.number,
|
||||
body: errorMessage
|
||||
});
|
||||
} catch (commentError) {
|
||||
logger.error({ err: commentError }, 'Failed to post unauthorized review attempt comment');
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Unauthorized user - review request ignored',
|
||||
context: {
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login,
|
||||
branch: pr.head.ref,
|
||||
commitSha: pr.head.sha
|
||||
},
|
||||
'Processing manual PR review request'
|
||||
);
|
||||
|
||||
// Add "review-in-progress" label
|
||||
try {
|
||||
await managePRLabels({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
prNumber: pr.number,
|
||||
labelsToAdd: ['claude-review-in-progress'],
|
||||
labelsToRemove: ['claude-review-needed', 'claude-review-complete']
|
||||
});
|
||||
} catch (labelError) {
|
||||
logger.error(
|
||||
{
|
||||
err: (labelError as Error).message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number
|
||||
},
|
||||
'Failed to add review-in-progress label for manual review'
|
||||
);
|
||||
// Continue with review even if label fails
|
||||
}
|
||||
|
||||
// Create the PR review prompt
|
||||
const prReviewPrompt = createPRReviewPrompt(pr.number, repo.full_name, pr.head.sha);
|
||||
|
||||
// Process the PR review with Claude
|
||||
logger.info('Sending PR for manual Claude review');
|
||||
const claudeResponse = await processCommand({
|
||||
repoFullName: repo.full_name,
|
||||
issueNumber: pr.number,
|
||||
command: prReviewPrompt,
|
||||
isPullRequest: true,
|
||||
branchName: pr.head.ref,
|
||||
operationType: 'manual-pr-review'
|
||||
});
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login,
|
||||
responseLength: claudeResponse ? claudeResponse.length : 0
|
||||
},
|
||||
'Manual PR review completed successfully'
|
||||
);
|
||||
|
||||
// Update label to show review is complete
|
||||
try {
|
||||
await managePRLabels({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
prNumber: pr.number,
|
||||
labelsToAdd: ['claude-review-complete'],
|
||||
labelsToRemove: ['claude-review-in-progress', 'claude-review-needed']
|
||||
});
|
||||
} catch (labelError) {
|
||||
logger.error(
|
||||
{
|
||||
err: (labelError as Error).message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number
|
||||
},
|
||||
'Failed to update review-complete label after manual review'
|
||||
);
|
||||
// Don't fail the review if label update fails
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Manual PR review completed successfully',
|
||||
context: {
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
type: 'manual_pr_review',
|
||||
sender: sender.login,
|
||||
branch: pr.head.ref
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: err.message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login
|
||||
},
|
||||
'Error processing manual PR review'
|
||||
);
|
||||
|
||||
// Remove in-progress label on error
|
||||
try {
|
||||
await managePRLabels({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
prNumber: pr.number,
|
||||
labelsToRemove: ['claude-review-in-progress']
|
||||
});
|
||||
} catch (labelError) {
|
||||
logger.error(
|
||||
{
|
||||
err: (labelError as Error).message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number
|
||||
},
|
||||
'Failed to remove review-in-progress label after manual review error'
|
||||
);
|
||||
}
|
||||
|
||||
// Post error comment
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
const errorId = `err-${Math.random().toString(36).substring(2, 10)}`;
|
||||
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`❌ An error occurred while processing the manual review request. (Reference: ${errorId}, Time: ${timestamp})
|
||||
|
||||
Please check with an administrator to review the logs for more details.`
|
||||
);
|
||||
|
||||
await postComment({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
issueNumber: pr.number,
|
||||
body: errorMessage
|
||||
});
|
||||
} catch (commentError) {
|
||||
logger.error({ err: commentError }, 'Failed to post manual review error comment');
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to process manual PR review',
|
||||
message: err.message,
|
||||
context: {
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
type: 'manual_pr_review_error',
|
||||
sender: sender.login
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command processing errors
|
||||
*/
|
||||
@@ -822,7 +1056,8 @@ async function processAutomatedPRReviews(
|
||||
issueNumber: pr.number,
|
||||
command: prReviewPrompt,
|
||||
isPullRequest: true,
|
||||
branchName: pr.head.ref
|
||||
branchName: pr.head.ref,
|
||||
operationType: 'pr-review'
|
||||
});
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type OperationType = 'auto-tagging' | 'pr-review' | 'default';
|
||||
export type OperationType = 'auto-tagging' | 'pr-review' | 'manual-pr-review' | 'default';
|
||||
|
||||
export interface ClaudeCommandOptions {
|
||||
repoFullName: string;
|
||||
|
||||
@@ -18,6 +18,15 @@ export interface GitHubIssue {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
html_url: string;
|
||||
pull_request?: {
|
||||
head?: {
|
||||
ref: string;
|
||||
sha: string;
|
||||
};
|
||||
base?: {
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubPullRequest {
|
||||
|
||||
@@ -7,26 +7,31 @@ The following shell test scripts have been migrated to the Jest E2E test suite a
|
||||
### Migrated Shell Scripts (✅ Completed)
|
||||
|
||||
**AWS Tests** (Directory: `test/aws/` - removed)
|
||||
|
||||
- `test-aws-mount.sh` → `test/e2e/scenarios/aws-authentication.test.js`
|
||||
- `test-aws-profile.sh` → `test/e2e/scenarios/aws-authentication.test.js`
|
||||
|
||||
**Claude Tests** (Directory: `test/claude/` - removed)
|
||||
|
||||
- `test-claude-direct.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-installation.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-no-firewall.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-response.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
|
||||
**Container Tests** (Directory: `test/container/` - removed)
|
||||
|
||||
- `test-basic-container.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test-container-cleanup.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test-container-privileged.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
|
||||
**Security Tests** (Directory: `test/security/` - removed)
|
||||
|
||||
- `test-firewall.sh` → `test/e2e/scenarios/security-firewall.test.js`
|
||||
- `test-github-token.sh` → `test/e2e/scenarios/github-integration.test.js`
|
||||
- `test-with-auth.sh` → `test/e2e/scenarios/security-firewall.test.js`
|
||||
|
||||
**Integration Tests** (Directory: `test/integration/` - removed)
|
||||
|
||||
- `test-full-flow.sh` → `test/e2e/scenarios/full-workflow.test.js`
|
||||
- `test-claudecode-docker.sh` → `test/e2e/scenarios/docker-execution.test.js` and `full-workflow.test.js`
|
||||
|
||||
|
||||
@@ -154,7 +154,8 @@ describe('GitHub Controller - Check Suite Events', () => {
|
||||
issueNumber: 42,
|
||||
command: expect.stringContaining('# GitHub PR Review - Complete Automated Review'),
|
||||
isPullRequest: true,
|
||||
branchName: 'feature-branch'
|
||||
branchName: 'feature-branch',
|
||||
operationType: 'pr-review'
|
||||
});
|
||||
|
||||
// Verify simple success response
|
||||
|
||||
Reference in New Issue
Block a user