diff --git a/.husky/pre-commit b/.husky/pre-commit index 18f885c..7ff188c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -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!" \ No newline at end of file diff --git a/scripts/runtime/claudecode-entrypoint.sh b/scripts/runtime/claudecode-entrypoint.sh index 0139fbd..bb02156 100755 --- a/scripts/runtime/claudecode-entrypoint.sh +++ b/scripts/runtime/claudecode-entrypoint.sh @@ -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 diff --git a/src/controllers/githubController.ts b/src/controllers/githubController.ts index 4cde922..aa0b0e8 100644 --- a/src/controllers/githubController.ts +++ b/src/controllers/githubController.ts @@ -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 +): Promise> { + 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( diff --git a/src/types/claude.ts b/src/types/claude.ts index 9848a4c..061cb9e 100644 --- a/src/types/claude.ts +++ b/src/types/claude.ts @@ -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; diff --git a/src/types/github.ts b/src/types/github.ts index 0690195..ec6d8f8 100644 --- a/src/types/github.ts +++ b/src/types/github.ts @@ -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 { diff --git a/test/MIGRATION_NOTICE.md b/test/MIGRATION_NOTICE.md index eca30ca..a76f655 100644 --- a/test/MIGRATION_NOTICE.md +++ b/test/MIGRATION_NOTICE.md @@ -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` diff --git a/test/unit/controllers/githubController-check-suite.test.js b/test/unit/controllers/githubController-check-suite.test.js index ee1ebac..b7e9eb9 100644 --- a/test/unit/controllers/githubController-check-suite.test.js +++ b/test/unit/controllers/githubController-check-suite.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