2025-05-30 23:15:18 +00:00
import { getInstallationOctokitByOwner , isUserCollaborator } from "../github/github-utils.js"
2025-03-10 16:24:51 +00:00
import { githubApp } from "../github/github-app.js"
2025-01-04 18:57:09 +00:00
import { Request , Response } from "express"
2025-03-10 16:24:51 +00:00
import { parseAndCreateOptimizationsDict } from "../github/pr-changes-utils.js"
import { posthog } from "../analytics.js"
2025-05-30 23:15:18 +00:00
import { userNickname } from "../auth0-mgmt.js"
2025-12-15 16:02:20 +00:00
import { logger } from "../utils/logger.js"
2026-01-19 17:33:57 +00:00
import {
missingRequiredFields ,
unauthorized ,
githubInstallationError ,
githubNotCollaborator ,
githubPrNotFound ,
internalServerError ,
} from "../exceptions/index.js"
2024-09-16 15:19:47 +00:00
2025-06-24 22:06:26 +00:00
// Dependencies interface for easier testing
export interface VerifyExistingOptimizationsDependencies {
getInstallationOctokitByOwner : typeof getInstallationOctokitByOwner
githubApp : typeof githubApp
parseAndCreateOptimizationsDict : typeof parseAndCreateOptimizationsDict
posthog : typeof posthog
userNickname : typeof userNickname
isUserCollaborator : typeof isUserCollaborator
}
// Default dependencies
let dependencies : VerifyExistingOptimizationsDependencies = {
getInstallationOctokitByOwner ,
githubApp ,
parseAndCreateOptimizationsDict ,
posthog ,
userNickname ,
isUserCollaborator ,
}
// For testing - allow dependency injection
export function setVerifyExistingOptimizationsDependencies (
deps : Partial < VerifyExistingOptimizationsDependencies > ,
) {
dependencies = { . . . dependencies , . . . deps }
}
export function resetVerifyExistingOptimizationsDependencies() {
dependencies = {
getInstallationOctokitByOwner ,
githubApp ,
parseAndCreateOptimizationsDict ,
posthog ,
userNickname ,
isUserCollaborator ,
}
}
2025-01-04 18:57:09 +00:00
export async function verifyExistingOptimizations ( req : Request , res : Response ) {
2025-05-30 23:15:18 +00:00
try {
const { repo_owner , repo_name , pr_number } = req . body
const userId = ( req as any ) . userId
2024-09-16 15:19:47 +00:00
2025-12-15 16:02:20 +00:00
logger . debug ( "Processing verify-existing-optimizations request" , {
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "request_start" ,
repo_owner ,
repo_name ,
pr_number ,
} )
2025-05-30 23:15:18 +00:00
if ( ! repo_name || ! repo_owner || ! pr_number ) {
2026-01-19 17:33:57 +00:00
throw missingRequiredFields ( "repo_name, repo_owner, pr_number" )
2025-05-30 23:15:18 +00:00
}
2024-09-16 15:19:47 +00:00
2025-06-24 22:06:26 +00:00
const nickname : string | null = await dependencies . userNickname ( userId )
2025-05-30 23:15:18 +00:00
if ( nickname == null ) {
2026-01-19 17:33:57 +00:00
throw unauthorized ( "" )
2024-09-16 15:48:26 +00:00
}
2025-06-24 22:06:26 +00:00
const octokit = await dependencies . getInstallationOctokitByOwner (
dependencies . githubApp ,
repo_owner ,
repo_name ,
2026-03-07 21:24:32 +00:00
userId ,
2025-06-24 22:06:26 +00:00
)
2025-05-30 23:15:18 +00:00
if ( octokit instanceof Error ) {
2026-01-19 17:33:57 +00:00
throw githubInstallationError ( octokit . message )
2025-05-30 23:15:18 +00:00
}
2024-09-16 15:19:47 +00:00
2025-12-15 16:02:20 +00:00
logger . info ( "Got installation Octokit for repository" , {
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "octokit_installation" ,
repo_owner ,
repo_name ,
} )
2025-06-24 22:06:26 +00:00
2025-05-30 23:15:18 +00:00
// Check collaborator status with error handling
try {
2025-06-24 22:06:26 +00:00
const isCollaborator = await dependencies . isUserCollaborator (
octokit ,
repo_owner ,
repo_name ,
nickname ,
)
2025-05-30 23:15:18 +00:00
if ( ! isCollaborator ) {
2025-12-15 16:02:20 +00:00
// Emit human-readable console message for tests (using info for console.log output)
// Don't include userId in context to avoid logger appending " for user ..."
logger . info ( ` ${ nickname } is not a collaborator on ${ repo_owner } / ${ repo_name } ` , {
requestId : ( req as any ) . requestId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "collaborator_verification" ,
repo_owner ,
repo_name ,
nickname ,
} )
2026-01-19 17:33:57 +00:00
throw githubNotCollaborator ( ` ${ repo_owner } / ${ repo_name } ` )
2025-05-30 23:15:18 +00:00
}
} catch ( error ) {
2026-01-19 17:33:57 +00:00
if ( error && typeof error === "object" && "getHttpStatus" in error ) {
throw error
}
2025-12-15 16:02:20 +00:00
// Concatenate error in message for tests
logger . error ( ` Error checking collaborator status: ${ error } ` , {
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "collaborator_verification" ,
repo_owner ,
repo_name ,
nickname ,
} )
2026-01-19 17:33:57 +00:00
throw internalServerError ( "Failed to verify collaborator status" )
2025-05-30 23:15:18 +00:00
}
2024-10-28 08:12:53 +00:00
2025-05-30 23:15:18 +00:00
// Get PR with specific 404 handling
2026-02-20 19:53:55 +00:00
// Note: GitHub returns 404 for both non-existent PRs and PRs the installation cannot access
2025-05-30 23:15:18 +00:00
let pr
try {
pr = await octokit . rest . pulls . get ( {
owner : repo_owner ,
repo : repo_name ,
pull_number : pr_number ,
} )
} catch ( error : any ) {
if ( error . status === 404 ) {
2026-02-20 19:53:55 +00:00
// Log additional context to help diagnose permission vs not-found issues
logger . warn (
` PR # ${ pr_number } returned 404 in ${ repo_owner } / ${ repo_name } . This could mean the PR doesn't exist, or the GitHub App installation doesn't have access to it. ` ,
{
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "pr_not_found_or_no_access" ,
repo_owner ,
repo_name ,
pr_number ,
nickname ,
errorMessage : error.message ,
errorResponse : error.response?.data ,
} ,
)
throw githubPrNotFound (
` # ${ pr_number } in ${ repo_owner } / ${ repo_name } . If the PR exists, ensure the GitHub App installation has access to this repository. ` ,
)
}
// Handle 403 (Forbidden) as a permissions issue
if ( error . status === 403 ) {
logger . warn (
` Access forbidden to PR # ${ pr_number } in ${ repo_owner } / ${ repo_name } . The GitHub App installation may not have sufficient permissions. ` ,
{
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "pr_access_forbidden" ,
repo_owner ,
repo_name ,
pr_number ,
nickname ,
errorMessage : error.message ,
} ,
)
throw githubInstallationError (
` Access forbidden to PR # ${ pr_number } in ${ repo_owner } / ${ repo_name } . Please ensure the GitHub App has the necessary permissions. ` ,
)
2025-05-30 23:15:18 +00:00
}
throw error // Re-throw to be caught by global handler
}
2024-10-28 08:12:53 +00:00
2025-05-30 23:15:18 +00:00
// Get PR comments with error handling
let pr_messages
try {
pr_messages = await octokit . rest . issues . listComments ( {
owner : repo_owner ,
repo : repo_name ,
issue_number : pr_number ,
} )
} catch ( error ) {
2025-12-15 16:02:20 +00:00
// Pass error object for tests
logger . error (
"Error getting PR messages:" ,
{
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "get_pr_messages" ,
repo_owner ,
repo_name ,
pr_number ,
} ,
undefined ,
error as Error ,
)
2026-01-19 17:33:57 +00:00
throw internalServerError ( ` Failed to retrieve PR comments for ${ repo_owner } / ${ repo_name } ` )
2025-05-30 23:15:18 +00:00
}
2024-11-27 01:01:00 +00:00
2025-05-30 23:15:18 +00:00
// Get PR reviews with error handling
let pr_reviews
try {
pr_reviews = await octokit . rest . pulls . listReviews ( {
owner : repo_owner ,
repo : repo_name ,
pull_number : pr_number ,
} )
} catch ( error ) {
2025-12-15 16:02:20 +00:00
// Pass error object for tests
logger . error (
"Error getting PR reviews:" ,
{
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "get_pr_reviews" ,
repo_owner ,
repo_name ,
pr_number ,
} ,
undefined ,
error as Error ,
)
2026-01-19 17:33:57 +00:00
throw internalServerError ( ` Failed to retrieve PR reviews for ${ repo_owner } / ${ repo_name } ` )
2025-05-30 23:15:18 +00:00
}
2025-06-24 22:06:26 +00:00
2026-04-13 16:03:05 +00:00
const reviewBodies : Array < { body : string } > = [ ]
2025-05-30 23:15:18 +00:00
for ( const review of pr_reviews . data ) {
// Add the main review body if it exists
if ( review . body ) {
reviewBodies . push ( { body : review.body } )
}
// Get review comments for this specific review
try {
const reviewComments = await octokit . rest . pulls . listCommentsForReview ( {
owner : repo_owner ,
repo : repo_name ,
pull_number : pr_number ,
review_id : review.id ,
} )
// Add each review comment body
for ( const comment of reviewComments . data ) {
if ( comment . body ) {
reviewBodies . push ( { body : comment.body } )
}
}
} catch ( error ) {
2025-12-15 16:02:20 +00:00
// Emit explicit console message for tests
logger . error (
` Error getting review comments for review ${ review . id } : ` ,
{
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "get_review_comments" ,
repo_owner ,
repo_name ,
pr_number ,
reviewId : review.id ,
} ,
undefined ,
error as Error ,
)
2025-05-30 23:15:18 +00:00
// Continue with other reviews even if one fails
}
}
// Also get all review comments (not tied to a specific review)
try {
const allReviewComments = await octokit . rest . pulls . listReviewComments ( {
owner : repo_owner ,
repo : repo_name ,
pull_number : pr_number ,
} )
for ( const comment of allReviewComments . data ) {
if ( comment . body ) {
reviewBodies . push ( { body : comment.body } )
}
}
} catch ( error ) {
2025-12-15 16:02:20 +00:00
// Emit explicit console message for tests
logger . error (
"Error getting all review comments:" ,
{
requestId : ( req as any ) . requestId ,
userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "get_all_review_comments" ,
repo_owner ,
repo_name ,
pr_number ,
} ,
undefined ,
error as Error ,
)
2025-05-30 23:15:18 +00:00
// Continue even if this fails
}
const prBody = pr . data . body || ""
const validComments = pr_messages . data . filter (
( comment : { body? : string } ) = > comment . body !== undefined ,
2026-04-13 16:03:05 +00:00
) as Array < { body : string } >
2025-05-30 23:15:18 +00:00
const allComments = [ . . . validComments , . . . reviewBodies ]
2025-06-24 22:06:26 +00:00
const optimizations_dict = dependencies . parseAndCreateOptimizationsDict ( prBody , allComments )
2025-05-30 23:15:18 +00:00
if ( Object . keys ( optimizations_dict ) . length === 0 ) {
2025-07-22 11:57:19 +00:00
return res . status ( 200 ) . json ( { error : "No optimizations found for this PR" } )
2025-05-30 23:15:18 +00:00
}
2026-04-13 16:03:05 +00:00
const response_dict : Record < string , string [ ] > = { }
2025-05-30 23:15:18 +00:00
for ( const key in optimizations_dict ) {
response_dict [ key ] = Array . from ( optimizations_dict [ key ] )
}
2025-01-28 17:20:28 +00:00
2025-12-31 00:29:06 +00:00
dependencies . posthog ? . capture ( {
2025-05-30 23:15:18 +00:00
distinctId : userId ,
event : ` cfapi-github-pr-optimization ` ,
properties : {
repo_owner ,
repo_name ,
pr_number ,
} ,
} )
return res . status ( 200 ) . json ( response_dict )
} catch ( error ) {
2026-01-19 17:33:57 +00:00
// Re-throw AppExceptions to be handled by GlobalExceptionHandler
if ( error && typeof error === "object" && "getHttpStatus" in error ) {
throw error
}
2025-12-15 16:02:20 +00:00
// Emit specific console message expected by tests (stringified for single-arg output)
// Note: Error object is still passed for Sentry, but test mode outputs stringified version
// Use String(error) to match test expectation of ${error} format
const errorStr = String ( error )
logger . errorWithSentry (
` Error in /cfapi/verify-existing-optimizations: ${ errorStr } ` ,
{
requestId : ( req as any ) . requestId ,
userId : ( req as any ) . userId ,
endpoint : "/cfapi/verify-existing-optimizations" ,
operation : "verify_existing_optimizations" ,
} ,
{
repo_owner : req.body?.repo_owner ,
repo_name : req.body?.repo_name ,
pr_number : req.body?.pr_number ,
} ,
error as Error ,
)
2025-05-30 23:15:18 +00:00
if ( error instanceof Error ) {
2026-01-19 17:33:57 +00:00
throw internalServerError ( ` Error verifying existing optimizations: ${ error . message } ` )
2025-05-30 23:15:18 +00:00
} else {
2026-01-19 17:33:57 +00:00
throw internalServerError ( "Error verifying existing optimizations" )
2025-05-30 23:15:18 +00:00
}
}
2025-01-28 17:20:28 +00:00
}