codeflash-internal/js/cf-api/github/github-app.ts
Sarthak Agarwal 1cb2051b15
consistency in formatting across ide & js projs (#1499)
### **PR Type**
- Enhancement



___

### **Description**
- Add pre-commit hook integration and formatting commands

- Introduce lint-staged and simple-git-hooks into package scripts

- Update prettier configuration and ignore files for consistency

- Refresh dependency lock files with new tooling entries


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Dependencies</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>package-lock.json</strong><dd><code>Update dependency lock
with new tooling entries</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-54c17cef859f033fc84a59da2e977235ebc494943710c25d132e310ec500c5ef">+754/-2</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>package-lock.json</strong><dd><code>Refresh package lock
with lint and formatting tools</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-75446c74353509ca0232d6a1350aef075ced8f72bd568e9bafa09cf255683142">+743/-0</a>&nbsp;
</td>

</tr>
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>4 files</summary><table>
<tr>
<td><strong>package.json</strong><dd><code>Add formatting, lint and
pre-commit hook scripts</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-4edec169b0f8d3312edaf35b5cc8521fe1edfa163ce174f60eff51906896601f">+34/-17</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Introduce formatting commands
and pre-commit hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-53ddfb1f8a02f1231d3d15a2e694ffe1407d2cc01d3e685de5653b67fec571c7">+18/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Integrate pre-commit hook and
formatting configurations</code>&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-b0d32af9c2caaba1377ec3e924eb553105cdc86e244018ffc6a866c530523599">+20/-3</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>settings.json</strong><dd><code>Enhance VSCode settings for
auto-format and lint fixes</code>&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-a5de3e5871ffcc383a2294845bd3df25d3eeff6c29ad46e3a396577c413bf357">+13/-1</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>7
files</summary><table>
<tr>
<td><strong>.editorconfig</strong><dd><code>Add consistent editor
settings for file formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-0947e2727d6bad8cd0ac4122f5314bb5b04e337393075bc4b5ef143b17fcbd5b">+32/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>.prettierrc</strong><dd><code>Update prettier config with
extended formatting rules</code>&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-ce5b7ae243151fb6eb3db1799b95d5c50ce2fe5080e8365c7834f81e8a44aade">+10/-4</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>.prettierrc</strong><dd><code>Update prettier settings for
consistent code style</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-e169a799a8a22863b844d1c816ebb5798c0bcf8151503b0329bf60a2b3050b03">+10/-4</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>.prettierrc</strong><dd><code>Add new prettier configuration
file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-7058ba9d421d7fa280582bcc9a2053e64ec0b2bb700ae46cb7073f295d154713">+10/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>.prettierignore</strong><dd><code>Extend ignore rules with
node_modules and dist folders</code>&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-a33307d68affc99ba88b1b79308f622350c8306bdeac2368b70d99ce72a7c8fa">+3/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>.prettierignore</strong><dd><code>Add ignore patterns for
node_modules and dist directories</code></dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-8f0741d174231baef1746c1fdb003dc727bb4416e16e99166edc020670861c1d">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>.prettierignore</strong><dd><code>Update ignore file to
include node_modules and dist folders</code></dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-e84a66c182e9d121fc156f4b50d606f385b591ed493f8c284628451d58907875">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td><strong>package-lock.json</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1499/files#diff-0214c85d1717ad8b736e0296bb8cbf50db2aed068f31316d3c39904824a14f8e">+1026/-52</a></td>

</tr>
</table></details></td></tr></tr></tbody></table>

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-03-04 23:52:45 +00:00

314 lines
12 KiB
TypeScript

import { App, createNodeMiddleware } from "octokit"
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"
const APP_ID: string = process.env.GH_APP_ID ?? "" // GitHub App ID
const APP_USER_ID: number = parseInt(process.env.GH_APP_USER_ID ?? "0") // GitHub App User ID
const PRIVATE_KEY: string =
process.env.NODE_ENV === "production"
? await getGithubAppPrivateKey()
: 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")
export const githubApp = 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 ?? "",
},
})
console.log(`Github App Initialized`)
const { data } = await githubApp.octokit.request("/app")
// Read more about custom logging: https://github.com/octokit/core.js#logging
githubApp.octokit.log.debug(`Authenticated as '${data.name}'`)
githubApp.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}'`)
githubApp.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}`)
})
githubApp.webhooks.on("pull_request.opened", async ({ octokit, payload }) => {
console.log(`Received a pull request opened event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("pull_request.edited", async ({ octokit, payload }) => {
console.log(`Received a pull request edited event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("pull_request.closed", async ({ octokit, payload }) => {
if (payload.pull_request) {
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 githubApp.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)
}
}
})
githubApp.webhooks.on("installation.created", async ({ octokit, payload }) => {
console.log(`Received a installation.created event: ${JSON.stringify(payload)}`)
})
githubApp.webhooks.on("installation_repositories.added", async ({ octokit, payload }) => {
console.log(`Received a installation_repositories.added event: ${JSON.stringify(payload)}`)
})
githubApp.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,
},
})
})
githubApp.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
githubApp.webhooks.onError(error => {
console.log(`Error occurred in Github App: ${error}`)
if (error instanceof Error && error.name === "AggregateError") {
// Log Secret verification errors
console.log(`Possible webhook secret verification error: ${JSON.stringify(error.event)}`)
} else {
console.log(error)
}
Sentry.captureException(error)
})
export const ghAppPathPrefix: string = "/cfapi/github"
export const ghAppMiddleware = createNodeMiddleware(githubApp, {
pathPrefix: ghAppPathPrefix,
})
const deleteBranchIfExists = async (installationOctokit, payload, branchName) => {
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) {
// 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)
}
}
}