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
989 lines
30 KiB
TypeScript
989 lines
30 KiB
TypeScript
import { fileDiffsToMap, isDiffContentsWellFormed } from "../diff_utils.js"
|
|
import type { FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"
|
|
import { userNickname } from "../auth0-mgmt.js"
|
|
import {
|
|
addLabelToPullRequest,
|
|
getInstallationOctokitByOwner,
|
|
isBranchExists,
|
|
isUserCollaborator,
|
|
} from "../github/github-utils.js"
|
|
import {
|
|
buildBenchmarkInfo,
|
|
buildOptimizationMetadata,
|
|
buildPrTitle,
|
|
buildResultDetails,
|
|
buildResultFooter,
|
|
buildResultHeader,
|
|
buildResultTestReport,
|
|
generateOptimizationReviewTemplate,
|
|
} from "../github/pr-changes-utils.js"
|
|
import { githubApp } from "../github/github-app.js"
|
|
import {
|
|
assignReviewer,
|
|
createNewBranchFromDiffContents,
|
|
createNewPullRequest,
|
|
PrCommentFields,
|
|
} from "../github/create-pr-from-diffcontents.js"
|
|
import { posthog } from "../analytics.js"
|
|
import { PrismaClient } from "@prisma/client"
|
|
import { Request, Response } from "express"
|
|
import {
|
|
createAppInstallation,
|
|
createRepositoryMember,
|
|
getAppInstallationByInstalltionId,
|
|
prisma,
|
|
upsertRepository,
|
|
} from "@codeflash-ai/common"
|
|
import { AuthorizedUserReq } from "../types.js"
|
|
import {
|
|
requestApproval,
|
|
requiresApprovalForRepo,
|
|
isQualityMonitoringRepo,
|
|
sendQualityMonitoringNotification,
|
|
} from "../github/optimization_approval.js"
|
|
import { registerRepositoryAndMember } from "./utils/github-repo-setup.js"
|
|
import { logger } from "../utils/logger.js"
|
|
import { saveStagingReview } from "./create-staging.js"
|
|
|
|
// 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
|
|
}
|
|
|
|
// Helper function to add line profiler data to metadata (avoids duplication)
|
|
function addLineProfilerToMetadata(
|
|
metadata: Record<string, unknown>,
|
|
originalLineProfiler?: string,
|
|
optimizedLineProfiler?: string,
|
|
): void {
|
|
if (originalLineProfiler && !metadata.originalLineProfiler) {
|
|
metadata.originalLineProfiler = originalLineProfiler
|
|
}
|
|
if (optimizedLineProfiler && !metadata.optimizedLineProfiler) {
|
|
metadata.optimizedLineProfiler = optimizedLineProfiler
|
|
}
|
|
}
|
|
|
|
// Define a comprehensive interface for PR title and body generation
|
|
export interface PrContentBuilder {
|
|
buildResultHeader: typeof buildResultHeader
|
|
buildBenchmarkInfo: typeof buildBenchmarkInfo
|
|
buildResultDetails: typeof buildResultDetails
|
|
buildResultTestReport: typeof buildResultTestReport
|
|
buildResultFooter: typeof buildResultFooter
|
|
buildPrTitle: typeof buildPrTitle
|
|
}
|
|
|
|
// Create a standalone PR title and body function with injectable dependencies
|
|
export function createStandalonePRTitleAndBody(
|
|
prCommentFields: PrCommentFields,
|
|
existingTests: string,
|
|
generatedTests: string,
|
|
coverage_message: string,
|
|
newBranchName: string,
|
|
builder: PrContentBuilder,
|
|
replayTests: string = "",
|
|
concolicTests: string = "",
|
|
optimizationReview: string = "",
|
|
trace_id: string,
|
|
hasLineProfilerData: boolean = false,
|
|
): { title: string; body: string } {
|
|
const prCommentHeader = builder.buildResultHeader(prCommentFields)
|
|
|
|
const benchmarkInfo =
|
|
prCommentFields.benchmark_details && prCommentFields.benchmark_details.length > 0
|
|
? builder.buildBenchmarkInfo(prCommentFields)
|
|
: ""
|
|
|
|
const prCommentBody = builder.buildResultDetails(prCommentFields)
|
|
const prCommentTestReport = builder.buildResultTestReport(
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
replayTests,
|
|
concolicTests,
|
|
)
|
|
const prCommentFooter = builder.buildResultFooter(newBranchName)
|
|
const title: string = builder.buildPrTitle(
|
|
prCommentFields.function_name,
|
|
prCommentFields.speedup_pct,
|
|
prCommentFields.speedup_x,
|
|
)
|
|
|
|
const metadata = buildOptimizationMetadata(prCommentFields, trace_id)
|
|
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
|
|
optReviewBadge &&= ` ${optReviewBadge}\n`
|
|
|
|
// Add line profiler link if profiler data exists
|
|
let lineProfilerSection = ""
|
|
if (hasLineProfilerData && trace_id) {
|
|
lineProfilerSection = `\n### 📊 Performance Profile\n[View detailed line-by-line performance analysis](https://app.codeflash.ai/review-optimizations/${trace_id}/profiler)\n`
|
|
}
|
|
|
|
const body: string = benchmarkInfo
|
|
? `${metadata}\n${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${lineProfilerSection}${prCommentFooter}${optReviewBadge}`
|
|
: `${metadata}\n${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${lineProfilerSection}${prCommentFooter}${optReviewBadge}`
|
|
|
|
return { title, body }
|
|
}
|
|
|
|
// Create a wrapper function for createNewPullRequest to match test expectations
|
|
export async function createStandalonePullRequest(
|
|
installationOctokit: any,
|
|
owner: string,
|
|
repo: string,
|
|
title: string,
|
|
body: string,
|
|
newBranchName: string,
|
|
baseBranch: string,
|
|
) {
|
|
return await createNewPullRequest(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
title,
|
|
body,
|
|
newBranchName,
|
|
baseBranch,
|
|
)
|
|
}
|
|
|
|
// Dependencies interface for easier testing
|
|
export interface CreatePrDependencies {
|
|
prisma: PrismaClient
|
|
userNickname: typeof userNickname
|
|
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
|
|
isUserCollaborator: typeof isUserCollaborator
|
|
requiresApproval: typeof requiresApprovalForRepo
|
|
requestApproval: typeof requestApproval
|
|
triggerCreatePr: typeof triggerCreatePr
|
|
posthog: typeof posthog
|
|
isDiffContentsWellFormed: typeof isDiffContentsWellFormed
|
|
githubApp: typeof githubApp
|
|
registerRepositoryAndMember: typeof registerRepositoryAndMember
|
|
}
|
|
|
|
// Dependencies for triggerCreatePr
|
|
export interface TriggerCreatePrDependencies {
|
|
prisma: PrismaClient
|
|
fileDiffsToMap: typeof fileDiffsToMap
|
|
buildPrTitle: typeof buildPrTitle
|
|
createNewBranchFromDiffContents: typeof createNewBranchFromDiffContents
|
|
createStandalonePullRequest: typeof createStandalonePullRequest
|
|
addLabelToPullRequest: typeof addLabelToPullRequest
|
|
assignReviewer: typeof assignReviewer
|
|
posthog: typeof posthog
|
|
createStandalonePRTitleAndBody: typeof createStandalonePRTitleAndBody
|
|
prContentBuilder: PrContentBuilder
|
|
}
|
|
|
|
// Default dependencies
|
|
const defaultPrContentBuilder: PrContentBuilder = {
|
|
buildResultHeader,
|
|
buildBenchmarkInfo,
|
|
buildResultDetails,
|
|
buildResultTestReport,
|
|
buildResultFooter,
|
|
buildPrTitle,
|
|
}
|
|
|
|
let dependencies: CreatePrDependencies = {
|
|
prisma,
|
|
userNickname,
|
|
getInstallationOctokitByOwner,
|
|
isUserCollaborator,
|
|
requiresApproval: requiresApprovalForRepo,
|
|
requestApproval,
|
|
triggerCreatePr,
|
|
posthog,
|
|
isDiffContentsWellFormed,
|
|
githubApp,
|
|
registerRepositoryAndMember,
|
|
}
|
|
|
|
let triggerCreatePrDeps: TriggerCreatePrDependencies = {
|
|
prisma,
|
|
fileDiffsToMap,
|
|
buildPrTitle,
|
|
createNewBranchFromDiffContents,
|
|
createStandalonePullRequest,
|
|
addLabelToPullRequest,
|
|
assignReviewer,
|
|
posthog,
|
|
createStandalonePRTitleAndBody,
|
|
prContentBuilder: defaultPrContentBuilder,
|
|
}
|
|
|
|
// For testing - allow dependency injection
|
|
export function setCreatePrDependencies(deps: Partial<CreatePrDependencies>) {
|
|
dependencies = { ...dependencies, ...deps }
|
|
}
|
|
|
|
export function resetCreatePrDependencies() {
|
|
dependencies = {
|
|
prisma,
|
|
userNickname,
|
|
getInstallationOctokitByOwner,
|
|
isUserCollaborator,
|
|
requiresApproval: requiresApprovalForRepo,
|
|
requestApproval,
|
|
triggerCreatePr,
|
|
posthog,
|
|
isDiffContentsWellFormed,
|
|
githubApp,
|
|
registerRepositoryAndMember,
|
|
}
|
|
}
|
|
|
|
export function setTriggerCreatePrDependencies(deps: Partial<TriggerCreatePrDependencies>) {
|
|
triggerCreatePrDeps = { ...triggerCreatePrDeps, ...deps }
|
|
}
|
|
|
|
export function resetTriggerCreatePrDependencies() {
|
|
triggerCreatePrDeps = {
|
|
prisma,
|
|
fileDiffsToMap,
|
|
buildPrTitle,
|
|
createNewBranchFromDiffContents,
|
|
createStandalonePullRequest,
|
|
addLabelToPullRequest,
|
|
assignReviewer,
|
|
posthog,
|
|
createStandalonePRTitleAndBody,
|
|
prContentBuilder: defaultPrContentBuilder,
|
|
}
|
|
}
|
|
|
|
export async function createPr(req: Request, res: Response) {
|
|
try {
|
|
const {
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
replayTests,
|
|
concolicTests,
|
|
optimizationReview,
|
|
stagingBranch,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
} = req.body
|
|
|
|
// Get source from headers instead of body
|
|
const source = (req.headers?.["x-codeflash-source"] as string) || "unknown"
|
|
const isCfWebApp = source === "webapp"
|
|
const userId = (req as any).userId
|
|
const organizationId = (req as any).organizationId || null
|
|
const traceId = req.body.traceId || ""
|
|
const userTier = (req as any).subscriptionInfo?.tier || "free"
|
|
|
|
// If stagingBranch is provided, we don't need diffContents
|
|
const needsDiffContents = !stagingBranch
|
|
if (
|
|
!repo ||
|
|
!owner ||
|
|
!baseBranch ||
|
|
(needsDiffContents && !dependencies.isDiffContentsWellFormed(diffContents))
|
|
) {
|
|
res.status(400).send("Missing or malformed fields")
|
|
return
|
|
}
|
|
|
|
// Fetch user nickname and installation octokit in parallel (independent calls)
|
|
const [nickname, installationOctokit] = await Promise.all([
|
|
dependencies.userNickname(userId),
|
|
dependencies.getInstallationOctokitByOwner(dependencies.githubApp, owner, repo, userId),
|
|
])
|
|
|
|
if (nickname == null) {
|
|
res.status(401).json({ error: "Unauthorized" })
|
|
return
|
|
}
|
|
|
|
if (installationOctokit instanceof Error) {
|
|
res.status(401).json({ error: installationOctokit.message })
|
|
return
|
|
}
|
|
const isCollaborator = await dependencies.isUserCollaborator(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
nickname,
|
|
)
|
|
|
|
if (!isCollaborator) {
|
|
const errorMsg = `You are not a collaborator on ${owner}/${repo}. Please ensure you have write access to the repository.`
|
|
logger.info(`${nickname} is not a collaborator on ${owner}/${repo}`, req, {
|
|
nickname,
|
|
traceId,
|
|
repo: `${owner}/${repo}`,
|
|
})
|
|
return res.status(403).json({ error: errorMsg })
|
|
}
|
|
if (
|
|
isCfWebApp &&
|
|
!(await isBranchExists({ owner, repo, branch: baseBranch, installationOctokit }))
|
|
) {
|
|
res.status(404).json({
|
|
error: `The base branch "${baseBranch}" does not exist in ${owner}/${repo}. Please verify the branch name and try again.`,
|
|
})
|
|
return
|
|
}
|
|
logger.info(`Verified ${nickname} is a collaborator on ${owner}/${repo}`, req)
|
|
// TODO: Remove this background upsert logic after ensuring all old repositories have been saved.
|
|
dependencies
|
|
.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,
|
|
)
|
|
})
|
|
|
|
if (traceId && dependencies.requiresApproval(owner, repo) && !isCfWebApp) {
|
|
const optimization = await dependencies.prisma.optimization_features.findUnique({
|
|
where: { trace_id: traceId },
|
|
select: {
|
|
approval_required: true,
|
|
approval_status: true,
|
|
},
|
|
})
|
|
|
|
if (optimization?.approval_status === "rejected") {
|
|
return res.status(403).json({
|
|
status: "rejected",
|
|
message: "This optimization request was rejected.",
|
|
})
|
|
}
|
|
|
|
if (optimization?.approval_status === "approved") {
|
|
logger.info(`Request ${traceId} was previously approved, continuing with PR creation`, req)
|
|
const prNumber = await dependencies.triggerCreatePr(
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
nickname,
|
|
installationOctokit,
|
|
replayTests,
|
|
concolicTests,
|
|
traceId,
|
|
optimizationReview,
|
|
stagingBranch,
|
|
userTier,
|
|
organizationId,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
)
|
|
|
|
if (prNumber > 0) {
|
|
return res.json(prNumber)
|
|
} else {
|
|
return res.status(500).send("Error creating pull request")
|
|
}
|
|
} else {
|
|
const requestData = {
|
|
type: "create-pr",
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
replayTests,
|
|
concolicTests,
|
|
}
|
|
|
|
await dependencies.requestApproval(
|
|
traceId,
|
|
owner,
|
|
repo,
|
|
prCommentFields.function_name,
|
|
userId,
|
|
requestData,
|
|
)
|
|
|
|
return res.status(202).json({
|
|
status: "pending_approval",
|
|
message:
|
|
"This optimization requires approval. You will be notified when it is processed.",
|
|
})
|
|
}
|
|
}
|
|
|
|
const prNumber = await dependencies.triggerCreatePr(
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
nickname,
|
|
installationOctokit,
|
|
replayTests,
|
|
concolicTests,
|
|
traceId,
|
|
optimizationReview,
|
|
stagingBranch,
|
|
userTier,
|
|
organizationId,
|
|
originalLineProfiler,
|
|
optimizedLineProfiler,
|
|
)
|
|
|
|
if (prNumber > 0) {
|
|
// Check if this is a quality monitoring repo and send notification
|
|
if (traceId && isQualityMonitoringRepo(owner, repo)) {
|
|
const requestData = {
|
|
type: "create-pr",
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
}
|
|
|
|
const prUrl = `https://github.com/${owner}/${repo}/pull/${prNumber}`
|
|
|
|
// Send quality monitoring notification (non-blocking)
|
|
sendQualityMonitoringNotification(
|
|
traceId,
|
|
owner,
|
|
repo,
|
|
prCommentFields.function_name,
|
|
userId,
|
|
requestData,
|
|
prUrl,
|
|
).catch(error => {
|
|
logger.error(`Failed to send quality monitoring notification:`, req, {}, error as Error)
|
|
})
|
|
|
|
logger.info(
|
|
`Quality monitoring notification triggered for ${owner}/${repo} PR #${prNumber}`,
|
|
req,
|
|
)
|
|
}
|
|
|
|
return res.json(prNumber)
|
|
} else {
|
|
// If PR creation failed, fallback to staging
|
|
if (traceId) {
|
|
logger.info(`PR creation failed, falling back to staging for traceId: ${traceId}`, req)
|
|
try {
|
|
const stagingResult = await saveStagingReview(
|
|
req.body,
|
|
userId,
|
|
organizationId,
|
|
(req as any).subscriptionInfo,
|
|
)
|
|
if (stagingResult.status === 200) {
|
|
return res.status(200).json({
|
|
message: "PR creation 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, traceId, stagingResult },
|
|
new Error(`Staging fallback returned status ${stagingResult.status}`),
|
|
)
|
|
return res.status(stagingResult.status).json({
|
|
message: "PR creation failed and staging fallback also failed",
|
|
...stagingResult.data,
|
|
})
|
|
} catch (stagingError) {
|
|
logger.errorWithSentry(
|
|
`Staging fallback threw an exception:`,
|
|
req,
|
|
{ reqBody: req.body, userId, traceId },
|
|
stagingError as Error,
|
|
)
|
|
return res.status(500).json({
|
|
message: "PR creation failed and staging fallback threw an error",
|
|
error: stagingError instanceof Error ? stagingError.message : String(stagingError),
|
|
})
|
|
}
|
|
}
|
|
return res.status(500).send("Error creating pull request")
|
|
}
|
|
} catch (error) {
|
|
logger.errorWithSentry(
|
|
`Error in /cfapi/create-pr`,
|
|
req,
|
|
{
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
reqBody: req.body,
|
|
userId: (req as any).userId,
|
|
},
|
|
error as Error,
|
|
)
|
|
if (error instanceof Error) {
|
|
// Capture error in PostHog
|
|
dependencies.posthog?.capture({
|
|
distinctId: (req as any).userId,
|
|
event: `cfapi-create-pr-failed-error-creating-standalone-pr`,
|
|
properties: {
|
|
error: error.message,
|
|
},
|
|
})
|
|
res.status(500).send(`Error creating pull request: ${error.message}`)
|
|
} else {
|
|
res.status(500).send(`Error creating pull request`)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function triggerCreatePr(
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
diffContents,
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
userId,
|
|
nickname,
|
|
installationOctokit,
|
|
replayTests = "",
|
|
concolicTests = "",
|
|
traceId = "",
|
|
optimizationReview = "",
|
|
stagingBranch = "",
|
|
userTier = "free",
|
|
organizationId: string | null = null,
|
|
originalLineProfiler: string | undefined = undefined,
|
|
optimizedLineProfiler: string | undefined = undefined,
|
|
): Promise<number> {
|
|
const isPaidUser = userTier.toLowerCase() !== "free"
|
|
logger.info(
|
|
`[triggerCreatePr] Starting PR creation for ${owner}/${repo}`,
|
|
{
|
|
traceId,
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "trigger_create_pr",
|
|
owner,
|
|
repo,
|
|
},
|
|
{
|
|
traceId,
|
|
nickname,
|
|
baseBranch,
|
|
functionName: prCommentFields?.function_name,
|
|
stagingBranch: stagingBranch || "none (will create new branch)",
|
|
userTier,
|
|
isPaidUser,
|
|
},
|
|
)
|
|
|
|
try {
|
|
let newBranchName: string
|
|
|
|
// If staging branch is provided, use it directly instead of creating a new branch
|
|
if (stagingBranch) {
|
|
newBranchName = stagingBranch
|
|
console.log(`[triggerCreatePr] Using existing staging branch: ${newBranchName}`)
|
|
} else {
|
|
const diffContentsMap: Map<string, FileDiffContent> =
|
|
triggerCreatePrDeps.fileDiffsToMap(diffContents)
|
|
|
|
const timestamp = Date.now()
|
|
const encodedTimestamp = timestamp.toString(36)
|
|
|
|
newBranchName = `codeflash/optimize-${prCommentFields.function_name}-${encodedTimestamp}`
|
|
const commitMessage = `Optimize ${prCommentFields.function_name}\n\n${prCommentFields.optimization_explanation}`
|
|
|
|
logger.info(`Creating branch: ${newBranchName}`, {
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "create_branch",
|
|
owner,
|
|
repo,
|
|
})
|
|
const branchCreated = await triggerCreatePrDeps.createNewBranchFromDiffContents(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
newBranchName,
|
|
baseBranch,
|
|
diffContentsMap,
|
|
commitMessage,
|
|
)
|
|
|
|
if (!branchCreated) {
|
|
console.error(`[triggerCreatePr] Failed to create branch ${newBranchName}`)
|
|
throw new Error(`Failed to create branch ${newBranchName}`)
|
|
}
|
|
console.log(`[triggerCreatePr] Branch created successfully: ${newBranchName}`)
|
|
}
|
|
|
|
// Use the injectable function instead of the hardcoded one
|
|
const hasLineProfilerData = !!(originalLineProfiler || optimizedLineProfiler)
|
|
const { title, body } = triggerCreatePrDeps.createStandalonePRTitleAndBody(
|
|
prCommentFields,
|
|
existingTests,
|
|
generatedTests,
|
|
coverage_message,
|
|
newBranchName,
|
|
triggerCreatePrDeps.prContentBuilder,
|
|
replayTests,
|
|
concolicTests,
|
|
optimizationReview,
|
|
traceId,
|
|
hasLineProfilerData,
|
|
)
|
|
|
|
logger.info(`Creating PR with title: ${title}`, {
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "create_pr",
|
|
owner,
|
|
repo,
|
|
})
|
|
const newPrData = await triggerCreatePrDeps.createStandalonePullRequest(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
title,
|
|
body,
|
|
newBranchName,
|
|
baseBranch,
|
|
)
|
|
logger.info(`PR created successfully: #${newPrData.data.number}`, {
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "create_pr",
|
|
owner,
|
|
repo,
|
|
})
|
|
// Run post-PR-creation tasks in parallel:
|
|
// 1. DB optimization_events update (non-fatal errors caught internally)
|
|
// 2. GitHub API calls: assign reviewer + add labels (run in parallel with each other)
|
|
// 3. DB optimization_features update
|
|
const updateOptimizationEventsTask = (async () => {
|
|
try {
|
|
// Check existing data first (preserve staging data)
|
|
const existing = await triggerCreatePrDeps.prisma.optimization_events.findUnique({
|
|
where: { trace_id: traceId },
|
|
select: {
|
|
function_name: true,
|
|
speedup_x: true,
|
|
file_path: true,
|
|
speedup_pct: true,
|
|
staging_storage_type: true,
|
|
metadata: true,
|
|
},
|
|
})
|
|
|
|
const updateData: any = {
|
|
pr_id: String(newPrData.data.id),
|
|
pr_url: `https://github.com/${owner}/${repo}/pull/${newPrData.data.number}`,
|
|
is_optimization_found: true,
|
|
event_type: "pr_created",
|
|
}
|
|
|
|
// Check if we should clean up plain text data (user is paid OR org has subscription)
|
|
let shouldCleanupData = isPaidUser
|
|
if (!shouldCleanupData && organizationId && traceId) {
|
|
// Check if org has subscription
|
|
const org = await triggerCreatePrDeps.prisma.organizations.findUnique({
|
|
where: { id: organizationId },
|
|
select: { subscription: true },
|
|
})
|
|
if (org?.subscription) {
|
|
shouldCleanupData = true
|
|
console.log(
|
|
`[triggerCreatePr] Org has subscription - will cleanup plain text data for traceId: ${traceId}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
// If user is paid or org has subscription, convert to git_branch storage and clear diffContents
|
|
if (shouldCleanupData && traceId) {
|
|
if (existing) {
|
|
const currentMetadata = (existing.metadata ?? {}) as Record<string, unknown>
|
|
|
|
// Remove diffContents from metadata if it exists (plain_text mode stores it there)
|
|
if (currentMetadata.diffContents) {
|
|
delete currentMetadata.diffContents
|
|
}
|
|
|
|
// Update metadata with the new staging branch name
|
|
currentMetadata.staging_branch_name = newBranchName
|
|
currentMetadata.storageType = "git_branch"
|
|
|
|
// Add line profiler data if provided and not already present
|
|
addLineProfilerToMetadata(currentMetadata, originalLineProfiler, optimizedLineProfiler)
|
|
|
|
updateData.staging_storage_type = "git_branch"
|
|
updateData.metadata = currentMetadata
|
|
updateData.is_staging = true
|
|
console.log(
|
|
`[triggerCreatePr] Paid user/subscribed org: Converting storage to git_branch for traceId: ${traceId}`,
|
|
)
|
|
}
|
|
} else if (traceId && (originalLineProfiler || optimizedLineProfiler)) {
|
|
// For non-paid users, still add line profiler data if provided
|
|
const currentMetadata = (existing?.metadata ?? {}) as Record<string, unknown>
|
|
addLineProfilerToMetadata(currentMetadata, originalLineProfiler, optimizedLineProfiler)
|
|
updateData.metadata = currentMetadata
|
|
}
|
|
// Only add if missing (preserve staging data)
|
|
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, "%")
|
|
}
|
|
}
|
|
|
|
await triggerCreatePrDeps.prisma.optimization_events.update({
|
|
where: { trace_id: traceId },
|
|
data: updateData,
|
|
})
|
|
} catch (eventError) {
|
|
logger.error(
|
|
"Failed to update optimization event:",
|
|
{
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "update_optimization_event",
|
|
owner,
|
|
repo,
|
|
},
|
|
{},
|
|
eventError as Error,
|
|
)
|
|
}
|
|
})()
|
|
|
|
// Run reviewer assignment and label additions in parallel
|
|
const githubPostPrTasks: Array<Promise<void>> = [
|
|
triggerCreatePrDeps.assignReviewer(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
newPrData.data.number,
|
|
nickname,
|
|
),
|
|
triggerCreatePrDeps.addLabelToPullRequest(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
newPrData.data.number,
|
|
),
|
|
]
|
|
if (optimizationReview) {
|
|
githubPostPrTasks.push(
|
|
triggerCreatePrDeps.addLabelToPullRequest(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
newPrData.data.number,
|
|
`🎯 Quality: ${optimizationReview.charAt(0).toUpperCase() + optimizationReview.slice(1).toLowerCase()}`,
|
|
"FFC043",
|
|
"Optimization Quality according to Codeflash",
|
|
),
|
|
)
|
|
}
|
|
|
|
const updateOptimizationFeaturesTask = (async () => {
|
|
if (traceId !== "") {
|
|
const pull_request_db = await triggerCreatePrDeps.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.pull_request as any).new_pr_url = newPrData.data.html_url
|
|
|
|
await triggerCreatePrDeps.prisma.optimization_features.update({
|
|
where: {
|
|
trace_id: traceId,
|
|
},
|
|
data: {
|
|
pull_request: pull_request_db.pull_request,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
})()
|
|
|
|
// Wait for all post-PR tasks in parallel
|
|
await Promise.all([
|
|
updateOptimizationEventsTask,
|
|
Promise.all(githubPostPrTasks),
|
|
updateOptimizationFeaturesTask,
|
|
])
|
|
|
|
logger.info(`Created new PR #${newPrData.data.number} with branch ${newPrData.data.head.ref}`, {
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "pr_created",
|
|
owner,
|
|
repo,
|
|
})
|
|
triggerCreatePrDeps.posthog?.capture({
|
|
distinctId: userId,
|
|
event: `cfapi-create-pr-success-standalone-pr-created`,
|
|
properties: {
|
|
owner,
|
|
repo,
|
|
newPrNumber: newPrData.data.number,
|
|
newPrBranch: newPrData.data.head.ref,
|
|
PRURL: newPrData.data.html_url,
|
|
},
|
|
})
|
|
|
|
return newPrData.data.number
|
|
} catch (error) {
|
|
logger.errorWithSentry(
|
|
`Error creating PR for ${owner}/${repo}`,
|
|
{
|
|
userId,
|
|
endpoint: "/cfapi/create-pr",
|
|
operation: "trigger_create_pr",
|
|
owner,
|
|
repo,
|
|
traceId,
|
|
},
|
|
{
|
|
nickname,
|
|
baseBranch,
|
|
functionName: prCommentFields?.function_name,
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
},
|
|
error as Error,
|
|
)
|
|
return -1
|
|
}
|
|
}
|
|
|
|
// Endpoint to manually add repositories, similar to webhook logic
|
|
export async function addRepositoryManually(req: AuthorizedUserReq, res: Response): Promise<void> {
|
|
try {
|
|
const { repositories_added, installation } = req.body
|
|
|
|
if (!repositories_added || !installation?.id) {
|
|
res.status(400).send("Missing required fields")
|
|
return
|
|
}
|
|
|
|
const installationExists = await getAppInstallationByInstalltionId(installation.id)
|
|
|
|
if (!installationExists) {
|
|
await createAppInstallation({
|
|
installation_id: installation.id,
|
|
account_id: installation.account.id,
|
|
account_login: installation.account.login,
|
|
account_type: installation.account.type,
|
|
})
|
|
logger.info(`Installation created for ID: ${installation.id}`, req)
|
|
}
|
|
|
|
for (const repo of repositories_added) {
|
|
try {
|
|
await prisma.repositories.upsert({
|
|
where: { github_repo_id: String(repo.id) },
|
|
update: {
|
|
name: repo.name,
|
|
full_name: repo.full_name,
|
|
is_private: repo.private,
|
|
installation_id: installation.id,
|
|
},
|
|
create: {
|
|
github_repo_id: String(repo.id),
|
|
installation_id: installation.id,
|
|
name: repo.name,
|
|
full_name: repo.full_name,
|
|
is_private: repo.private,
|
|
is_active: true,
|
|
},
|
|
})
|
|
const savedRepo = await upsertRepository({
|
|
github_repo_id: String(repo.id),
|
|
installation_id: installation.id,
|
|
name: repo.name,
|
|
full_name: repo.full_name,
|
|
is_private: repo.private,
|
|
})
|
|
await createRepositoryMember({
|
|
repository_id: savedRepo.id,
|
|
user_id: req.userId,
|
|
role: "",
|
|
})
|
|
logger.info(`Repository upserted: ${repo.full_name}`, req)
|
|
} catch (error) {
|
|
logger.errorWithSentry(
|
|
`Failed to add/reactivate repository ${repo.full_name}`,
|
|
req,
|
|
{ repoId: repo.id, repoFullName: repo.full_name },
|
|
error as Error,
|
|
)
|
|
}
|
|
}
|
|
|
|
res.status(200).send("Repositories added successfully")
|
|
} catch (error) {
|
|
logger.errorWithSentry(`Error adding repositories:`, req, {}, error as Error)
|
|
if (error instanceof Error) {
|
|
res.status(500).send(`Error adding repositories: ${error.message}`)
|
|
} else {
|
|
res.status(500).send("Error adding repositories")
|
|
}
|
|
}
|
|
}
|