codeflash-internal/js/cf-api/endpoints/create-pr.ts
Kevin Turcios d7a8b8f227
perf: fix CI build + lazy-load heavy libs + parallelize DB queries (#2601)
## 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
2026-04-13 11:03:05 -05:00

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")
}
}
}