feat: Add @botaccount review command for manual PR reviews (#131) (#152)

* 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:
Cheffromspace
2025-05-31 22:15:53 -05:00
committed by GitHub
parent 2e5fa7aa26
commit 31efbbc2bb
7 changed files with 279 additions and 11 deletions

View File

@@ -1,13 +1,25 @@
# Run formatting with auto-fix #!/bin/sh
echo "🎨 Running Prettier..." set -e
npm run format
# Run linting for code quality (not formatting) echo "🎨 Running Prettier check..."
echo "🔍 Running ESLint..." if ! npm run format:check; then
npm run lint:check 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..." 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!" echo "✅ All pre-commit checks passed!"

View File

@@ -119,6 +119,12 @@ chown node:node "${RESPONSE_FILE}"
if [ "${OPERATION_TYPE}" = "auto-tagging" ]; then 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) 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 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 else
ALLOWED_TOOLS="Bash,Create,Edit,Read,Write,GitHub" # Full tools for general operations ALLOWED_TOOLS="Bash,Create,Edit,Read,Write,GitHub" # Full tools for general operations
echo "Running Claude Code with full tool access..." >&2 echo "Running Claude Code with full tool access..." >&2

View File

@@ -385,6 +385,11 @@ async function handlePullRequestComment(
if (commandMatch?.[1]) { if (commandMatch?.[1]) {
const command = commandMatch[1].trim(); const command = commandMatch[1].trim();
// Check for manual review command
if (command.toLowerCase() === 'review') {
return await handleManualPRReview(pr, repo, payload.sender, res);
}
try { try {
// Process the command with Claude // Process the command with Claude
logger.info('Sending command to Claude service'); logger.info('Sending command to Claude service');
@@ -490,6 +495,30 @@ async function processBotMention(
if (commandMatch?.[1]) { if (commandMatch?.[1]) {
const command = commandMatch[1].trim(); 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 { try {
// Process the command with Claude // Process the command with Claude
logger.info('Sending command to Claude service'); logger.info('Sending command to Claude service');
@@ -530,6 +559,211 @@ async function processBotMention(
return res.status(200).json({ message: 'Webhook processed successfully' }); 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 * Handle command processing errors
*/ */
@@ -822,7 +1056,8 @@ async function processAutomatedPRReviews(
issueNumber: pr.number, issueNumber: pr.number,
command: prReviewPrompt, command: prReviewPrompt,
isPullRequest: true, isPullRequest: true,
branchName: pr.head.ref branchName: pr.head.ref,
operationType: 'pr-review'
}); });
logger.info( logger.info(

View File

@@ -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 { export interface ClaudeCommandOptions {
repoFullName: string; repoFullName: string;

View File

@@ -18,6 +18,15 @@ export interface GitHubIssue {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
html_url: string; html_url: string;
pull_request?: {
head?: {
ref: string;
sha: string;
};
base?: {
ref: string;
};
};
} }
export interface GitHubPullRequest { export interface GitHubPullRequest {

View File

@@ -7,26 +7,31 @@ The following shell test scripts have been migrated to the Jest E2E test suite a
### Migrated Shell Scripts (✅ Completed) ### Migrated Shell Scripts (✅ Completed)
**AWS Tests** (Directory: `test/aws/` - removed) **AWS Tests** (Directory: `test/aws/` - removed)
- `test-aws-mount.sh``test/e2e/scenarios/aws-authentication.test.js` - `test-aws-mount.sh``test/e2e/scenarios/aws-authentication.test.js`
- `test-aws-profile.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) **Claude Tests** (Directory: `test/claude/` - removed)
- `test-claude-direct.sh``test/e2e/scenarios/claude-integration.test.js` - `test-claude-direct.sh``test/e2e/scenarios/claude-integration.test.js`
- `test-claude-installation.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-no-firewall.sh``test/e2e/scenarios/claude-integration.test.js`
- `test-claude-response.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) **Container Tests** (Directory: `test/container/` - removed)
- `test-basic-container.sh``test/e2e/scenarios/container-execution.test.js` - `test-basic-container.sh``test/e2e/scenarios/container-execution.test.js`
- `test-container-cleanup.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` - `test-container-privileged.sh``test/e2e/scenarios/container-execution.test.js`
**Security Tests** (Directory: `test/security/` - removed) **Security Tests** (Directory: `test/security/` - removed)
- `test-firewall.sh``test/e2e/scenarios/security-firewall.test.js` - `test-firewall.sh``test/e2e/scenarios/security-firewall.test.js`
- `test-github-token.sh``test/e2e/scenarios/github-integration.test.js` - `test-github-token.sh``test/e2e/scenarios/github-integration.test.js`
- `test-with-auth.sh``test/e2e/scenarios/security-firewall.test.js` - `test-with-auth.sh``test/e2e/scenarios/security-firewall.test.js`
**Integration Tests** (Directory: `test/integration/` - removed) **Integration Tests** (Directory: `test/integration/` - removed)
- `test-full-flow.sh``test/e2e/scenarios/full-workflow.test.js` - `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` - `test-claudecode-docker.sh``test/e2e/scenarios/docker-execution.test.js` and `full-workflow.test.js`

View File

@@ -154,7 +154,8 @@ describe('GitHub Controller - Check Suite Events', () => {
issueNumber: 42, issueNumber: 42,
command: expect.stringContaining('# GitHub PR Review - Complete Automated Review'), command: expect.stringContaining('# GitHub PR Review - Complete Automated Review'),
isPullRequest: true, isPullRequest: true,
branchName: 'feature-branch' branchName: 'feature-branch',
operationType: 'pr-review'
}); });
// Verify simple success response // Verify simple success response