mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
## Summary - **Fix CI build failure**: Auth0Client crashes during Next.js prerendering when env vars aren't set. Returns a no-op stub (`getSession → null`) when domain is missing — semantically correct for static generation - **Lazy-load markdown libs (~260kb)**: ReactMarkdown, remarkGfm, and react-syntax-highlighter were eagerly imported in monaco-diff-viewer but only rendered when user expands "Generated Tests". Extracted into a dynamic component - **Parallelize repo detail query**: `getRepositoryById` ran the activity count sequentially after the repo lookup. Since `repoId` is already available, all three queries now run in parallel ## Test plan - [ ] CI `build` check passes (was failing since #2598) - [ ] Trace page still renders generated tests correctly when expanded - [ ] Repository detail page loads correctly with activity status
962 lines
28 KiB
TypeScript
962 lines
28 KiB
TypeScript
import { determineValidHunks, fileDiffsToMap, isDiffContentsWellFormed } from "../diff_utils.js"
|
|
import { userNickname } from "../auth0-mgmt.js"
|
|
import { logger } from "../utils/logger.js"
|
|
import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
|
|
import { githubApp } from "../github/github-app.js"
|
|
import {
|
|
buildDependentPrTitle,
|
|
buildPrCommentBody,
|
|
generateOptimizationReviewTemplate,
|
|
originalPRComment,
|
|
} from "../github/pr-changes-utils.js"
|
|
import {
|
|
createDependentPullRequest,
|
|
createNewBranchFromDiffContents,
|
|
} from "../github/create-pr-from-diffcontents.js"
|
|
import { posthog } from "../analytics.js"
|
|
import type { FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"
|
|
import { PrismaClient } from "@prisma/client"
|
|
import { prisma } from "@codeflash-ai/common"
|
|
import { sendSlackMessage } from "../github/slack_util.js"
|
|
import { Response } from "express"
|
|
import {
|
|
requestApproval,
|
|
requiresApprovalForRepo,
|
|
isQualityMonitoringRepo,
|
|
sendQualityMonitoringNotification,
|
|
} from "../github/optimization_approval.js"
|
|
import { registerRepositoryAndMember } from "./utils/github-repo-setup.js"
|
|
import { saveStagingReview } from "./create-staging.js"
|
|
import { AnyOctokit, AuthorizedUserReq, PullRequestDB } from "../types.js"
|
|
import { OptimizationReview } from "../OptimizationReview.js"
|
|
import {
|
|
missingRequiredFields,
|
|
validationFailure,
|
|
unauthorized,
|
|
githubInstallationError,
|
|
githubNotCollaborator,
|
|
optimizationRejected,
|
|
unprocessableEntity,
|
|
internalServerError,
|
|
} from "../exceptions/index.js"
|
|
|
|
// Dependencies interface for easier testing
|
|
export interface SuggestPrChangesDependencies {
|
|
prisma: PrismaClient
|
|
userNickname: typeof userNickname
|
|
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
|
|
isUserCollaborator: typeof isUserCollaborator
|
|
requiresApproval: typeof requiresApprovalForRepo
|
|
requestApproval: typeof requestApproval
|
|
posthog: typeof posthog
|
|
githubApp: typeof githubApp
|
|
isDiffContentsWellFormed: typeof isDiffContentsWellFormed
|
|
fileDiffsToMap: typeof fileDiffsToMap
|
|
determineValidHunks: typeof determineValidHunks
|
|
buildDependentPrTitle: typeof buildDependentPrTitle
|
|
buildPrCommentBody: typeof buildPrCommentBody
|
|
createNewBranchFromDiffContents: typeof createNewBranchFromDiffContents
|
|
createDependentPullRequest: typeof createDependentPullRequest
|
|
sendSlackMessage: typeof sendSlackMessage
|
|
updateOptimizationEvent: typeof updateOptimizationEvent
|
|
saveStagingReview: typeof saveStagingReview
|
|
}
|
|
|
|
// Default dependencies
|
|
let dependencies: SuggestPrChangesDependencies = {
|
|
prisma,
|
|
userNickname,
|
|
getInstallationOctokitByOwner,
|
|
isUserCollaborator,
|
|
requiresApproval: requiresApprovalForRepo,
|
|
requestApproval,
|
|
posthog,
|
|
githubApp,
|
|
isDiffContentsWellFormed,
|
|
fileDiffsToMap,
|
|
determineValidHunks,
|
|
buildDependentPrTitle,
|
|
buildPrCommentBody,
|
|
createNewBranchFromDiffContents,
|
|
createDependentPullRequest,
|
|
sendSlackMessage,
|
|
updateOptimizationEvent,
|
|
saveStagingReview,
|
|
}
|
|
|
|
// For testing - allow dependency injection
|
|
export function setSuggestPrChangesDependencies(deps: Partial<SuggestPrChangesDependencies>) {
|
|
dependencies = { ...dependencies, ...deps }
|
|
}
|
|
|
|
export function resetSuggestPrChangesDependencies() {
|
|
dependencies = {
|
|
prisma,
|
|
userNickname,
|
|
getInstallationOctokitByOwner,
|
|
isUserCollaborator,
|
|
requiresApproval: requiresApprovalForRepo,
|
|
requestApproval,
|
|
posthog,
|
|
githubApp,
|
|
isDiffContentsWellFormed,
|
|
fileDiffsToMap,
|
|
determineValidHunks,
|
|
buildDependentPrTitle,
|
|
buildPrCommentBody,
|
|
createNewBranchFromDiffContents,
|
|
createDependentPullRequest,
|
|
sendSlackMessage,
|
|
updateOptimizationEvent,
|
|
saveStagingReview,
|
|
}
|
|
}
|
|
|
|
const slackNotificationConfig = {
|
|
"Future-House": ["aviary", "paper-qa"],
|
|
"langflow-ai": ["langflow"],
|
|
"albumentations-team": ["albumentations"],
|
|
"Skyvern-AI": ["skyvern"],
|
|
roboflow: ["inference"],
|
|
gdsfactory: ["gdsfactory"],
|
|
stackai: ["stackend"],
|
|
}
|
|
|
|
// Helper function to parse speedup values (same as create-staging.ts)
|
|
function parseSpeedupValue(value: unknown, suffix: "x" | "%"): number | null {
|
|
if (value === undefined || value === null || value === "") {
|
|
return null
|
|
}
|
|
|
|
let stringValue = String(value).trim().toLowerCase()
|
|
|
|
if (stringValue.endsWith(suffix)) {
|
|
stringValue = stringValue.slice(0, -1)
|
|
}
|
|
|
|
const parsed = parseFloat(stringValue.replace(",", ""))
|
|
return isNaN(parsed) ? null : parsed
|
|
}
|
|
|
|
// Utility function to update optimization event in the database
|
|
export async function updateOptimizationEvent(
|
|
traceId: string,
|
|
prId?: string,
|
|
prCommentFields?: any,
|
|
originalLineProfiler?: string,
|
|
optimizedLineProfiler?: string,
|
|
) {
|
|
if (traceId !== "") {
|
|
try {
|
|
// Check existing data first (preserve staging data)
|
|
const existing = await dependencies.prisma.optimization_events.findUnique({
|
|
where: { trace_id: traceId },
|
|
select: {
|
|
function_name: true,
|
|
speedup_x: true,
|
|
file_path: true,
|
|
speedup_pct: true,
|
|
metadata: true,
|
|
},
|
|
})
|
|
|
|
// Extract data from prCommentFields if provided
|
|
const updateData: any = {
|
|
...(prId ? { pr_id: String(prId) } : {}),
|
|
is_optimization_found: true,
|
|
event_type: prId ? "pr_created" : "no-pr",
|
|
}
|
|
|
|
// Only add optimization data if prCommentFields provided AND not already set
|
|
if (prCommentFields) {
|
|
if (!existing?.function_name && prCommentFields.function_name) {
|
|
updateData.function_name = prCommentFields.function_name
|
|
}
|
|
if (!existing?.file_path && prCommentFields.file_path) {
|
|
updateData.file_path = prCommentFields.file_path
|
|
}
|
|
if (existing?.speedup_x == null && prCommentFields.speedup_x) {
|
|
updateData.speedup_x = parseSpeedupValue(prCommentFields.speedup_x, "x")
|
|
}
|
|
if (existing?.speedup_pct == null && prCommentFields.speedup_pct) {
|
|
updateData.speedup_pct = parseSpeedupValue(prCommentFields.speedup_pct, "%")
|
|
}
|
|
}
|
|
|
|
// Add line profiler data to metadata if provided
|
|
if (originalLineProfiler || optimizedLineProfiler) {
|
|
const currentMetadata = (existing?.metadata ?? {}) as Record<string, unknown>
|
|
if (originalLineProfiler && !currentMetadata.originalLineProfiler) {
|
|
currentMetadata.originalLineProfiler = originalLineProfiler
|
|
}
|
|
if (optimizedLineProfiler && !currentMetadata.optimizedLineProfiler) {
|
|
currentMetadata.optimizedLineProfiler = optimizedLineProfiler
|
|
}
|
|
updateData.metadata = currentMetadata
|
|
}
|
|
|
|
await dependencies.prisma.optimization_events.update({
|
|
where: { trace_id: traceId },
|
|
data: updateData,
|
|
})
|
|
} catch (eventError) {
|
|
logger.error(
|
|
"Failed to update optimization event:",
|
|
{ endpoint: "/cfapi/suggest-pr-changes", operation: "update_optimization_event" },
|
|
{},
|
|
eventError as Error,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function suggestPrChanges(
|
|
req: AuthorizedUserReq,
|
|
res: Response,
|
|
): Promise<Response | undefined> {
|
|
try {
|
|
const {
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
replayTests,
|
|
concolicTests,
|
|
optimizationReview,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
} = req.body
|
|
|
|
const userId = req.userId
|
|
const traceId = req.body.traceId || ""
|
|
logger.info(`traceId: ${traceId}`, req)
|
|
|
|
if (!repo || !owner || !pullNumber || !dependencies.isDiffContentsWellFormed(diffContents)) {
|
|
throw validationFailure("Missing or malformed fields: repo, owner, pullNumber, diffContents")
|
|
}
|
|
|
|
const nickname = await dependencies.userNickname(userId)
|
|
if (nickname == null) {
|
|
throw unauthorized("")
|
|
}
|
|
|
|
const installationOctokit = await dependencies.getInstallationOctokitByOwner(
|
|
dependencies.githubApp,
|
|
owner,
|
|
repo,
|
|
userId,
|
|
)
|
|
if (installationOctokit instanceof Error) {
|
|
throw githubInstallationError(installationOctokit.message)
|
|
}
|
|
|
|
const isCollaborator = await dependencies.isUserCollaborator(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
nickname,
|
|
)
|
|
if (!isCollaborator) {
|
|
logger.info(`${nickname} is not a collaborator on ${owner}/${repo}`, req)
|
|
throw githubNotCollaborator(`${owner}/${repo}`)
|
|
}
|
|
logger.info(`${nickname} is a collaborator on ${owner}/${repo}`, req)
|
|
// TODO: Remove this background upsert logic after ensuring all old repositories have been saved.
|
|
registerRepositoryAndMember(owner, repo, nickname, userId, installationOctokit)
|
|
.then(() => {
|
|
logger.info(`Background repo and member upsert completed for ${owner}/${repo}`, req)
|
|
})
|
|
.catch(err => {
|
|
logger.errorWithSentry(
|
|
`Error in background upsertRepoAndCreateMember`,
|
|
req,
|
|
{ owner, repo, nickname, userId },
|
|
err as Error,
|
|
)
|
|
})
|
|
// Check if approval is required
|
|
if (traceId && dependencies.requiresApproval(owner, repo)) {
|
|
const optimization = await dependencies.prisma.optimization_features.findUnique({
|
|
where: { trace_id: traceId },
|
|
select: {
|
|
approval_required: true,
|
|
approval_status: true,
|
|
},
|
|
})
|
|
|
|
if (optimization?.approval_status === "rejected") {
|
|
throw optimizationRejected("This optimization request was rejected")
|
|
}
|
|
|
|
if (optimization?.approval_status === "approved") {
|
|
logger.info(
|
|
`Request ${traceId} was previously approved, continuing with PR suggestion`,
|
|
req,
|
|
)
|
|
const result = await triggerSuggestPrChanges(
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
nickname,
|
|
installationOctokit,
|
|
replayTests,
|
|
concolicTests,
|
|
traceId,
|
|
optimizationReview,
|
|
res,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
)
|
|
|
|
if (result && typeof result === "object" && "status" in result) {
|
|
return result
|
|
}
|
|
return res.json(result)
|
|
} else {
|
|
const requestData = {
|
|
type: "suggest-pr-changes",
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
optimizationReview,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
}
|
|
|
|
await dependencies.requestApproval(
|
|
traceId,
|
|
owner,
|
|
repo,
|
|
prCommentFields.function_name,
|
|
userId,
|
|
requestData,
|
|
)
|
|
|
|
return res.status(202).json({
|
|
status: "pending_approval",
|
|
message:
|
|
"This PR suggestion requires approval. You will be notified when it is processed.",
|
|
})
|
|
}
|
|
}
|
|
|
|
// No approval required, proceed with PR suggestion
|
|
const result = await triggerSuggestPrChanges(
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
nickname,
|
|
installationOctokit,
|
|
replayTests,
|
|
concolicTests,
|
|
traceId,
|
|
optimizationReview,
|
|
res,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
)
|
|
|
|
// Check if this is a quality monitoring repo and send notification
|
|
if (traceId && isQualityMonitoringRepo(owner, repo) && result && typeof result === "number") {
|
|
const requestData = {
|
|
type: "suggest-pr-changes",
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
replayTests,
|
|
concolicTests,
|
|
optimizationReview,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
}
|
|
|
|
// Send quality monitoring notification (non-blocking)
|
|
sendQualityMonitoringNotification(
|
|
traceId,
|
|
owner,
|
|
repo,
|
|
prCommentFields.function_name,
|
|
userId,
|
|
requestData,
|
|
).catch(error => {
|
|
logger.error(`Failed to send quality monitoring notification:`, req, {}, error as Error)
|
|
})
|
|
|
|
logger.info(
|
|
`Quality monitoring notification triggered for ${owner}/${repo} PR #${pullNumber}`,
|
|
req,
|
|
)
|
|
}
|
|
|
|
// Don't call res.json(result) if result is already a Response object
|
|
if (result && typeof result === "object" && "status" in result) {
|
|
return result
|
|
}
|
|
return res.json(result)
|
|
} catch (error: any) {
|
|
// Try to fallback to staging if we have a traceId
|
|
const traceId = req.body.traceId || ""
|
|
if (traceId) {
|
|
logger.info(`PR suggestion failed, falling back to staging for traceId: ${traceId}`, req)
|
|
try {
|
|
const stagingResult = await dependencies.saveStagingReview(
|
|
req.body,
|
|
req.userId,
|
|
req.organizationId,
|
|
req.subscriptionInfo,
|
|
)
|
|
if (stagingResult.status === 200) {
|
|
return res.status(200).json({
|
|
message: "PR suggestion failed, staging created as fallback",
|
|
...stagingResult.data,
|
|
})
|
|
}
|
|
// Handle non-200 staging response - return staging's actual status/error
|
|
logger.errorWithSentry(
|
|
`Staging fallback returned status ${stagingResult.status}`,
|
|
req,
|
|
{ reqBody: req.body, userId: req.userId, traceId, stagingResult },
|
|
new Error(`Staging fallback returned status ${stagingResult.status}`),
|
|
)
|
|
return res.status(stagingResult.status).json({
|
|
message: "PR suggestion failed and staging fallback also failed",
|
|
...stagingResult.data,
|
|
})
|
|
} catch (stagingError) {
|
|
logger.errorWithSentry(
|
|
`Staging fallback threw an exception`,
|
|
req,
|
|
{ reqBody: req.body, userId: req.userId, traceId },
|
|
stagingError as Error,
|
|
)
|
|
return res.status(500).json({
|
|
message: "PR suggestion failed and staging fallback threw an error",
|
|
error: stagingError instanceof Error ? stagingError.message : String(stagingError),
|
|
})
|
|
}
|
|
}
|
|
|
|
logger.errorWithSentry(
|
|
`Error in /cfapi/suggest-pr-changes: ${error}`,
|
|
req,
|
|
{ errorMessage: error.message },
|
|
error as Error,
|
|
)
|
|
dependencies.posthog.capture({
|
|
distinctId: req.userId,
|
|
event: `cfapi-suggest-pr-changes-failed-error`,
|
|
properties: { error: error.message, stack: error.stack },
|
|
})
|
|
throw internalServerError(`Error creating pull request: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
export async function triggerSuggestPrChanges(
|
|
owner: string,
|
|
repo: string,
|
|
pullNumber: number,
|
|
diffContents: any,
|
|
prCommentFields: any,
|
|
existingTests: string,
|
|
generatedTests: string,
|
|
coverage_message: string,
|
|
userId: string,
|
|
nickname: string,
|
|
installationOctokit: any,
|
|
replayTests: string = "",
|
|
concolicTests: string = "",
|
|
traceId: string = "",
|
|
optimizationReview: string = "",
|
|
res?: Response,
|
|
originalLineProfiler: string = "",
|
|
optimizedLineProfiler: string = "",
|
|
): Promise<Response | number | null> {
|
|
try {
|
|
const diffContentsMap: Map<string, FileDiffContent> = dependencies.fileDiffsToMap(diffContents)
|
|
|
|
const { validHunks, invalidHunks } = await dependencies.determineValidHunks(
|
|
installationOctokit.rest,
|
|
{ owner, repo },
|
|
pullNumber,
|
|
100,
|
|
diffContentsMap,
|
|
)
|
|
|
|
const timestamp = new Date()
|
|
.toISOString()
|
|
.replace(/:/g, ".")
|
|
.replace(/\.\d+Z$/, "")
|
|
|
|
const newBranchName = `codeflash/optimize-pr${pullNumber}-${timestamp}`
|
|
|
|
const originalPrData = await installationOctokit.rest.pulls.get({
|
|
owner,
|
|
repo,
|
|
pull_number: pullNumber,
|
|
})
|
|
|
|
// Check if the PR is merged or closed - we can't suggest changes on merged/closed PRs
|
|
if (originalPrData.data.merged) {
|
|
logger.info(`PR #${pullNumber} is already merged, cannot suggest changes`, {
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "pr_merged_check",
|
|
owner,
|
|
repo,
|
|
userId,
|
|
})
|
|
throw unprocessableEntity(
|
|
`Cannot suggest changes on merged PR #${pullNumber}. The PR was already merged.`,
|
|
)
|
|
}
|
|
|
|
if (originalPrData.data.state === "closed") {
|
|
logger.info(`PR #${pullNumber} is closed, cannot suggest changes`, {
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "pr_closed_check",
|
|
owner,
|
|
repo,
|
|
userId,
|
|
})
|
|
throw unprocessableEntity(
|
|
`Cannot suggest changes on closed PR #${pullNumber}. The PR is no longer open.`,
|
|
)
|
|
}
|
|
|
|
const baseBranch = originalPrData.data.head.ref
|
|
logger.info(`Attempting to access ref for: ${owner}/${repo}, branch: ${baseBranch}`, {
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "access_ref",
|
|
owner,
|
|
repo,
|
|
userId,
|
|
})
|
|
|
|
const commitMessage = `Optimize ${prCommentFields.function_name} \n\n${prCommentFields.optimization_explanation}`
|
|
|
|
let hasMultipleHunksInSameFile = false
|
|
const hasMultipleFiles = validHunks.size > 1
|
|
|
|
for (const [filePath, hunks] of validHunks.entries()) {
|
|
if (hunks.length > 1) {
|
|
logger.info(
|
|
`File ${filePath} has ${hunks.length} hunks, using dependent PR instead of review comments`,
|
|
{
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "multiple_hunks",
|
|
owner,
|
|
repo,
|
|
userId,
|
|
},
|
|
)
|
|
hasMultipleHunksInSameFile = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (hasMultipleFiles) {
|
|
logger.info(
|
|
`Found ${validHunks.size} files with changes, using dependent PR instead of review comments`,
|
|
{ endpoint: "/cfapi/suggest-pr-changes", operation: "multiple_files", owner, repo, userId },
|
|
)
|
|
}
|
|
|
|
if (
|
|
invalidHunks.size > 0 ||
|
|
validHunks.size > 1 ||
|
|
hasMultipleFiles ||
|
|
hasMultipleHunksInSameFile
|
|
) {
|
|
logger.info(
|
|
`Creating a dependent PR because there are ${invalidHunks.size > 0 ? "invalid hunks" : "multiple valid hunks"}.`,
|
|
{
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "create_dependent_pr",
|
|
owner,
|
|
repo,
|
|
},
|
|
)
|
|
logger.info(`Making a new dependent PR...`, {
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "create_dependent_pr",
|
|
owner,
|
|
repo,
|
|
})
|
|
if (
|
|
OptimizationReview.MEDIUM === optimizationReview &&
|
|
!dependencies.requiresApproval(owner, repo) &&
|
|
traceId
|
|
) {
|
|
const result = await dependencies.saveStagingReview(
|
|
{
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
baseBranch,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
replayTests,
|
|
concolicTests,
|
|
optimizationReview,
|
|
traceId,
|
|
diffContents,
|
|
},
|
|
userId,
|
|
null, // organizationId - not available in this context
|
|
)
|
|
if (result.status == 200) {
|
|
const commentBody: string = originalPRComment(
|
|
prCommentFields,
|
|
traceId, //We will send trace Id instead of PRNumber
|
|
baseBranch,
|
|
optimizationReview,
|
|
)
|
|
await installationOctokit.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: pullNumber,
|
|
body: commentBody,
|
|
})
|
|
if (res) {
|
|
return res.status(200).json({ message: "staging created successfully" })
|
|
}
|
|
}
|
|
}
|
|
const branchCreated = await dependencies.createNewBranchFromDiffContents(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
newBranchName,
|
|
baseBranch,
|
|
diffContentsMap,
|
|
commitMessage,
|
|
)
|
|
|
|
if (!branchCreated) {
|
|
throw new Error(`Failed to create branch ${newBranchName}`)
|
|
}
|
|
const newPrData = await dependencies.createDependentPullRequest(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
pullNumber,
|
|
newBranchName,
|
|
baseBranch,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
replayTests,
|
|
concolicTests,
|
|
optimizationReview,
|
|
)
|
|
|
|
logger.info(
|
|
`Created new dependent PR #${newPrData.data.number} from branch ${newPrData.data.head.ref}`,
|
|
{
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "dependent_pr_created",
|
|
owner,
|
|
repo,
|
|
},
|
|
)
|
|
|
|
if (slackNotificationConfig[owner as keyof typeof slackNotificationConfig]?.includes(repo)) {
|
|
await dependencies.sendSlackMessage(
|
|
`new dependent PR created: ${newPrData.data.html_url} for ${owner}/${repo}`,
|
|
)
|
|
}
|
|
|
|
dependencies.posthog?.capture({
|
|
distinctId: userId,
|
|
event: `cfapi-suggest-pr-changes-success-dependent-pr-created`,
|
|
properties: {
|
|
owner,
|
|
repo,
|
|
newPrNumber: newPrData.data.number,
|
|
newPrBranch: newPrData.data.head.ref,
|
|
PRURL: newPrData.data.html_url,
|
|
},
|
|
})
|
|
|
|
if (traceId !== "") {
|
|
const pull_request_db = await dependencies.prisma.optimization_features.findUnique({
|
|
where: {
|
|
trace_id: traceId,
|
|
},
|
|
select: {
|
|
pull_request: true,
|
|
},
|
|
})
|
|
|
|
if (pull_request_db) {
|
|
if (pull_request_db.pull_request === null || pull_request_db.pull_request === undefined) {
|
|
pull_request_db.pull_request = {}
|
|
}
|
|
;(pull_request_db as PullRequestDB).pull_request.dependent_pr_url =
|
|
newPrData.data.html_url
|
|
|
|
await dependencies.prisma.optimization_features.update({
|
|
where: {
|
|
trace_id: traceId,
|
|
},
|
|
data: {
|
|
pull_request: pull_request_db.pull_request,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
await dependencies.updateOptimizationEvent(
|
|
traceId,
|
|
newPrData.data.id,
|
|
prCommentFields,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
)
|
|
|
|
if (res) {
|
|
res.json(newPrData.data.number)
|
|
return res
|
|
}
|
|
|
|
return newPrData.data.number
|
|
} else {
|
|
logger.info(`Creating unified review for a single valid hunk.`, {
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "create_unified_review",
|
|
owner,
|
|
repo,
|
|
})
|
|
|
|
const prCommentBody = dependencies.buildPrCommentBody(
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
newBranchName,
|
|
replayTests,
|
|
concolicTests,
|
|
traceId,
|
|
{ isUnifiedReview: true, includeHeader: false, isCollapsed: true },
|
|
)
|
|
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
|
|
optReviewBadge &&= `\n\n${optReviewBadge}\n`
|
|
const reviewComments = []
|
|
let foundInvalidHunk = false
|
|
|
|
for (const [filePath, hunks] of validHunks.entries()) {
|
|
for (const hunk of hunks) {
|
|
if (hunk.oldStart <= hunk.oldEnd) {
|
|
const newContent = hunk.newContent.join("\n")
|
|
const isLongDiff = newContent.length > 500
|
|
let commentBody
|
|
|
|
if (isLongDiff) {
|
|
commentBody =
|
|
`${prCommentBody}\n\n` +
|
|
`<details>\n` +
|
|
`<summary>Click to see suggested changes</summary>\n\n` +
|
|
`\`\`\`suggestion\n${newContent}\n\`\`\`\n` +
|
|
`</details>` +
|
|
`\n${optReviewBadge}`
|
|
} else {
|
|
commentBody =
|
|
`${prCommentBody}\n\n` +
|
|
`\`\`\`suggestion\n${newContent}\n\`\`\`` +
|
|
`\n${optReviewBadge}`
|
|
}
|
|
|
|
reviewComments.push({
|
|
path: filePath,
|
|
line: hunk.oldEnd,
|
|
start_line: hunk.oldStart,
|
|
side: "RIGHT",
|
|
body: commentBody,
|
|
})
|
|
} else {
|
|
logger.warn(
|
|
`Found invalid review range for ${filePath}: start_line (${hunk.oldStart}) must be less than or equal to line (${hunk.oldEnd}) with content ${hunk.newContent.join("\n")}`,
|
|
{
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "invalid_review_range",
|
|
owner,
|
|
repo,
|
|
},
|
|
)
|
|
foundInvalidHunk = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (foundInvalidHunk) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!foundInvalidHunk && reviewComments.length > 0) {
|
|
const review = await installationOctokit.rest.pulls.createReview({
|
|
owner,
|
|
repo,
|
|
pull_number: pullNumber,
|
|
commit_id: originalPrData.data.head.sha,
|
|
event: "COMMENT",
|
|
comments: reviewComments,
|
|
})
|
|
|
|
logger.info(`Added review comment to PR #${pullNumber}: ${review.data.html_url}`, {
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "review_comment_added",
|
|
owner,
|
|
repo,
|
|
})
|
|
|
|
if (
|
|
slackNotificationConfig[owner as keyof typeof slackNotificationConfig]?.includes(repo)
|
|
) {
|
|
await dependencies.sendSlackMessage(
|
|
`Suggestions made for ${review.data.html_url} in ${owner}/${repo}`,
|
|
)
|
|
}
|
|
|
|
dependencies.posthog?.capture({
|
|
distinctId: userId,
|
|
event: `cfapi-suggest-pr-changes-success-suggestions-made`,
|
|
properties: {
|
|
owner,
|
|
repo,
|
|
reviewId: review.data.id,
|
|
PRURL: review.data.html_url,
|
|
},
|
|
})
|
|
|
|
if (traceId !== "") {
|
|
const pull_request_db = await dependencies.prisma.optimization_features.findUnique({
|
|
where: {
|
|
trace_id: traceId,
|
|
},
|
|
select: {
|
|
pull_request: true,
|
|
},
|
|
})
|
|
|
|
if (pull_request_db) {
|
|
if (
|
|
pull_request_db.pull_request === null ||
|
|
pull_request_db.pull_request === undefined
|
|
) {
|
|
pull_request_db.pull_request = {}
|
|
}
|
|
|
|
;(pull_request_db as PullRequestDB).pull_request.review_suggestion_pr_url =
|
|
review.data.html_url
|
|
|
|
await dependencies.prisma.optimization_features.update({
|
|
where: {
|
|
trace_id: traceId,
|
|
},
|
|
data: {
|
|
pull_request: pull_request_db.pull_request,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
await dependencies.updateOptimizationEvent(
|
|
traceId,
|
|
undefined,
|
|
prCommentFields,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
)
|
|
|
|
if (res) {
|
|
res.json(review.data.id)
|
|
return res
|
|
}
|
|
|
|
return review.data.id
|
|
} else {
|
|
const reason = foundInvalidHunk
|
|
? "invalid line ordering in hunks"
|
|
: "no valid review comments could be created"
|
|
|
|
logger.warn(`Cannot create review due to ${reason}`, {
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "cannot_create_review",
|
|
owner,
|
|
repo,
|
|
})
|
|
|
|
logger.error(`Cannot create review comments due to ${reason}`, {
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "cannot_create_review",
|
|
owner,
|
|
repo,
|
|
})
|
|
throw unprocessableEntity(
|
|
`Cannot create review comments due to ${reason}. Please consider creating a dependent PR instead`,
|
|
)
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
// Re-throw AppExceptions to be handled by GlobalExceptionHandler
|
|
if (error && typeof error === "object" && "getHttpStatus" in error) {
|
|
throw error
|
|
}
|
|
|
|
logger.errorWithSentry(
|
|
`Error in triggerSuggestPrChanges: ${error}`,
|
|
{
|
|
userId,
|
|
endpoint: "/cfapi/suggest-pr-changes",
|
|
operation: "trigger_suggest_pr_changes",
|
|
owner,
|
|
repo,
|
|
},
|
|
{
|
|
errorMessage: error.message,
|
|
},
|
|
error as Error,
|
|
)
|
|
|
|
throw internalServerError(`Error creating pull request: ${error.message}`)
|
|
}
|
|
}
|