import { App } from "octokit" import { createNodeMiddleware } from "@octokit/webhooks" import fs from "node:fs" import { getGithubAppPrivateKey, getGithubAppWebhookSecret, } from "@codeflash-ai/common/dist/src/azure-keyvault.js" import { posthog } from "../analytics.js" import * as Sentry from "@sentry/node" import { logger } from "../utils/logger.js" import { createAppInstallation, createOrUpdateUser, getAppInstallationByInstalltionId, organizationMemberRepository, organizationRepository, prisma, upsertRepository, upsertRepositoryMember, } from "@codeflash-ai/common" import { getUserRole } from "./github-utils.js" const APP_USER_ID: number = parseInt(process.env.GH_APP_USER_ID ?? "0") // GitHub App User ID /** Extract common webhook context fields for structured logging */ function webhookContext(payload: any, operation: string) { return { repoOwner: payload?.repository?.owner?.login, repoName: payload?.repository?.name, prNumber: payload?.pull_request?.number, installationId: payload?.installation?.id, operation, } } // Create a function to get config without initializing the app async function getGithubAppConfig() { const APP_ID: string = process.env.GH_APP_ID ?? "" const PRIVATE_KEY: string = process.env.NODE_ENV === "production" ? await getGithubAppPrivateKey() : process.env.NODE_ENV === "test" ? "test-private-key" : fs.readFileSync("github/Codeflash AI Dev GitHub App Private Key.pem", "utf8") const WEBHOOK_SECRET: string = process.env.NODE_ENV === "production" ? await getGithubAppWebhookSecret() : (process.env.GH_APP_WEBHOOK_SECRET ?? "default-secret") return { APP_ID, PRIVATE_KEY, WEBHOOK_SECRET } } // Initialize the app conditionally const initializeApp = async () => { const { APP_ID, PRIVATE_KEY, WEBHOOK_SECRET } = await getGithubAppConfig() return new App({ appId: APP_ID, privateKey: PRIVATE_KEY, webhooks: { secret: WEBHOOK_SECRET, }, oauth: { // OAuth details are currently unused by the app clientId: "", // process.env.GH_APP_CLIENT_ID ?? "", clientSecret: "", // process.env.GH_APP_CLIENT_SECRET ?? "", }, }) } // Export the actual App instance, initialized based on environment export const githubApp = await (async () => { // Check if GitHub App is configured const GH_APP_ID = process.env.GH_APP_ID if (!GH_APP_ID || GH_APP_ID === "" || process.env.NODE_ENV === "test") { logger.warn("GitHub App not configured (GH_APP_ID missing)", { operation: "server_startup" }) logger.warn("PR creation and GitHub webhook features are disabled", { operation: "server_startup", }) logger.info("CLI and optimization features will continue to work", { operation: "server_startup", }) // Return a minimal mock that won't fail return { octokit: { request: async () => ({ data: { name: "GitHub App Disabled" } }), log: { debug: () => {}, }, }, webhooks: { on: () => {}, onAny: () => {}, onError: () => {}, }, getInstallationOctokit: async () => { throw new Error("GitHub App not configured. Set GH_APP_ID to enable PR creation.") }, } as any as App } // In other environments, initialize normally logger.info(`GitHub App ID ${GH_APP_ID} detected, initializing...`, { operation: "server_startup", }) const app = await initializeApp() logger.info("GitHub App initialized", { operation: "server_startup" }) const { data } = await app.octokit.request("/app") // Read more about custom logging: https://github.com/octokit/core.js#logging app.octokit.log.debug(`Authenticated as '${data.name}'`) app.webhooks.onAny(async ({ id, name, payload }) => { // Only log event type and ID, not full payload (too verbose) logger.info( "GitHub App: Received webhook event", { operation: "webhook_received", repoOwner: (payload as any)?.repository?.owner?.login, repoName: (payload as any)?.repository?.name, }, { eventType: name, eventId: id }, ) posthog?.capture({ distinctId: `github|${payload.sender?.id}`, event: `cfapi-github-webhook-received`, properties: { event: name, id, }, }) }) logger.info(`GitHub App authenticated as '${data.name}'`, { operation: "server_startup" }) app.webhooks.on("installation", async ({ octokit, payload }) => { const account = payload.installation.account const accountName = account && "login" in account ? account.login : account && "slug" in account ? account.slug : "unknown" logger.info( `Received installation event: installation_id=${payload.installation.id}, account=${accountName}`, webhookContext(payload, "installation"), ) // Create an installation access token const installationAccessToken = await octokit.rest.apps.createInstallationAccessToken({ installation_id: payload.installation.id, }) // Don't log the token for security reasons }) app.webhooks.on("pull_request.opened", async ({ octokit, payload }) => { logger.info( `Received pull_request.opened event: PR #${payload.pull_request?.number} in ${payload.repository?.full_name}`, webhookContext(payload, "pull_request_opened"), ) }) app.webhooks.on("pull_request.edited", async ({ octokit, payload }) => { logger.info( `Received pull_request.edited event: PR #${payload.pull_request?.number} in ${payload.repository?.full_name}`, webhookContext(payload, "pull_request_edited"), ) }) app.webhooks.on("pull_request.closed", async ({ octokit, payload }) => { if (payload.pull_request) { const prId = String(payload.pull_request.id) try { const optimizationEvent = await prisma.optimization_events.findUnique({ where: { pr_id: prId }, select: { id: true, metadata: true }, }) if (optimizationEvent) { const currentMetadata = (optimizationEvent.metadata ?? {}) as Record // Remove line profiler data from metadata delete currentMetadata.originalLineProfiler delete currentMetadata.optimizedLineProfiler await prisma.optimization_events.update({ where: { id: optimizationEvent.id }, data: { event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed", metadata: currentMetadata as object, }, }) } logger.info( `Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"} and removed line profiler data`, webhookContext(payload, "pull_request_closed"), ) } catch (err) { logger.error( `Failed to update optimization_event for PR ID ${prId}:`, webhookContext(payload, "pull_request_closed"), {}, err as Error, ) } logger.info( `Received pull_request.closed event: PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed`, webhookContext(payload, "pull_request_closed"), ) // Check if the PR was merged and is a PR created by Codeflash const is_user_code_flash = payload.pull_request.user.id === APP_USER_ID try { if (payload.pull_request.merged && is_user_code_flash) { // Extract the original PR number from the branch name const dependentBranchNamePattern = /codeflash.optimize-pr(\d+)-\d{4}-\d{2}-\d{2}T.+$/ const standaloneBranchNamePattern = /codeflash.optimize-(.+)-\d{4}-\d{2}-\d{2}T.+$/ const dependentPrMatch = dependentBranchNamePattern.exec(payload.pull_request.head.ref) const standalonePrMatch = standaloneBranchNamePattern.exec(payload.pull_request.head.ref) if (dependentPrMatch != null) { const originalPrNumber = parseInt(dependentPrMatch[1]) let username = "You" if (payload.pull_request.merged_by != null) { // should not be null, but check anyway username = `@${payload.pull_request.merged_by.login}` } // Comment on the original PR await octokit.rest.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: originalPrNumber, body: `This PR is now faster! 🚀 ${username} accepted my optimizations from: - #${payload.pull_request.number}`, }) posthog?.capture({ distinctId: `github|${payload.sender.id}`, // this is the user who merged the PR event: `cfapi-github-dependent-pr-merged`, properties: { owner: payload.repository.owner.login, repo: payload.repository.name, originalPrNumber, dependentPrNumber: payload.pull_request.number, mergedBy: payload.pull_request.merged_by?.login, }, }) logger.info( `Commented on original PR #${originalPrNumber} and logged the event to PostHog`, webhookContext(payload, "dependent_pr_merged"), ) } else if (standalonePrMatch != null) { posthog?.capture({ distinctId: `github|${payload.sender.id}`, event: `cfapi-github-standalone-pr-merged`, properties: { owner: payload.repository.owner.login, repo: payload.repository.name, functionName: standalonePrMatch[1], prNumber: payload.pull_request.number, mergedBy: payload.pull_request.merged_by?.login, }, }) logger.info( `Logged standalone PR #${payload.pull_request.number} merge event to PostHog`, webhookContext(payload, "standalone_pr_merged"), ) } } } catch (mergedPrError) { logger.errorWithSentry( "Failed to process merged PR comment/analytics", webhookContext(payload, "pull_request_closed"), {}, mergedPrError as Error, ) } // Close any open optimization PRs targeting the branch of the closed PR // Ensure we only close PRs that are targeting the branch of the PR that was just closed const closedPrBranch = payload.pull_request.head.ref const closeCtx = webhookContext(payload, "close_dependent_prs") logger.info(`Closing optimization PRs targeting branch ${closedPrBranch}`, closeCtx, { is_user_code_flash, closedPrBranch, APP_USER_ID, }) if (payload.installation === undefined) { logger.error( "Installation ID is missing from payload. Cannot close PRs for this installation!", closeCtx, ) return } try { const installationOctokit = await app.getInstallationOctokit(payload.installation.id) const openPrs = await installationOctokit.rest.pulls.list({ owner: payload.repository.owner.login, repo: payload.repository.name, state: "open", base: closedPrBranch, }) logger.info( `Found ${openPrs.data.length} open PRs targeting branch ${closedPrBranch}`, closeCtx, { openPrCount: openPrs.data.length, openPrNumbers: openPrs.data.map(pr => pr.number).join(","), openPrUsers: openPrs.data .map(pr => `#${pr.number}:${pr.user?.login}(id=${pr.user?.id},type=${pr.user?.type})`) .join(","), }, ) for (const pr of openPrs.data) { // Check if the PR is opened by the Codeflash GitHub App and targets the same base branch as the closed PR if ( pr.user?.type === "Bot" && pr.user?.id === APP_USER_ID && pr.base.ref === closedPrBranch ) { await installationOctokit.rest.pulls.update({ owner: payload.repository.owner.login, repo: payload.repository.name, pull_number: pr.number, state: "closed", }) logger.info( `Closed optimization PR #${pr.number} targeting branch '${closedPrBranch}' because original PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed`, webhookContext(payload, "close_dependent_prs"), ) logger.info( "Posting pull request comment...", webhookContext(payload, "close_dependent_prs"), ) await octokit.rest.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: pr.number, body: `>This PR has been automatically closed because the original PR #${payload.pull_request.number} ` + `by ${payload.pull_request.user.login} was closed.`, }) // Delete the optimization PR's branch (safe: pr.user.id === APP_USER_ID verified above) await deleteBranchIfExists(installationOctokit, payload, pr.head.ref) } } // If the closed PR itself was created by Codeflash, delete its branch too. // Guard is correct here: we must NOT delete a user's feature branch. if (is_user_code_flash) { await deleteBranchIfExists(installationOctokit, payload, closedPrBranch) } } catch (error) { logger.errorWithSentry( `Failed to close optimization PRs targeting branch ${closedPrBranch}`, webhookContext(payload, "close_dependent_prs"), {}, error as Error, ) } } }) app.webhooks.on("installation.created", async ({ octokit, payload }) => { // TODO: check if it's organization const account = payload.installation.account const accountName = account && "login" in account ? account.login : account && "slug" in account ? account.slug : "unknown" logger.info( `Received installation.created event: installation_id=${payload.installation.id}, account=${accountName}`, webhookContext(payload, "installation_created"), ) }) app.webhooks.on("installation_repositories.added", async ({ octokit, payload }) => { const repoCount = payload.repositories_added?.length || 0 logger.info( `Received installation_repositories.added event: installation_id=${payload.installation.id}, repositories_added=${repoCount}`, webhookContext(payload, "installation_repositories_added"), ) }) app.webhooks.on("marketplace_purchase", async ({ id, name, payload }) => { logger.info( `Received marketplace purchase event: ${name} (${id})`, webhookContext(payload, "marketplace_purchase"), ) posthog?.capture({ distinctId: `github|${payload.sender.id}`, event: `cfapi-github-marketplace-purchase`, properties: { event: name, id, }, }) }) app.webhooks.on("pull_request.synchronize", async ({ octokit, payload }) => { if (payload.pull_request) { logger.info( `Received pull_request.synchronize event: PR #${payload.pull_request.number} by ${payload.pull_request?.user?.login} was updated with new commits`, webhookContext(payload, "pull_request_synchronize"), ) // Retrieve the list of commits for the pull request const commits = await octokit.rest.pulls.listCommits({ owner: payload.repository.owner.login, repo: payload.repository.name, pull_number: payload.pull_request.number, }) // Check the latest commit for the co-authored-by line const latestCommit = commits.data[commits.data.length - 1] if ( latestCommit.commit.message.includes( "Co-authored-by: codeflash-ai[bot] <148906541+codeflash-ai[bot]@users.noreply.github.com>", ) ) { // Log the event to Posthog posthog?.capture({ distinctId: `github|${payload.sender.id}`, event: `cfapi-github-commit-coauthored-by-codeflash`, properties: { prNumber: payload.pull_request.number, commitId: latestCommit.sha, repository: payload.repository.full_name, author: latestCommit.commit.author?.name, }, }) logger.info( `Logged co-authored commit to PostHog: ${latestCommit.sha}`, webhookContext(payload, "pull_request_synchronize"), ) // should not be null, but check anyway const authorname = latestCommit.commit.author?.name ?? "You" // Comment on the PR await octokit.rest.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: payload.pull_request.number, body: `This PR is now faster! 🚀 ${authorname} accepted my code suggestion above.`, }) logger.info( `Commented on PR #${payload.pull_request.number} about the accepted review comment`, webhookContext(payload, "pull_request_synchronize"), ) } } }) // Helper function to process feedback from PR comments async function processFeedbackComment({ octokit, commentBody, commentAuthor, commentId, prNumber, repository, commentType, }: { octokit: any commentBody: string commentAuthor: { id: number; login: string; email?: string | null; name?: string | null } commentId: number prNumber: number repository: { owner: { login: string }; name: string; full_name: string } commentType: "issue_comment" | "review_comment" }) { // Check if the comment mentions the bot // Support both @codeflash-ai and @codeflash-ai[bot] mentions // Extract feedback as everything after the mention const mentionPattern = /@codeflash-ai(?:\[bot\])?\s*([\s\S]*)/i const mentionMatch = mentionPattern.exec(commentBody) if (!mentionMatch) { return } const feedbackContent = mentionMatch[1].trim() if (!feedbackContent) { logger.info(`Empty feedback received from ${commentAuthor.login}, ignoring`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }) return } logger.info( `Received feedback (${commentType}) from ${commentAuthor.login} on PR #${prNumber}: "${feedbackContent.substring(0, 100)}..."`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }, ) // Helper to add reaction based on comment type const addReaction = async (content: "+1") => { if (commentType === "issue_comment") { await octokit.rest.reactions.createForIssueComment({ owner: repository.owner.login, repo: repository.name, comment_id: commentId, content, }) } else { await octokit.rest.reactions.createForPullRequestReviewComment({ owner: repository.owner.login, repo: repository.name, comment_id: commentId, content, }) } } try { // Find the optimization event associated with this PR const pr = await octokit.rest.pulls.get({ owner: repository.owner.login, repo: repository.name, pull_number: prNumber, }) const prId = String(pr.data.id) const prUrl = pr.data.html_url logger.info(`Looking for optimization event with pr_id=${prId} or pr_url=${prUrl}`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }) // Find optimization events by PR ID or by PR URL const optimizationEvent = await prisma.optimization_events.findFirst({ where: { OR: [ { pr_id: prId }, { pr_url: prUrl }, { pr_url: { contains: `/${repository.full_name}/pull/${prNumber}`, }, }, ], }, orderBy: { created_at: "desc", }, }) if (!optimizationEvent) { logger.info( `No optimization event found for PR #${prNumber} in ${repository.full_name} (pr_id=${prId})`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }, ) await addReaction("+1") return } logger.info( `Found optimization event: id=${optimizationEvent.id}, trace_id=${optimizationEvent.trace_id}`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }, ) // Create or get the user const user = await createOrUpdateUser( `github|${commentAuthor.id}`, commentAuthor.login, commentAuthor.email ?? null, commentAuthor.name ?? null, ) // Atomically append feedback to optimization event's feedback field (JSON array) // Using SELECT FOR UPDATE to lock the row and prevent race conditions const newFeedbackEntry = { content: feedbackContent, authorLogin: commentAuthor.login, authorId: commentAuthor.id, commentType, createdAt: new Date().toISOString(), } await prisma.$transaction(async tx => { // Lock the row with FOR UPDATE to prevent concurrent modifications const [lockedEvent] = await tx.$queryRaw>` SELECT feedback FROM optimization_events WHERE id = ${optimizationEvent.id} FOR UPDATE ` const existingFeedback = (lockedEvent.feedback as any[]) || [] await tx.optimization_events.update({ where: { id: String(optimizationEvent.id) }, data: { feedback: [...existingFeedback, newFeedbackEntry], }, }) }) logger.info( `Saved feedback from ${commentAuthor.login} for optimization event ${optimizationEvent.id} (trace_id: ${optimizationEvent.trace_id})`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }, ) // Log to PostHog posthog?.capture({ distinctId: `github|${commentAuthor.id}`, event: `cfapi-github-feedback-received`, properties: { prNumber, repository: repository.full_name, optimizationEventId: optimizationEvent.id, traceId: optimizationEvent.trace_id, feedbackContent, feedbackLength: feedbackContent.length, commentType, functionName: optimizationEvent.function_name, filePath: optimizationEvent.file_path, speedupX: optimizationEvent.speedup_x, speedupPct: optimizationEvent.speedup_pct, authorLogin: commentAuthor.login, }, }) // React with a thumbs up to acknowledge the feedback await addReaction("+1") } catch (error) { logger.errorWithSentry( `Failed to process feedback from ${commentAuthor.login}`, { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }, {}, error as Error, ) try { await addReaction("+1") } catch (reactionError) { logger.error( "Failed to add reaction:", { operation: "process_feedback", repoOwner: repository.owner.login, repoName: repository.name, prNumber, }, {}, reactionError as Error, ) } } } // Handle PR conversation thread comments (issue_comment) app.webhooks.on("issue_comment.created", async ({ octokit, payload }) => { // Only process comments on pull requests (not regular issues) if (!payload.issue.pull_request) { return } await processFeedbackComment({ octokit, commentBody: payload.comment.body, commentAuthor: payload.comment.user, commentId: payload.comment.id, prNumber: payload.issue.number, repository: payload.repository, commentType: "issue_comment", }) }) // Handle PR review comments (comments on specific lines of code) app.webhooks.on("pull_request_review_comment.created", async ({ octokit, payload }) => { await processFeedbackComment({ octokit, commentBody: payload.comment.body, commentAuthor: payload.comment.user, commentId: payload.comment.id, prNumber: payload.pull_request.number, repository: payload.repository, commentType: "review_comment", }) }) // Optional: Handle errors app.webhooks.onError(error => { const errorContext = { operation: "webhook_error" } logger.error(`Error occurred in GitHub App's onError handler: ${error}`, errorContext) if (error instanceof Error) { // Check if it's an AggregateError, common for signature issues if (error.name === "AggregateError" && Array.isArray((error as any).errors)) { logger.error( "AggregateError details (possible secret mismatch or multiple issues):", errorContext, ) ;(error as any).errors.forEach((subError: Error, i: number) => { logger.error(` Sub-error ${i + 1}: ${subError.message}`, errorContext) }) } else if (error.message.includes("content length")) { logger.error( "Content length mismatch detected by Octokit. Payload may be truncated or header incorrect.", errorContext, ) const eventRequest = (error as any).event?.request if (eventRequest?.headers) { logger.error("Request headers from error.event:", errorContext, { headers: JSON.stringify(eventRequest.headers, null, 2), }) } } // Log the full error structure for better debugging logger.error("Full error object (onError):", errorContext, { errorDetails: JSON.stringify(error, Object.getOwnPropertyNames(error), 2), }) } else { logger.error("Full error (onError, non-Error instance):", errorContext, { errorDetails: String(error), }) } Sentry.captureException(error) }) app.webhooks.on("installation_repositories", async ({ payload }) => { const repoCount = payload.repositories_added?.length || 0 logger.info( `Received installation_repositories event: installation_id=${payload.installation?.id}, repositories_added=${repoCount}`, webhookContext(payload, "installation_repositories"), ) const { repositories_added, installation, sender } = payload // Check if required fields are missing if (!repositories_added || !installation?.id) { logger.warn( "Missing repositories_added or installation.id", webhookContext(payload, "installation_repositories"), ) return } const account = installation.account let accountLogin: string | undefined // Check if the account is a user or an organization if ("login" in account) { // It's a user account, use `login` accountLogin = account.login } else if ("slug" in account) { // It's an organization account, use `slug` accountLogin = account.slug } let accountType: string | undefined if ("type" in account) { accountType = account.type } else { accountType = "Organization" // fallback assumption } if (!accountLogin) { logger.error( "Account login or slug not found", webhookContext(payload, "installation_repositories"), ) return } // Check if the installation exists, if not, create it const installationExists = await getAppInstallationByInstalltionId(installation.id) if (!installationExists) { // Create the installation if it doesn't exist await createAppInstallation({ installation_id: installation.id, account_id: installation.account.id, account_login: accountLogin, account_type: accountType, }) logger.info( `Installation created for ID: ${installation.id}`, webhookContext(payload, "installation_repositories"), ) } // Process each repository in the list of added repositories for (const repo of repositories_added) { try { // Get the GitHub user ID of the sender const githubUserId = sender?.id if (githubUserId) { logger.info( `GitHub User ID: ${githubUserId} triggered the event`, webhookContext(payload, "installation_repositories"), ) // Fetch the user's role using the helper // Use octokit from getInstallationOctokit for this installation const installationOctokit = await app.getInstallationOctokit(installation.id) const userRole = await getUserRole({ octokit: installationOctokit, owner: accountLogin, repo: repo.name, username: sender.login, isOrg: accountType === "Organization", }) logger.info( `Fetched user role: ${userRole}`, webhookContext(payload, "installation_repositories"), ) const user = await createOrUpdateUser( `github|${githubUserId}`, sender.login, sender.email ?? null, sender.name ?? null, ) let orgId: string = undefined if (accountType === "Organization") { const ghOrgId = String(account.id) const existingOrg = await prisma.organizations.findUnique({ where: { github_org_id: ghOrgId }, }) orgId = existingOrg ? String(existingOrg.id) : undefined if (!existingOrg) { const organization = await organizationRepository.upsertOrganization({ github_org_id: ghOrgId, name: account.name || accountLogin, added_by: user.user_id, }) await organizationMemberRepository.addMember({ organizationId: organization.id, userId: user.user_id, role: "admin", addedBy: user.user_id, // Indicates that this user was the first to be added . If user_id equals addedBy, it means this user installed GitHub App for this repository. }) logger.info( `Organization upserted: ${accountLogin}`, webhookContext(payload, "installation_repositories"), ) orgId = organization.id } } 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, organization_id: orgId, }) logger.info( `Repository upserted: ${savedRepo.full_name}`, webhookContext(payload, "installation_repositories"), ) await upsertRepositoryMember({ repository_id: savedRepo.id, user_id: user.user_id, role: userRole, }) } else { logger.error( "GitHub User ID not found in sender", webhookContext(payload, "installation_repositories"), ) } } catch (error) { logger.errorWithSentry( `Failed to add/reactivate repository ${repo.full_name}`, webhookContext(payload, "installation_repositories"), {}, error as Error, ) } } }) return app })() export const ghAppPathPrefix: string = "/cfapi/github" // Path is already prefixed with /cfapi/github in the appExpress.postAsync handler export const ghAppMiddleware = createNodeMiddleware(githubApp.webhooks, { path: "/cfapi/github/webhooks", }) const deleteBranchIfExists = async (installationOctokit: any, payload: any, branchName: string) => { const ctx = { ...webhookContext(payload, "delete_branch"), branchName, } try { logger.info(`Deleting branch '${branchName}' associated with the closed PR`, ctx) // Check if the branch exists by querying the reference const ref = await installationOctokit.rest.git.getRef({ owner: payload.repository.owner.login, repo: payload.repository.name, ref: `heads/${branchName}`, }) // If the ref is found, it means the branch exists, and you can delete it logger.info(`Branch exists: ${ref.data.ref}`, ctx) // Proceed to delete the branch await installationOctokit.rest.git.deleteRef({ owner: payload.repository.owner.login, repo: payload.repository.name, ref: `heads/${branchName}`, }) logger.info(`Branch '${branchName}' has been deleted`, ctx) } catch (error: any) { // If the branch doesn't exist or other errors occur, catch the error if (error.status === 404) { logger.info(`Branch '${branchName}' does not exist`, ctx) } else { logger.error( `Error checking branch existence or deleting '${branchName}':`, ctx, {}, error as Error, ) } } }