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"
|
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-05-30 23:15:18 +00:00
|
|
|
if (!repo_name || !repo_owner || !pr_number) {
|
|
|
|
|
return res.status(400).json({ error: "Missing or malformed fields" })
|
|
|
|
|
}
|
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) {
|
|
|
|
|
return res.status(401).json({ error: "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,
|
|
|
|
|
)
|
2025-05-30 23:15:18 +00:00
|
|
|
if (octokit instanceof Error) {
|
|
|
|
|
return res.status(500).json({ error: octokit.message })
|
|
|
|
|
}
|
2024-09-16 15:19:47 +00:00
|
|
|
|
2025-06-24 22:06:26 +00:00
|
|
|
console.log(`Got installation Octokit for ${repo_owner}/${repo_name}`)
|
|
|
|
|
|
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) {
|
|
|
|
|
console.log(`${nickname} is not a collaborator on ${repo_owner}/${repo_name}`)
|
|
|
|
|
return res.status(401).json({ error: "Unauthorized - User is not a collaborator" })
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error checking collaborator status: ${error}`)
|
|
|
|
|
return res.status(500).json({ error: "Failed to verify collaborator status" })
|
|
|
|
|
}
|
2024-10-28 08:12:53 +00:00
|
|
|
|
2025-05-30 23:15:18 +00:00
|
|
|
// Get PR with specific 404 handling
|
|
|
|
|
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) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
error: `PR #${pr_number} not found for ${repo_owner}/${repo_name}`,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
console.error("Error getting PR messages:", error)
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
error: `Failed to retrieve PR comments for ${repo_owner}/${repo_name}`,
|
|
|
|
|
})
|
|
|
|
|
}
|
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) {
|
|
|
|
|
console.error("Error getting PR reviews:", error)
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
error: `Failed to retrieve PR reviews for ${repo_owner}/${repo_name}`,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-06-24 22:06:26 +00:00
|
|
|
|
2025-05-30 23:15:18 +00:00
|
|
|
const reviewBodies: { body: string }[] = []
|
|
|
|
|
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) {
|
|
|
|
|
console.error(`Error getting review comments for review ${review.id}:`, error)
|
|
|
|
|
// 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) {
|
|
|
|
|
console.error("Error getting all review comments:", error)
|
|
|
|
|
// Continue even if this fails
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prBody = pr.data.body || ""
|
|
|
|
|
const validComments = pr_messages.data.filter(
|
|
|
|
|
(comment: { body?: string }) => comment.body !== undefined,
|
|
|
|
|
) as { body: string }[]
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response_dict: { [key: string]: string[] } = {}
|
|
|
|
|
for (const key in optimizations_dict) {
|
|
|
|
|
response_dict[key] = Array.from(optimizations_dict[key])
|
|
|
|
|
}
|
2025-01-28 17:20:28 +00:00
|
|
|
|
2025-06-24 22:06:26 +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) {
|
|
|
|
|
console.error(`Error in /cfapi/verify-existing-optimizations: ${error}`)
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
error: `Error verifying existing optimizations: ${error.message}`,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
error: "Error verifying existing optimizations",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-28 17:20:28 +00:00
|
|
|
}
|