199 lines
5.3 KiB
TypeScript
199 lines
5.3 KiB
TypeScript
import { type App, type Octokit } from "octokit"
|
|
import * as Sentry from "@sentry/node"
|
|
|
|
// 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,
|
|
): 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) {
|
|
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}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
await installationOctokit.rest.issues.createLabel({
|
|
owner,
|
|
repo,
|
|
name: labelName,
|
|
color,
|
|
description,
|
|
})
|
|
} else {
|
|
// An error occurred that wasn't a 404, rethrow it
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
console.log(`Org role for ${username} in ${owner}: ${membership.role}`)
|
|
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)
|
|
}
|
|
}
|