codeflash-internal/js/cf-api/github/github-app.ts
Saurabh Misra 7c1933180a
local setup (#1898)
Signed-off-by: Saurabh Misra <misra.saurabh1@gmail.com>
Co-authored-by: saga4 <saga4@codeflashs-MacBook-Air.local>
Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
Co-authored-by: Mohamed Ashraf <mohamedashrraf222@gmail.com>
Co-authored-by: Aseem Saxena <aseem.bits@gmail.com>
2025-11-17 12:35:09 -08:00

544 lines
20 KiB
TypeScript

import { App } from "octokit"
import { createNodeMiddleware } from "@octokit/webhooks"
import fs from "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 {
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
// 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") {
console.log("caution: GitHub App not configured (GH_APP_ID missing)")
console.log("caution: PR creation and GitHub webhook features are disabled")
console.log("caution: CLI and optimization features will continue to work")
// 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
console.log(`GitHub App ID ${GH_APP_ID} detected, initializing...`)
const app = await initializeApp()
console.log(`Github App Initialized`)
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 }) => {
console.log(`Github App: Received webhook event ${name} (${id})`)
console.log(`Payload: ${JSON.stringify(payload)}`)
posthog.capture({
distinctId: `github|${payload.sender?.id}`,
event: `cfapi-github-webhook-received`,
properties: {
event: name,
id,
},
})
})
console.log(`Github App Authenticated as '${data.name}'`)
app.webhooks.on("installation", async ({ octokit, payload }) => {
console.log(`Received a new installation event: ${JSON.stringify(payload)}`)
// Create an installation access token
const installationAccessToken = await octokit.rest.apps.createInstallationAccessToken({
installation_id: payload.installation.id,
})
console.log(`Installation access token: ${installationAccessToken.data.token}`)
})
app.webhooks.on("pull_request.opened", async ({ octokit, payload }) => {
console.log(`Received a pull request opened event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("pull_request.edited", async ({ octokit, payload }) => {
console.log(`Received a pull request edited event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("pull_request.closed", async ({ octokit, payload }) => {
if (payload.pull_request) {
const prId = String(payload.pull_request.id)
try {
await prisma.optimization_events.updateMany({
where: { pr_id: prId },
data: {
event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed",
},
})
console.log(
`Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"}`,
)
} catch (err) {
console.error(`Failed to update optimization_event for PR ID ${prId}:`, err)
}
console.log(
`Received a pull request closed event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request.user.login} was 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
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,
},
})
console.log(
`Commented on original PR #${originalPrNumber} and logged the event to Posthog.`,
)
} 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,
},
})
console.log(
`Logged standalone PR #${payload.pull_request.number} merge event to Posthog.`,
)
}
}
// 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
// Logic to close any open optimization PRs targeting this branch
console.log(`Closing optimization PRs targeting branch ${closedPrBranch}...`)
if (payload.installation === undefined) {
console.error(
`Error! Installation ID is missing from payload. Cannot close PRs for this installation!`,
)
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,
})
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",
})
console.log(
`Closed optimization PR #${pr.number} targeting branch '${closedPrBranch}' ` +
`because original PR #${payload.pull_request.number} by ${payload.pull_request.user.login} was closed.`,
)
console.log(`Posting pull request comment...`)
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.`,
})
// Proceed to delete the branch
if (is_user_code_flash) {
await deleteBranchIfExists(installationOctokit, payload, `heads/${pr.head.ref}`)
}
}
}
// If there was no open PR's, still delete the branch in case of inline comment
if (is_user_code_flash) {
await deleteBranchIfExists(installationOctokit, payload, closedPrBranch)
}
} catch (error) {
console.error(
`Failed to close optimization PRs targeting branch ${closedPrBranch}: ${error}`,
)
Sentry.captureException(error)
}
}
})
app.webhooks.on("installation.created", async ({ octokit, payload }) => {
// TODO: check if it's organization
console.log(`Received a installation.created event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("installation_repositories.added", async ({ octokit, payload }) => {
console.log(`Received a installation_repositories.added event: ${JSON.stringify(payload)}`)
})
app.webhooks.on("marketplace_purchase", async ({ id, name, payload }) => {
console.log(`Received a marketplace purchase event: ${name} (${id})`)
console.log(`Payload: ${JSON.stringify(payload)}`)
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) {
console.log(
`Received a pull request synchronize event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request?.user?.login} was updated with new commits.`,
)
// 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,
},
})
console.log(`Logged co-authored commit to Posthog: ${latestCommit.sha}`)
// 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.`,
})
console.log(
`Commented on PR #${payload.pull_request.number} about the accepted review comment.`,
)
}
}
})
// Optional: Handle errors
app.webhooks.onError(error => {
console.error(`Error occurred in Github App's onError handler: ${error}`)
if (error instanceof Error) {
// Check if it's an AggregateError, common for signature issues
if (error.name === "AggregateError" && Array.isArray((error as any).errors)) {
console.error("AggregateError details (possible secret mismatch or multiple issues):")
;(error as any).errors.forEach((subError: Error, i: number) => {
console.error(` Sub-error ${i + 1}: ${subError.message}`)
})
} else if (error.message.includes("content length")) {
console.error(
"Content length mismatch detected by Octokit. Payload may be truncated or header incorrect.",
)
const eventRequest = (error as any).event?.request
if (eventRequest && eventRequest.headers) {
console.error(
"Request headers from error.event:",
JSON.stringify(eventRequest.headers, null, 2),
)
}
}
// Log the full error structure for better debugging
console.error(
"Full error object (onError):",
JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
)
} else {
console.error("Full error (onError, non-Error instance):", error)
}
Sentry.captureException(error)
})
app.webhooks.on("installation_repositories", async ({ payload }) => {
console.log(`Received a installation_repositories event: ${JSON.stringify(payload)}`)
const { repositories_added, installation, sender } = payload
// Check if required fields are missing
if (!repositories_added || !installation?.id) {
console.log(
`Missing repositories_added or installation.id. Full payload: ${JSON.stringify(payload, null, 2)}`,
)
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) {
console.error("Error: Account login or slug not found")
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,
})
console.log(`Installation created for ID: ${installation.id}`)
}
// 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) {
console.log(`GitHub User ID: ${githubUserId} triggered the event`)
// 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",
})
console.log(`Fetched user role: ${userRole}`)
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?.id
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.
})
console.log(`Organization upserted: ${accountLogin}`)
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,
})
console.log(`Repository upserted: ${savedRepo.full_name}`)
await upsertRepositoryMember({
repository_id: savedRepo.id,
user_id: user.user_id,
role: userRole,
})
} else {
console.error("GitHub User ID not found in sender.")
}
} catch (error) {
console.error(`Failed to add/reactivate repository ${repo.full_name}:`, error)
Sentry.captureException(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) => {
try {
console.log(`Deleting the branch associated with the closed PR...`)
// 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
console.log(`Check Branch exists: ${ref.data.ref}`)
// Proceed to delete the branch
await installationOctokit.rest.git.deleteRef({
owner: payload.repository.owner.login,
repo: payload.repository.name,
ref: `heads/${branchName}`,
})
console.log(`Branch '${branchName}' has been deleted.`)
} catch (error: any) {
// If the branch doesn't exist or other errors occur, catch the error
if (error.status === 404) {
console.log("Branch does not exist!")
} else {
console.error("Error checking branch existence or deleting:", error)
}
}
}