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
589 lines
17 KiB
TypeScript
589 lines
17 KiB
TypeScript
import { Octokit, type App } from "octokit"
|
|
import * as Sentry from "@sentry/node"
|
|
import {
|
|
createAppInstallation,
|
|
createOrUpdateUser,
|
|
deleteOrganizationMemberApiKeys,
|
|
getAppInstallationByInstalltionId,
|
|
getUserById,
|
|
organizationMemberRepository,
|
|
organizationRepository,
|
|
prisma,
|
|
upsertRepository,
|
|
} from "@codeflash-ai/common"
|
|
import { sendSlackMessage } from "./slack_util.js"
|
|
import { isCodeflashEmployee } from "../utils/employee-utils.js"
|
|
|
|
// Channel for GitHub App installation alerts (falls back to default channel)
|
|
const GITHUB_ALERTS_CHANNEL = process.env.SLACK_CHANNEL_ID
|
|
|
|
// Dependencies interface for easier testing
|
|
export interface GithubUtilsDependencies {
|
|
console: {
|
|
error: typeof console.error
|
|
}
|
|
}
|
|
|
|
// Default dependencies
|
|
let dependencies: GithubUtilsDependencies = {
|
|
console: {
|
|
error: console.error,
|
|
},
|
|
}
|
|
|
|
// For testing - allow dependency injection
|
|
export function setGithubUtilsDependencies(deps: Partial<GithubUtilsDependencies>) {
|
|
dependencies = { ...dependencies, ...deps }
|
|
}
|
|
|
|
export function resetGithubUtilsDependencies() {
|
|
dependencies = {
|
|
console: {
|
|
error: console.error,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Checks if the user is a collaborator on the repository
|
|
export async function isUserCollaborator(
|
|
installationOctokit: Octokit,
|
|
owner: string,
|
|
repo: string,
|
|
nickname: string,
|
|
): Promise<boolean> {
|
|
try {
|
|
const response = await installationOctokit.rest.repos.checkCollaborator({
|
|
owner,
|
|
repo,
|
|
username: nickname,
|
|
})
|
|
// If the request is successful, the user is a collaborator and we can continue
|
|
return response.status === 204
|
|
} catch (error: any) {
|
|
if (error.status === 404) {
|
|
return false // User is not a collaborator
|
|
}
|
|
// Handle other potential errors
|
|
throw error // Rethrow error for upstream handling
|
|
}
|
|
}
|
|
|
|
export async function getInstallationOctokitByOwner(
|
|
githubApp: App,
|
|
owner: string,
|
|
repo: string,
|
|
userId?: string,
|
|
): Promise<Octokit | Error> {
|
|
try {
|
|
const repoInstallation = await githubApp.octokit.rest.apps.getRepoInstallation({
|
|
owner,
|
|
repo,
|
|
})
|
|
const installationId = repoInstallation.data.id
|
|
return await githubApp.getInstallationOctokit(installationId)
|
|
} catch (error: any) {
|
|
dependencies.console.error(`Error getting installation Octokit for ${owner}/${repo}:`, error)
|
|
if (error.status === 404) {
|
|
// Send Slack alert for GitHub App installation failures
|
|
// This helps the team proactively reach out to affected customers
|
|
// Skip notification for Codeflash employees to reduce internal noise
|
|
if (!userId || !isCodeflashEmployee(userId)) {
|
|
sendSlackMessage(
|
|
{
|
|
text: `GitHub App Installation Issue`,
|
|
blocks: [
|
|
{
|
|
type: "header",
|
|
text: {
|
|
type: "plain_text",
|
|
text: "GitHub App Installation Issue",
|
|
emoji: true,
|
|
},
|
|
},
|
|
{
|
|
type: "section",
|
|
fields: [
|
|
{
|
|
type: "mrkdwn",
|
|
text: `*Repository:*\n${owner}/${repo}`,
|
|
},
|
|
{
|
|
type: "mrkdwn",
|
|
text: `*Error:*\nApp not installed (404)`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: "section",
|
|
text: {
|
|
type: "mrkdwn",
|
|
text: `This may indicate the customer needs to accept updated GitHub App permissions or reinstall the app.\n\n<https://github.com/${owner}|View Organization on GitHub>`,
|
|
},
|
|
},
|
|
{
|
|
type: "context",
|
|
elements: [
|
|
{
|
|
type: "mrkdwn",
|
|
text: `Timestamp: ${new Date().toISOString()}`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
GITHUB_ALERTS_CHANNEL,
|
|
).catch(slackError => {
|
|
// Don't let Slack errors affect the main flow
|
|
console.error("Failed to send Slack alert for GitHub App installation issue:", slackError)
|
|
})
|
|
}
|
|
|
|
return Error(`GitHub App is not installed on the repository ${owner}/${repo}`)
|
|
} else {
|
|
return Error(`Error checking GitHub App installation status for repo ${owner}/${repo}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
function isLabelAlreadyExistsError(error: any): boolean {
|
|
return (
|
|
error?.status === 422 &&
|
|
Array.isArray(error?.response?.data?.errors) &&
|
|
error.response.data.errors.some(
|
|
(e: any) => e.resource === "Label" && e.code === "already_exists" && e.field === "name",
|
|
)
|
|
)
|
|
}
|
|
|
|
// Ensures that a label exists on a repository, creating it if it doesn't
|
|
export async function ensureLabelExists(
|
|
installationOctokit: Octokit,
|
|
owner: string,
|
|
repo: string,
|
|
labelName: string,
|
|
color: string,
|
|
description: string,
|
|
): Promise<void> {
|
|
try {
|
|
await installationOctokit.rest.issues.getLabel({
|
|
owner,
|
|
repo,
|
|
name: labelName,
|
|
})
|
|
} catch (error: any) {
|
|
if (error.status === 404) {
|
|
// Label does not exist, create it
|
|
try {
|
|
await installationOctokit.rest.issues.createLabel({
|
|
owner,
|
|
repo,
|
|
name: labelName,
|
|
color,
|
|
description,
|
|
})
|
|
} catch (createError: any) {
|
|
// Ignore "already_exists" error - another concurrent request may have created it
|
|
if (!isLabelAlreadyExistsError(createError)) {
|
|
Sentry.captureException(createError, {
|
|
extra: { owner, repo, labelName, context: "ensureLabelExists - createLabel failed" },
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
// Log to Sentry and prevent throwing the error
|
|
Sentry.captureException(error, {
|
|
extra: { owner, repo, labelName, context: "ensureLabelExists - getLabel failed" },
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adds a label to a pull request, creating the label if it doesn't exist
|
|
export async function addLabelToPullRequest(
|
|
installationOctokit: Octokit,
|
|
owner: string,
|
|
repo: string,
|
|
pullNumber: number,
|
|
labelName: string = "⚡️ codeflash",
|
|
labelColor: string = "FFC043",
|
|
description: string = "Optimization PR opened by Codeflash AI",
|
|
): Promise<void> {
|
|
await ensureLabelExists(installationOctokit, owner, repo, labelName, labelColor, description)
|
|
|
|
await installationOctokit.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: pullNumber,
|
|
labels: [labelName],
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Fetches the user's role in an organization.
|
|
* Expected values: "admin" (owner), "member" (normal member).
|
|
*/
|
|
async function fetchOrgRole(octokit: Octokit, owner: string, username: string): Promise<string> {
|
|
try {
|
|
const { data: membership } = await octokit.rest.orgs.getMembershipForUser({
|
|
org: owner,
|
|
username,
|
|
})
|
|
return membership.role || ""
|
|
} catch (error) {
|
|
console.error(`Error fetching org role for ${username} in ${owner}:`, error)
|
|
Sentry.captureException(error)
|
|
return ""
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches the user's permission level in a repository.
|
|
* Expected values: "admin", "maintain", "write", "triage", "read".
|
|
*/
|
|
async function fetchRepoRole(
|
|
octokit: Octokit,
|
|
owner: string,
|
|
repo: string,
|
|
username: string,
|
|
): Promise<string> {
|
|
try {
|
|
const { data: permissions } = await octokit.rest.repos.getCollaboratorPermissionLevel({
|
|
owner,
|
|
repo,
|
|
username,
|
|
})
|
|
console.log(`Repo role for ${username} in ${owner}/${repo}: ${permissions.permission}`)
|
|
return permissions.permission || ""
|
|
} catch (error) {
|
|
console.error(`Error fetching repo role for ${username} in ${owner}/${repo}:`, error)
|
|
Sentry.captureException(error)
|
|
return ""
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the user's role in an org or a repo, depending on what's available.
|
|
* If it fails, logs the error to Sentry and returns an empty string.
|
|
*/
|
|
export async function getUserRole({
|
|
octokit,
|
|
owner,
|
|
repo,
|
|
username,
|
|
isOrg,
|
|
}: {
|
|
octokit: Octokit
|
|
owner: string
|
|
repo: string
|
|
username: string
|
|
isOrg: boolean
|
|
}): Promise<string> {
|
|
if (isOrg) {
|
|
// Fetch org membership role
|
|
// Expected values: "admin" (org owner) or "member" (regular member)
|
|
return await fetchOrgRole(octokit, owner, username)
|
|
} else {
|
|
// Fetch repo permission
|
|
// Expected values: "admin", "maintain", "write", "triage", "read"
|
|
return await fetchRepoRole(octokit, owner, repo, username)
|
|
}
|
|
}
|
|
|
|
async function getInstallations(app: App) {
|
|
const installations: any[] = []
|
|
let page = 1
|
|
|
|
console.log("fetching installations...")
|
|
|
|
while (true) {
|
|
try {
|
|
const response = await app.octokit.rest.apps.listInstallations({
|
|
per_page: 90, // max is 90
|
|
page,
|
|
})
|
|
|
|
if (!response.data.length) break
|
|
|
|
installations.push(...response.data)
|
|
page++
|
|
} catch (error) {
|
|
console.error("Error fetching installations:", error)
|
|
Sentry.captureException(error)
|
|
break
|
|
}
|
|
}
|
|
|
|
console.log("Done")
|
|
return installations
|
|
}
|
|
/**
|
|
* Checks whether a branch exists in a GitHub repository.
|
|
*
|
|
* @param {object} params - Parameters for branch existence check.
|
|
* @param {string} params.owner - The repository owner.
|
|
* @param {string} params.repo - The repository name.
|
|
* @param {string} params.branch - The branch name to check.
|
|
* @param {Octokit} params.installationOctokit - The authenticated installation Octokit client for the repo.
|
|
* @returns {Promise<boolean>} - Returns true if the branch exists, false if not.
|
|
*
|
|
* @example
|
|
* const exists = await isBranchExists({
|
|
* owner: "octocat",
|
|
* repo: "Hello-World",
|
|
* branch: "feature-branch",
|
|
* installationOctokit,
|
|
* });
|
|
* console.log("Branch exists:", exists);
|
|
*/
|
|
export async function isBranchExists({
|
|
owner,
|
|
repo,
|
|
branch,
|
|
installationOctokit,
|
|
}: {
|
|
owner: string
|
|
repo: string
|
|
branch: string
|
|
installationOctokit: Octokit
|
|
}): Promise<boolean> {
|
|
try {
|
|
await installationOctokit.rest.git.getRef({
|
|
owner,
|
|
repo,
|
|
ref: `heads/${branch}`,
|
|
})
|
|
return true
|
|
} catch (error: any) {
|
|
if (error.status === 404) {
|
|
return false
|
|
}
|
|
Sentry.captureException(error)
|
|
dependencies.console.error(
|
|
`Error checking if branch ${branch} exists in ${owner}/${repo}:`,
|
|
error,
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function filterInstalltionAccountByOrg(installations: any, orgNames?: string[]) {
|
|
let orgInstallations: typeof installations = []
|
|
for (const installation of installations) {
|
|
const login = installation.account?.login
|
|
if (installation.account?.type === "Organization" && login) {
|
|
orgInstallations.push(installation)
|
|
}
|
|
}
|
|
if (orgNames && orgNames.length) {
|
|
orgInstallations = orgInstallations.filter(install => orgNames.includes(install.account!.login))
|
|
}
|
|
return orgInstallations
|
|
}
|
|
|
|
async function getReposForInstallation(installationOctokit: Octokit): Promise<any[]> {
|
|
const repos: any[] = []
|
|
let page = 1
|
|
|
|
while (true) {
|
|
try {
|
|
const reposResponse = await installationOctokit.rest.apps.listReposAccessibleToInstallation({
|
|
per_page: 90,
|
|
page,
|
|
})
|
|
|
|
if (!reposResponse.data.repositories.length) break
|
|
|
|
repos.push(...reposResponse.data.repositories)
|
|
page++
|
|
} catch (error) {
|
|
console.error("Error fetching repos for installation:", error)
|
|
Sentry.captureException(error)
|
|
break
|
|
}
|
|
}
|
|
|
|
return repos
|
|
}
|
|
|
|
async function getMembersWithRolesForOrg(
|
|
installationOctokit: Octokit,
|
|
orgLogin: string,
|
|
): Promise<Array<{ id: number; username: string; role: string }>> {
|
|
const members: Array<{ id: number; username: string; role: string }> = []
|
|
const memberData: Array<{ id: number; login: string }> = []
|
|
let page = 1
|
|
|
|
// ---- Fetch members (paginated) ----
|
|
while (true) {
|
|
try {
|
|
const membersResponse = await installationOctokit.rest.orgs.listMembers({
|
|
org: orgLogin,
|
|
per_page: 90,
|
|
page,
|
|
})
|
|
|
|
if (!membersResponse.data.length) break
|
|
|
|
memberData.push(
|
|
...membersResponse.data.map(member => ({
|
|
id: member.id,
|
|
login: member.login,
|
|
})),
|
|
)
|
|
|
|
page++
|
|
} catch (error) {
|
|
console.error(`Error fetching members for org ${orgLogin}:`, error)
|
|
Sentry.captureException(error)
|
|
break
|
|
}
|
|
}
|
|
|
|
// ---- Fetch roles for each member ----
|
|
for (const member of memberData) {
|
|
try {
|
|
const role = await fetchOrgRole(installationOctokit, orgLogin, member.login)
|
|
members.push({ id: member.id, username: member.login, role })
|
|
} catch (error) {
|
|
console.error(`Error fetching role for member ${member.login} in org ${orgLogin}:`, error)
|
|
Sentry.captureException(error)
|
|
}
|
|
}
|
|
|
|
return members
|
|
}
|
|
|
|
export async function syncOrgsWithMembers(app: App, orgNames?: string[]) {
|
|
Sentry.captureMessage("syncOrgsWithMembers started")
|
|
const installations = await getInstallations(app)
|
|
const orgInstallations = await filterInstalltionAccountByOrg(installations, orgNames)
|
|
|
|
for (const installation of orgInstallations) {
|
|
try {
|
|
const login = installation.account!.login
|
|
let repos: any[] = []
|
|
let members: Array<{ id: number; username: string; role: string }> = []
|
|
console.log(`fetch repos for ${login}`)
|
|
|
|
const installationOctokit = await app.getInstallationOctokit(installation.id)
|
|
|
|
const accountLogin = installation.account!.login
|
|
const accountType = installation.account!.type
|
|
|
|
// Check if the installation exists, if not, create it
|
|
const installationExists = await getAppInstallationByInstalltionId(installation.id)
|
|
|
|
if (!installationExists) {
|
|
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}`)
|
|
}
|
|
|
|
// --- Fetch all repos with pagination ---
|
|
repos = await getReposForInstallation(installationOctokit)
|
|
|
|
console.log("Done... ")
|
|
console.log(`fetch members for ${login}`)
|
|
|
|
// --- Fetch all members with roles ---
|
|
members = await getMembersWithRolesForOrg(installationOctokit, login)
|
|
|
|
console.log("Done... ")
|
|
|
|
// Get or create organization
|
|
let organization = await organizationRepository.findByGithubOrgId(
|
|
String(installation.account!.id),
|
|
)
|
|
organization ||= await organizationRepository.create({
|
|
github_org_id: String(installation.account!.id),
|
|
name: login,
|
|
added_by: "Codeflash",
|
|
})
|
|
|
|
// Fetch existing members in organization from DB
|
|
const existingMembersInDb = await prisma.organization_members.findMany({
|
|
where: { organization_id: organization.id },
|
|
})
|
|
|
|
// Current member IDs from GitHub
|
|
const currentMemberIds = members.map(m => `github|${m.id}`)
|
|
|
|
// Remove members who no longer exist in the org
|
|
for (const existingMember of existingMembersInDb) {
|
|
if (!currentMemberIds.includes(String(existingMember.user_id))) {
|
|
await organizationMemberRepository.removeMember(
|
|
String(organization.id),
|
|
String(existingMember.user_id),
|
|
)
|
|
await deleteOrganizationMemberApiKeys(
|
|
String(existingMember.user_id),
|
|
String(organization.id),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Process each member: create or update user and add/update organization membership
|
|
for (const member of members) {
|
|
const userId = `github|${member.id}`
|
|
const existingUser = await getUserById(userId)
|
|
if (!existingUser) {
|
|
await createOrUpdateUser(userId, member.username, null, null)
|
|
}
|
|
try {
|
|
// Check if member already exists in organization_members
|
|
const existingMember = await prisma.organization_members.findUnique({
|
|
where: {
|
|
organization_id_user_id: {
|
|
organization_id: organization.id,
|
|
user_id: userId,
|
|
},
|
|
},
|
|
})
|
|
if (!existingMember) {
|
|
await organizationMemberRepository.addMember({
|
|
organizationId: organization.id,
|
|
userId,
|
|
role: member.role,
|
|
})
|
|
} else if (existingMember.role !== member.role) {
|
|
// Update role if changed
|
|
await organizationMemberRepository.updateRole(organization.id, userId, member.role)
|
|
}
|
|
} catch (error) {
|
|
// If error is due to unique constraint, skip
|
|
if (error instanceof Error && error.message.includes("Unique constraint failed")) {
|
|
// skip
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process each repo
|
|
for (const repo of repos) {
|
|
// Upsert repository
|
|
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: organization.id,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
dependencies.console.error(
|
|
`Error fetching data for org ${installation.account!.login}:`,
|
|
error,
|
|
)
|
|
Sentry.captureException(error)
|
|
continue
|
|
}
|
|
}
|
|
|
|
Sentry.captureMessage("syncOrgsWithMembers completed")
|
|
}
|