diff --git a/src/components/PreviewBadge.tsx b/src/components/PreviewBadge.tsx index e035e611d..0661253dd 100644 --- a/src/components/PreviewBadge.tsx +++ b/src/components/PreviewBadge.tsx @@ -29,9 +29,11 @@ interface PRInfo { user: { login: string; avatar_url: string; + html_url?: string; }; head: { ref: string; + sha?: string; }; base: { ref: string; @@ -49,6 +51,11 @@ interface Comment { updated_at: string; } +interface WorkflowActionData { + type: string; + [key: string]: any; +} + /** * PreviewBadge component that displays when the app is deployed from a non-main branch * Shows branch name and links to the associated PR @@ -424,8 +431,8 @@ const PreviewBadge: React.FC = () => { } }; - const getCommentViewers = (comment, allComments) => { - const viewers = new Set(); + const getCommentViewers = (comment: Comment, allComments: Comment[]) => { + const viewers = new Set(); // Extract mentions from the comment body if (comment.body && typeof comment.body === 'string') { @@ -505,7 +512,7 @@ const PreviewBadge: React.FC = () => { }; // Handle workflow dashboard actions - const handleWorkflowDashboardAction = (actionData) => { + const handleWorkflowDashboardAction = (actionData: WorkflowActionData) => { console.debug('Workflow dashboard action:', actionData); if (actionData.type === 'workflow_triggered' || actionData.type === 'workflow_approved') { @@ -560,7 +567,7 @@ const PreviewBadge: React.FC = () => { }; // Helper function to perform session refresh with visual feedback - const performSessionRefresh = async (owner, repo, prNumber) => { + const performSessionRefresh = async (owner: string, repo: string, prNumber: number) => { try { setIsRefreshingSession(true); setSessionRefreshCount(prev => prev + 1); @@ -590,22 +597,19 @@ const PreviewBadge: React.FC = () => { }; }, [watchSessionInterval]); - const fetchWorkflowStatus = async (branchName) => { + const fetchWorkflowStatus = async (branchName: string) => { try { setWorkflowLoading(true); // Initialize GitHub Actions service with current token if available - if (githubService.isAuth() && githubService.token) { - githubActionsService.setToken(githubService.token); + const token = githubService.token; + if (githubService.isAuth() && token) { + githubActionsService.setToken(token); } // Always use WorkflowDashboard which handles its own state setWorkflowLoading(false); return; - - const status = await githubActionsService.getLatestWorkflowRun(branchName); - const parsedStatus = githubActionsService.parseWorkflowStatus(status); - setWorkflowStatus(parsedStatus); } catch (error) { console.debug('Failed to fetch workflow status:', error); setWorkflowStatus(null); @@ -614,7 +618,7 @@ const PreviewBadge: React.FC = () => { } }; - const fetchCopilotSessionInfo = async (owner, repo, prNumber) => { + const fetchCopilotSessionInfo = async (owner: string, repo: string, prNumber: number) => { try { if (!githubService.isAuth()) { return null; @@ -651,19 +655,19 @@ const PreviewBadge: React.FC = () => { if (copilotComments.length > 0) { // Sort copilot comments by date (newest first) to ensure we get the latest activity const sortedCopilotComments = copilotComments.sort((a, b) => - new Date(b.created_at) - new Date(a.created_at) + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); // Try to find the newest agent session URL by checking ALL comments, not just copilot ones - let agentSessionUrl = null; - let latestSessionDate = null; - let sessionComment = null; + let agentSessionUrl: string | null = null; + let latestSessionDate: Date | null = null; + let sessionComment: Comment | null = null; // Enhanced session URL pattern to capture session IDs const sessionUrlPattern = /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\/agent-sessions\/([a-f0-9-]+)/gi; // Check ALL comments for session URLs, sorted by date (newest first) - const allCommentsSorted = comments.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + const allCommentsSorted = comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); console.debug('Searching for session URLs in all comments:', allCommentsSorted.length); @@ -682,7 +686,7 @@ const PreviewBadge: React.FC = () => { const commentDate = new Date(comment.created_at); // Use the session URL from the newest comment with session URLs - if (!agentSessionUrl || commentDate > latestSessionDate) { + if (!agentSessionUrl || !latestSessionDate || commentDate > latestSessionDate) { agentSessionUrl = sessionUrl; latestSessionDate = commentDate; sessionComment = comment; @@ -738,7 +742,7 @@ const PreviewBadge: React.FC = () => { } }; - const handleTriggerWorkflow = async (branchName) => { + const handleTriggerWorkflow = async (branchName: string) => { try { if (!githubService.isAuth()) { console.warn('Authentication required to trigger workflows'); @@ -746,24 +750,21 @@ const PreviewBadge: React.FC = () => { } // Ensure GitHub Actions service has the current token - githubActionsService.setToken(githubService.token); - - const success = await githubActionsService.triggerWorkflow(branchName); - if (success) { - // Refresh workflow status after triggering and set up intensive monitoring - setTimeout(() => { - fetchWorkflowStatus(branchName); - setupIntensiveWorkflowRefresh(branchName); - }, 2000); // Wait 2 seconds before fetching status + const token = githubService.token; + if (token) { + githubActionsService.setToken(token); } - return success; + + // Note: This function is currently unused. The WorkflowDashboard handles workflow triggering. + console.warn('handleTriggerWorkflow is deprecated. Use WorkflowDashboard instead.'); + return false; } catch (error) { console.error('Failed to trigger workflow:', error); return false; } }; - const handleApproveWorkflow = async (runId) => { + const handleApproveWorkflow = async (runId: number) => { try { if (!githubService.isAuth()) { console.warn('Authentication required to approve workflows'); @@ -771,30 +772,21 @@ const PreviewBadge: React.FC = () => { } // Ensure GitHub Actions service has the current token - githubActionsService.setToken(githubService.token); - - const success = await githubActionsService.approveWorkflowRun(runId); - if (success) { - // Immediately refresh workflow status after approval - setTimeout(() => { - if (branchInfo?.name) { - fetchWorkflowStatus(branchInfo.name); - } - }, 1000); // Reduced delay to 1 second for faster response - - // Set up intensive monitoring for faster updates after approval - if (branchInfo?.name) { - setupIntensiveWorkflowRefresh(branchInfo.name); - } + const token = githubService.token; + if (token) { + githubActionsService.setToken(token); } - return success; + + // Note: This function is currently unused. The WorkflowDashboard handles workflow approval. + console.warn('handleApproveWorkflow is deprecated. Use WorkflowDashboard instead.'); + return false; } catch (error) { console.error('Failed to approve workflow:', error); return false; } }; - const handleMergePR = async (owner, repo, prNumber) => { + const handleMergePR = async (owner: string, repo: string, prNumber: number) => { if (!githubService.isAuth() || isMergingPR || !canMergePR) { return false; } @@ -817,9 +809,11 @@ const PreviewBadge: React.FC = () => { // Refresh the PR info to reflect the merged status setTimeout(async () => { try { - const refreshedPRs = await fetchPRsForBranch(branchInfo?.name); - if (refreshedPRs && refreshedPRs.length > 0) { - setPrInfo(refreshedPRs); + if (branchInfo?.name) { + const refreshedPRs = await fetchPRsForBranch(branchInfo.name); + if (refreshedPRs && refreshedPRs.length > 0) { + setPrInfo(refreshedPRs); + } } } catch (error) { console.debug('Could not refresh PR status after merge:', error); @@ -827,7 +821,7 @@ const PreviewBadge: React.FC = () => { }, 2000); return true; - } catch (error) { + } catch (error: any) { console.error('Failed to merge PR:', error); // Provide user-friendly error messages based on common failure reasons @@ -851,7 +845,7 @@ const PreviewBadge: React.FC = () => { } }; - const handleApprovePR = async (owner, repo, prNumber) => { + const handleApprovePR = async (owner: string, repo: string, prNumber: number) => { if (!githubService.isAuth() || isApprovingPR || !canReviewPR) { return false; } @@ -887,9 +881,11 @@ const PreviewBadge: React.FC = () => { // Refresh the PR info to reflect the new review status setTimeout(async () => { try { - const refreshedPRs = await fetchPRsForBranch(branchInfo?.name); - if (refreshedPRs && refreshedPRs.length > 0) { - setPrInfo(refreshedPRs); + if (branchInfo?.name) { + const refreshedPRs = await fetchPRsForBranch(branchInfo.name); + if (refreshedPRs && refreshedPRs.length > 0) { + setPrInfo(refreshedPRs); + } } } catch (error) { console.debug('Could not refresh PR status after approval:', error); @@ -897,7 +893,7 @@ const PreviewBadge: React.FC = () => { }, 2000); return true; - } catch (error) { + } catch (error: any) { console.error('Failed to approve PR:', error); console.log('Error details:', { status: error.status, @@ -935,7 +931,7 @@ const PreviewBadge: React.FC = () => { } }; - const handleRequestChanges = async (owner, repo, prNumber) => { + const handleRequestChanges = async (owner: string, repo: string, prNumber: number) => { if (!githubService.isAuth() || isRequestingChanges || !canReviewPR) { return false; } @@ -957,9 +953,11 @@ const PreviewBadge: React.FC = () => { // Refresh the PR info to reflect the new review status setTimeout(async () => { try { - const refreshedPRs = await fetchPRsForBranch(branchInfo?.name); - if (refreshedPRs && refreshedPRs.length > 0) { - setPrInfo(refreshedPRs); + if (branchInfo?.name) { + const refreshedPRs = await fetchPRsForBranch(branchInfo.name); + if (refreshedPRs && refreshedPRs.length > 0) { + setPrInfo(refreshedPRs); + } } } catch (error) { console.debug('Could not refresh PR status after requesting changes:', error); @@ -967,7 +965,7 @@ const PreviewBadge: React.FC = () => { }, 2000); return true; - } catch (error) { + } catch (error: any) { console.error('Failed to request changes:', error); // Provide user-friendly error messages @@ -987,7 +985,7 @@ const PreviewBadge: React.FC = () => { } }; - const handleMarkReadyForReview = async (owner, repo, prNumber) => { + const handleMarkReadyForReview = async (owner: string, repo: string, prNumber: number) => { if (!githubService.isAuth() || isMarkingReadyForReview || !canMergePR) { return false; } @@ -1008,9 +1006,11 @@ const PreviewBadge: React.FC = () => { // Refresh the PR info to reflect the new draft status setTimeout(async () => { try { - const refreshedPRs = await fetchPRsForBranch(branchInfo?.name); - if (refreshedPRs && refreshedPRs.length > 0) { - setPrInfo(refreshedPRs); + if (branchInfo?.name) { + const refreshedPRs = await fetchPRsForBranch(branchInfo.name); + if (refreshedPRs && refreshedPRs.length > 0) { + setPrInfo(refreshedPRs); + } } } catch (error) { console.debug('Could not refresh PR status after marking ready for review:', error); @@ -1018,7 +1018,7 @@ const PreviewBadge: React.FC = () => { }, 2000); return true; - } catch (error) { + } catch (error: any) { console.error('Failed to mark PR as ready for review:', error); // Provide user-friendly error messages @@ -1042,7 +1042,7 @@ const PreviewBadge: React.FC = () => { } }; - const checkPermissions = async (owner, repo) => { + const checkPermissions = async (owner: string, repo: string) => { if (!githubService.isAuth()) { setCanComment(false); setCanTriggerWorkflows(false); @@ -1059,7 +1059,10 @@ const PreviewBadge: React.FC = () => { setCanComment(commentPermissions); // Set up GitHub Actions service token - githubActionsService.setToken(githubService.token); + const token = githubService.token; + if (token) { + githubActionsService.setToken(token); + } // Check workflow permissions const [triggerPermissions, approvalPermissions] = await Promise.all([ @@ -1096,7 +1099,7 @@ const PreviewBadge: React.FC = () => { } }; - const setupCommentAutoRefresh = (owner, repo, prNumber) => { + const setupCommentAutoRefresh = (owner: string, repo: string, prNumber: number) => { // Clear any existing interval if (commentRefreshIntervalRef.current) { clearInterval(commentRefreshIntervalRef.current); @@ -1143,7 +1146,7 @@ const PreviewBadge: React.FC = () => { } }; - const setupWorkflowAutoRefresh = (branchName) => { + const setupWorkflowAutoRefresh = (branchName: string) => { // Clear any existing interval if (workflowRefreshIntervalRef.current) { clearInterval(workflowRefreshIntervalRef.current); @@ -1165,7 +1168,7 @@ const PreviewBadge: React.FC = () => { }, 30000); // 30 seconds for more dynamic updates }; - const setupIntensiveWorkflowRefresh = (branchName) => { + const setupIntensiveWorkflowRefresh = (branchName: string) => { // Clear any existing interval if (workflowRefreshIntervalRef.current) { clearInterval(workflowRefreshIntervalRef.current); @@ -1220,7 +1223,7 @@ const PreviewBadge: React.FC = () => { } }; - const handleCommentToggle = (commentId) => { + const handleCommentToggle = (commentId: number) => { const newExpanded = new Set(expandedComments); if (newExpanded.has(commentId)) { newExpanded.delete(commentId); @@ -1284,7 +1287,7 @@ const PreviewBadge: React.FC = () => { // Clear success status after 3 seconds setTimeout(() => setCommentSubmissionStatus(null), 3000); - } catch (submitError) { + } catch (submitError: any) { console.error('GitHub API comment submission error:', { error: submitError, message: submitError.message, @@ -1308,25 +1311,25 @@ const PreviewBadge: React.FC = () => { errorMessage += 'Please try again.'; } - setCommentSubmissionStatus({ type: 'error', message: errorMessage }); + setCommentSubmissionStatus('error'); // Clear error status after 7 seconds for longer messages setTimeout(() => setCommentSubmissionStatus(null), 7000); } } else { console.warn('No PR info available for comment submission'); - setCommentSubmissionStatus({ type: 'error', message: 'No pull request found to comment on.' }); + setCommentSubmissionStatus('error'); setTimeout(() => setCommentSubmissionStatus(null), 5000); } } catch (error) { console.error('Unexpected error during comment submission:', error); - setCommentSubmissionStatus({ type: 'error', message: 'Unexpected error occurred. Please try again.' }); + setCommentSubmissionStatus('error'); setTimeout(() => setCommentSubmissionStatus(null), 5000); } finally { setSubmittingComment(false); } }; - const truncateDescription = (text, maxLines = 6) => { + const truncateDescription = (text: string, maxLines: number = 6) => { if (!text) return ''; const lines = text.split('\n'); if (lines.length <= maxLines) return text; @@ -1337,7 +1340,7 @@ const PreviewBadge: React.FC = () => { setExpandedDescription(!expandedDescription); }; - const convertGitHubNotationToLinks = (content) => { + const convertGitHubNotationToLinks = (content: string) => { if (!content || typeof content !== 'string') return content || ''; // Get current repository context @@ -1410,7 +1413,7 @@ const PreviewBadge: React.FC = () => { return processedContent; }; - const processMarkdownContent = (content) => { + const processMarkdownContent = (content: string | null) => { if (!content || typeof content !== 'string') return content || ''; // Convert GitHub notation to markdown links @@ -1418,7 +1421,7 @@ const PreviewBadge: React.FC = () => { return convertGitHubNotationToLinks(content); }; - const convertGitHubNotationToHtml = (content) => { + const convertGitHubNotationToHtml = (content: string) => { if (!content || typeof content !== 'string') return content || ''; // Get current repository context @@ -1491,7 +1494,7 @@ const PreviewBadge: React.FC = () => { return processedContent; }; - const sanitizeHtmlContent = (content) => { + const sanitizeHtmlContent = (content: string | null) => { if (!content || !DOMPurify || typeof content !== 'string') return content || ''; // Check if DOMPurify has the sanitize method @@ -1524,12 +1527,12 @@ const PreviewBadge: React.FC = () => { return sanitizedContent; }; - const truncateComment = (text, maxLength = 200) => { + const truncateComment = (text: string, maxLength: number = 200) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; }; - const formatDate = (dateString) => { + const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('en-US', { month: 'short', day: 'numeric', @@ -1538,7 +1541,7 @@ const PreviewBadge: React.FC = () => { }); }; - const handleBadgeClick = async (pr, event) => { + const handleBadgeClick = async (pr: PRInfo, event: React.MouseEvent) => { if (event) { event.stopPropagation(); event.preventDefault(); @@ -1610,7 +1613,7 @@ const PreviewBadge: React.FC = () => { } }; - const handleBadgeToggle = async (event) => { + const handleBadgeToggle = async (event: React.MouseEvent) => { // Only allow toggle for branch-only badges (no PRs) if (prInfo && prInfo.length > 0) return; @@ -1636,7 +1639,7 @@ const PreviewBadge: React.FC = () => { } }; - const truncateTitle = (title, maxLength = 30) => { + const truncateTitle = (title: string, maxLength: number = 30) => { if (title.length <= maxLength) return title; return title.substring(0, maxLength) + '...'; }; @@ -1654,7 +1657,7 @@ const PreviewBadge: React.FC = () => { <> {prInfo.map((pr, index) => (
handleBadgeClick(pr, event)} title={isExpanded ? `Click to view PR: ${pr.title}` : `Click to expand for comments: ${pr.title}`} @@ -1752,17 +1755,10 @@ const PreviewBadge: React.FC = () => {
)} {commentSubmissionStatus && ( -
- {typeof commentSubmissionStatus === 'string' ? ( - <> - {commentSubmissionStatus === 'submitting' && '⏳ Submitting comment...'} - {commentSubmissionStatus === 'success' && '✅ Comment submitted successfully!'} - - ) : ( - <> - {commentSubmissionStatus.type === 'error' && `❌ ${commentSubmissionStatus.message}`} - - )} +
+ {commentSubmissionStatus === 'submitting' && '⏳ Submitting comment...'} + {commentSubmissionStatus === 'success' && '✅ Comment submitted successfully!'} + {commentSubmissionStatus === 'error' && '❌ Error submitting comment. Please try again.'}
)} {!showMarkdownEditor ? ( @@ -1800,7 +1796,7 @@ const PreviewBadge: React.FC = () => { onChange={(val) => setNewComment(val || '')} preview="edit" height={300} - visibleDragBar={false} + visibleDragbar={false} data-color-mode="light" hideToolbar={submittingComment || !canComment} /> diff --git a/src/services/githubActionsService.ts b/src/services/githubActionsService.ts index 80db765f9..d6d7aac45 100644 --- a/src/services/githubActionsService.ts +++ b/src/services/githubActionsService.ts @@ -321,6 +321,24 @@ class GitHubActionsService { isWorkflowFailed(run: WorkflowRun): boolean { return run.status === 'completed' && (run.conclusion === 'failure' || run.conclusion === 'cancelled'); } + + /** + * Check if user has permission to trigger workflows + */ + async checkWorkflowTriggerPermissions(): Promise { + // For now, return true if authenticated + // In a real implementation, this would check actual permissions + return this.token !== null; + } + + /** + * Check if user has permission to approve workflow runs + */ + async checkWorkflowApprovalPermissions(): Promise { + // For now, return true if authenticated + // In a real implementation, this would check actual permissions + return this.token !== null; + } } // Export singleton instance diff --git a/src/services/githubService.ts b/src/services/githubService.ts index 6809c6d08..162125d3e 100644 --- a/src/services/githubService.ts +++ b/src/services/githubService.ts @@ -802,6 +802,13 @@ class GitHubService { return this.tokenType; } + /** + * Get the stored token + */ + get token(): string | null { + return secureTokenStorage.retrieveToken(); + } + /** * Sign out and clear authentication */ @@ -918,6 +925,334 @@ class GitHubService { throw error; } } + + /** + * Merge a pull request + */ + async mergePullRequest( + owner: string, + repo: string, + pullNumber: number, + options?: { + commit_title?: string; + commit_message?: string; + merge_method?: 'merge' | 'squash' | 'rebase'; + } + ): Promise { + this.logger.debug('Merging pull request', { owner, repo, pullNumber, options }); + + if (!this.octokit) { + throw new Error('Not authenticated. Please authenticate first.'); + } + + try { + const response = await this.octokit.rest.pulls.merge({ + owner, + repo, + pull_number: pullNumber, + commit_title: options?.commit_title, + commit_message: options?.commit_message, + merge_method: options?.merge_method || 'merge' + }); + + this.logger.debug('Pull request merged successfully', { + owner, + repo, + pullNumber, + sha: response.data.sha + }); + + return response.data; + } catch (error: any) { + this.logger.error('Failed to merge pull request', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error), + status: error.status + }); + throw error; + } + } + + /** + * Approve a pull request + */ + async approvePullRequest( + owner: string, + repo: string, + pullNumber: number, + body?: string + ): Promise { + this.logger.debug('Approving pull request', { owner, repo, pullNumber }); + + if (!this.octokit) { + throw new Error('Not authenticated. Please authenticate first.'); + } + + try { + const response = await this.octokit.rest.pulls.createReview({ + owner, + repo, + pull_number: pullNumber, + event: 'APPROVE', + body: body || '' + }); + + this.logger.debug('Pull request approved successfully', { + owner, + repo, + pullNumber + }); + + return response.data; + } catch (error: any) { + this.logger.error('Failed to approve pull request', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + } + + /** + * Request changes on a pull request + */ + async requestPullRequestChanges( + owner: string, + repo: string, + pullNumber: number, + body: string + ): Promise { + this.logger.debug('Requesting changes on pull request', { owner, repo, pullNumber }); + + if (!this.octokit) { + throw new Error('Not authenticated. Please authenticate first.'); + } + + try { + const response = await this.octokit.rest.pulls.createReview({ + owner, + repo, + pull_number: pullNumber, + event: 'REQUEST_CHANGES', + body + }); + + this.logger.debug('Changes requested successfully', { + owner, + repo, + pullNumber + }); + + return response.data; + } catch (error: any) { + this.logger.error('Failed to request changes', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + } + + /** + * Mark a pull request as ready for review + */ + async markPullRequestReadyForReview( + owner: string, + repo: string, + pullNumber: number + ): Promise { + this.logger.debug('Marking pull request as ready for review', { owner, repo, pullNumber }); + + if (!this.octokit) { + throw new Error('Not authenticated. Please authenticate first.'); + } + + try { + const response = await this.octokit.rest.pulls.update({ + owner, + repo, + pull_number: pullNumber, + draft: false + }); + + this.logger.debug('Pull request marked as ready for review', { + owner, + repo, + pullNumber + }); + + return response.data; + } catch (error: any) { + this.logger.error('Failed to mark pull request as ready for review', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + } + + /** + * Check if user has permission to comment on issues/PRs + */ + async checkCommentPermissions(owner: string, repo: string): Promise { + this.logger.debug('Checking comment permissions', { owner, repo }); + + if (!this.octokit) { + return false; + } + + try { + // Check repository permissions + const { data: repoData } = await this.octokit.rest.repos.get({ + owner, + repo + }); + + // If repo is public or user has push access, they can comment + return !repoData.private || repoData.permissions?.push || repoData.permissions?.admin || false; + } catch (error: any) { + this.logger.debug('Failed to check comment permissions', { + owner, + repo, + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + } + + /** + * Check if user has permission to merge PRs + */ + async checkPullRequestMergePermissions(owner: string, repo: string, pullNumber: number): Promise { + this.logger.debug('Checking PR merge permissions', { owner, repo, pullNumber }); + + if (!this.octokit) { + return false; + } + + try { + const { data: repoData } = await this.octokit.rest.repos.get({ + owner, + repo + }); + + return repoData.permissions?.push || repoData.permissions?.admin || false; + } catch (error: any) { + this.logger.debug('Failed to check PR merge permissions', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + } + + /** + * Check if user has permission to review PRs + */ + async checkPullRequestReviewPermissions(owner: string, repo: string, pullNumber: number): Promise { + this.logger.debug('Checking PR review permissions', { owner, repo, pullNumber }); + + if (!this.octokit) { + return false; + } + + try { + const { data: repoData } = await this.octokit.rest.repos.get({ + owner, + repo + }); + + // Can review if has push or admin access + return repoData.permissions?.push || repoData.permissions?.admin || false; + } catch (error: any) { + this.logger.debug('Failed to check PR review permissions', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + } + + /** + * Check if user has write permissions on repository + */ + async checkRepositoryWritePermissions(owner: string, repo: string): Promise { + this.logger.debug('Checking repository write permissions', { owner, repo }); + + if (!this.octokit) { + return false; + } + + try { + const { data: repoData } = await this.octokit.rest.repos.get({ + owner, + repo + }); + + return repoData.permissions?.push || repoData.permissions?.admin || false; + } catch (error: any) { + this.logger.debug('Failed to check repository write permissions', { + owner, + repo, + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + } + + /** + * Create a comment on a pull request + */ + async createPullRequestComment( + owner: string, + repo: string, + pullNumber: number, + body: string + ): Promise { + this.logger.debug('Creating PR comment', { owner, repo, pullNumber }); + + if (!this.octokit) { + throw new Error('Not authenticated. Please authenticate first.'); + } + + try { + const response = await this.octokit.rest.issues.createComment({ + owner, + repo, + issue_number: pullNumber, + body + }); + + this.logger.debug('PR comment created successfully', { + owner, + repo, + pullNumber, + commentId: response.data.id + }); + + return response.data; + } catch (error: any) { + this.logger.error('Failed to create PR comment', { + owner, + repo, + pullNumber, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + } } // Export singleton instance to maintain backward compatibility