mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
Reverts the following commits from main: -d7a8b8f2perf: fix CI build + lazy-load heavy libs + parallelize DB queries (#2601) -48b5e2b4fix: make tree-sitter WASM build failure non-fatal when cache exists (#2602) -c372b6bcMerge pull request #2603 from codeflash-ai/fix/deploy-build-common -b656bb1dfix: cf-api deploy broken by pnpm workspace migration -c1b0076cfix: align TypeScript versions to deduplicate @prisma/client in pnpm -09ed4d4bfix: use redirect instead of throw for auth failures during prerender -71127055fix: redirect remaining auth throws that crash prerendering PR #2601 introduced 18 bugs including 5 authorization bypass vulnerabilities: - Cross-org data access via forged currentOrganizationId cookie - Cross-repo/cross-org member role escalation and deletion (unscoped lookups) - Missing replayTests/concolicTests in approval flow - repository_id filter silently broken for personal accounts - Tests mocking wrong Prisma method ($queryRawUnsafe vs $queryRaw) The subsequent PRs (#2602, #2603, and follow-up commits) were dependent fixes for issues caused by #2601 and are reverted together. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
457 lines
14 KiB
TypeScript
457 lines
14 KiB
TypeScript
import * as Sentry from "@sentry/node"
|
|
import { Request, Response } from "express"
|
|
import { type Octokit } from "octokit"
|
|
import { userNickname } from "../auth0-mgmt.js"
|
|
import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
|
|
import { githubApp } from "../github/github-app.js"
|
|
import { posthog } from "../analytics.js"
|
|
import { AuthorizedUserReq } from "types.js"
|
|
import { registerRepositoryAndMember } from "./utils/github-repo-setup.js"
|
|
import { createNewPullRequest } from "../github/create-pr-from-diffcontents.js"
|
|
import {
|
|
missingRequiredFields,
|
|
validationFailure,
|
|
unauthorized,
|
|
githubInstallationNotFound,
|
|
githubInstallationError,
|
|
githubNotCollaborator,
|
|
internalServerError,
|
|
} from "../exceptions/index.js"
|
|
|
|
/**
|
|
* Required GitHub App Permissions:
|
|
* - contents: write (for creating branches and files)
|
|
* - workflows: write (for creating/updating workflow files in .github/workflows/)
|
|
* - pull_requests: write (for creating pull requests)
|
|
*
|
|
* The GitHub App must be installed on the target repository.
|
|
*/
|
|
|
|
// Dependencies interface for easier testing
|
|
export interface SetupGithubActionsDependencies {
|
|
userNickname: typeof userNickname
|
|
getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
|
|
isUserCollaborator: typeof isUserCollaborator
|
|
githubApp: typeof githubApp
|
|
registerRepositoryAndMember: typeof registerRepositoryAndMember
|
|
posthog: typeof posthog
|
|
}
|
|
|
|
// Default dependencies
|
|
let dependencies: SetupGithubActionsDependencies = {
|
|
userNickname,
|
|
getInstallationOctokitByOwner,
|
|
isUserCollaborator,
|
|
githubApp,
|
|
registerRepositoryAndMember,
|
|
posthog,
|
|
}
|
|
|
|
// For testing - allow dependency injection
|
|
export function setSetupGithubActionsDependencies(deps: Partial<SetupGithubActionsDependencies>) {
|
|
dependencies = { ...dependencies, ...deps }
|
|
}
|
|
|
|
export function resetSetupGithubActionsDependencies() {
|
|
dependencies = {
|
|
userNickname,
|
|
getInstallationOctokitByOwner,
|
|
isUserCollaborator,
|
|
githubApp,
|
|
registerRepositoryAndMember,
|
|
posthog,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if workflow file already exists on base branch
|
|
* Returns the file content if it exists, null otherwise
|
|
*/
|
|
async function checkWorkflowFileExists(
|
|
installationOctokit: Octokit,
|
|
owner: string,
|
|
repo: string,
|
|
baseBranch: string,
|
|
): Promise<{ content: string; sha: string } | null> {
|
|
try {
|
|
const response = await installationOctokit.rest.repos.getContent({
|
|
owner,
|
|
repo,
|
|
path: ".github/workflows/codeflash.yaml",
|
|
ref: baseBranch,
|
|
})
|
|
|
|
if ("content" in response.data && response.data.encoding === "base64") {
|
|
const content = Buffer.from(response.data.content, "base64").toString("utf-8")
|
|
return { content, sha: response.data.sha }
|
|
}
|
|
return null
|
|
} catch (error: any) {
|
|
if (error.status === 404) {
|
|
return null // File doesn't exist
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate unique branch name for the PR
|
|
*/
|
|
function generateBranchName(): string {
|
|
const timestamp = Date.now()
|
|
return `codeflash/setup-github-actions-${timestamp}`
|
|
}
|
|
|
|
/**
|
|
* Create PR with workflow file using Contents API
|
|
*/
|
|
async function createPrWithWorkflowFile(
|
|
installationOctokit: Octokit,
|
|
owner: string,
|
|
repo: string,
|
|
baseBranch: string,
|
|
workflowContent: string,
|
|
): Promise<
|
|
| { pr_url: string; pr_number: number; already_exists?: false }
|
|
| { pr_url: null; pr_number: null; already_exists: true }
|
|
> {
|
|
const filePath = ".github/workflows/codeflash.yaml"
|
|
const branchName = generateBranchName()
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Creating PR for ${owner}/${repo} on branch ${baseBranch}, branchName=${branchName}, filePath=${filePath}`,
|
|
)
|
|
|
|
// Step 1: Get base branch SHA, with fallback to default branch if base branch doesn't exist
|
|
let resolvedBaseBranch = baseBranch
|
|
let baseRef
|
|
try {
|
|
baseRef = await installationOctokit.rest.git.getRef({
|
|
owner,
|
|
repo,
|
|
ref: `heads/${baseBranch}`,
|
|
})
|
|
} catch (error: any) {
|
|
if (error.status === 404) {
|
|
// Base branch doesn't exist on GitHub (likely a local-only branch)
|
|
// Fall back to the repository's default branch
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Base branch ${baseBranch} not found on GitHub, fetching default branch for ${owner}/${repo}`,
|
|
)
|
|
|
|
const repoInfo = await installationOctokit.rest.repos.get({
|
|
owner,
|
|
repo,
|
|
})
|
|
resolvedBaseBranch = repoInfo.data.default_branch
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Using default branch ${resolvedBaseBranch} for ${owner}/${repo}`,
|
|
)
|
|
|
|
// Get the default branch reference
|
|
baseRef = await installationOctokit.rest.git.getRef({
|
|
owner,
|
|
repo,
|
|
ref: `heads/${resolvedBaseBranch}`,
|
|
})
|
|
} else {
|
|
// Re-throw other errors
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const baseSha = baseRef.data.object.sha
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Base branch SHA: ${baseSha} for ${owner}/${repo} (using branch: ${resolvedBaseBranch})`,
|
|
)
|
|
|
|
// Step 2: Check if workflow file already exists (idempotency)
|
|
const existingFile = await checkWorkflowFileExists(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
resolvedBaseBranch,
|
|
)
|
|
|
|
if (existingFile) {
|
|
// Compare content
|
|
if (existingFile.content === workflowContent) {
|
|
console.log(
|
|
`[setup-github-actions] Workflow file already exists with same content, skipping PR creation`,
|
|
)
|
|
// File exists with same content - return early
|
|
// This is a valid case - the workflow is already set up
|
|
return {
|
|
pr_url: null,
|
|
pr_number: null,
|
|
already_exists: true,
|
|
}
|
|
} else {
|
|
console.log(
|
|
`[setup-github-actions] Workflow file exists but content differs, creating PR with update`,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Step 3: Create branch
|
|
await installationOctokit.rest.git.createRef({
|
|
owner,
|
|
repo,
|
|
ref: `refs/heads/${branchName}`,
|
|
sha: baseSha,
|
|
})
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Created branch ${branchName} for ${owner}/${repo}`,
|
|
)
|
|
|
|
// Step 4: Create workflow file using Contents API
|
|
// This requires 'workflows: write' permission, which we now have
|
|
const commitMessage = "Add CodeFlash GitHub Actions workflow"
|
|
|
|
try {
|
|
await installationOctokit.rest.repos.createOrUpdateFileContents({
|
|
owner,
|
|
repo,
|
|
path: filePath,
|
|
message: commitMessage,
|
|
content: Buffer.from(workflowContent, "utf-8").toString("base64"),
|
|
branch: branchName,
|
|
})
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Created workflow file on branch ${branchName} for ${owner}/${repo}`,
|
|
)
|
|
} catch (error: any) {
|
|
// Clean up the branch we created if file creation fails
|
|
try {
|
|
await installationOctokit.rest.git.deleteRef({
|
|
owner,
|
|
repo,
|
|
ref: `heads/${branchName}`,
|
|
})
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Cleaned up branch ${branchName} after error for ${owner}/${repo}`,
|
|
)
|
|
} catch (cleanupError: any) {
|
|
console.error(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Failed to cleanup branch ${branchName} for ${owner}/${repo}:`,
|
|
cleanupError,
|
|
)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
// Step 5: Create PR using existing helper
|
|
const prTitle = "Add CodeFlash GitHub Actions workflow"
|
|
const prBody = `This PR adds the CodeFlash GitHub Actions workflow to automatically find optimizations for new code in future pull requests.
|
|
|
|
The workflow will:
|
|
|
|
- Run on every pull request when Python code is modified
|
|
- Automatically attempt to optimize Python code
|
|
- Create optimization suggestions when improvements are found
|
|
|
|
**Setup Required:**
|
|
|
|
To use this workflow, you'll need to add the \`CODEFLASH_API_KEY\` secret to your repository:
|
|
|
|
1. Get your API key from [codeflash.ai](https://app.codeflash.ai/apikeys)
|
|
2. Go to your repository settings: **Settings → Secrets and variables → Actions**
|
|
3. Click **New repository secret**
|
|
4. Name: \`CODEFLASH_API_KEY\`
|
|
5. Value: Your CodeFlash API key
|
|
6. Click **Add secret**
|
|
|
|
The workflow will use this key to authenticate with CodeFlash's optimization service.`
|
|
|
|
const prResponse = await createNewPullRequest(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
prTitle,
|
|
prBody,
|
|
branchName,
|
|
resolvedBaseBranch,
|
|
)
|
|
|
|
const prUrl = prResponse.data.html_url
|
|
const prNumber = prResponse.data.number
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:createPrWithWorkflowFile] Created PR #${prNumber}: ${prUrl} for ${owner}/${repo}`,
|
|
)
|
|
|
|
return { pr_url: prUrl, pr_number: prNumber }
|
|
}
|
|
|
|
/**
|
|
* Main endpoint handler for setting up GitHub Actions
|
|
*/
|
|
export async function setupGithubActions(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { owner, repo, baseBranch, workflowContent } = req.body
|
|
|
|
// Validate required fields
|
|
if (!owner || !repo || !baseBranch || !workflowContent) {
|
|
throw missingRequiredFields("owner, repo, baseBranch, workflowContent")
|
|
}
|
|
|
|
// Validate workflowContent is a string
|
|
if (typeof workflowContent !== "string") {
|
|
throw validationFailure("workflowContent must be a string")
|
|
}
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:setupGithubActions] Received request with parameters: owner=${owner}, repo=${repo}, baseBranch=${baseBranch}, workflowContentLength=${workflowContent.length}`,
|
|
)
|
|
|
|
const userId = (req as any).userId
|
|
|
|
// Get user nickname for authentication
|
|
const nickname: string | null = await dependencies.userNickname(userId)
|
|
if (nickname == null) {
|
|
throw unauthorized("")
|
|
}
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:setupGithubActions] Request from user ${nickname} for ${owner}/${repo}`,
|
|
)
|
|
|
|
// Get installation Octokit
|
|
const installationOctokit = await dependencies.getInstallationOctokitByOwner(
|
|
dependencies.githubApp,
|
|
owner,
|
|
repo,
|
|
userId,
|
|
)
|
|
|
|
if (installationOctokit instanceof Error) {
|
|
console.error(
|
|
`[setup-github-actions.ts:setupGithubActions] Error getting installation Octokit for ${owner}/${repo}: ${installationOctokit.message}`,
|
|
)
|
|
|
|
// Check if it's a "not installed" error (404)
|
|
if (installationOctokit.message.includes("not installed")) {
|
|
throw githubInstallationNotFound(`${owner}/${repo}`)
|
|
}
|
|
|
|
// Other installation errors
|
|
throw githubInstallationError(installationOctokit.message)
|
|
}
|
|
|
|
// Verify user is collaborator
|
|
const isCollaborator = await dependencies.isUserCollaborator(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
nickname,
|
|
)
|
|
|
|
if (!isCollaborator) {
|
|
console.log(
|
|
`[setup-github-actions.ts:setupGithubActions] ${nickname} is not a collaborator on ${owner}/${repo}`,
|
|
)
|
|
throw githubNotCollaborator(`${owner}/${repo}`)
|
|
}
|
|
|
|
console.log(
|
|
`[setup-github-actions.ts:setupGithubActions] Verified ${nickname} is a collaborator on ${owner}/${repo}`,
|
|
)
|
|
|
|
// Register repository and member in background
|
|
dependencies
|
|
.registerRepositoryAndMember(owner, repo, nickname, userId, installationOctokit)
|
|
.then(() =>
|
|
console.log(
|
|
`[setup-github-actions.ts:setupGithubActions] Background repo and member upsert completed for ${owner}/${repo}`,
|
|
),
|
|
)
|
|
.catch(err => {
|
|
console.error(
|
|
`[setup-github-actions.ts:setupGithubActions] Error in background upsert for ${owner}/${repo}:`,
|
|
err,
|
|
)
|
|
Sentry.captureException(err)
|
|
})
|
|
|
|
// Create PR with workflow file
|
|
const result = await createPrWithWorkflowFile(
|
|
installationOctokit,
|
|
owner,
|
|
repo,
|
|
baseBranch,
|
|
workflowContent,
|
|
)
|
|
|
|
// Handle case where file already exists with same content
|
|
if (result.already_exists) {
|
|
res.status(200).json({
|
|
success: true,
|
|
pr_url: null,
|
|
pr_number: null,
|
|
message: "Workflow file already exists with the same content. No changes needed.",
|
|
already_exists: true,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Track success
|
|
dependencies.posthog?.capture({
|
|
distinctId: `github|${userId}`,
|
|
event: "cfapi-setup-github-actions-success",
|
|
properties: {
|
|
owner,
|
|
repo,
|
|
pr_number: result.pr_number,
|
|
},
|
|
})
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
pr_url: result.pr_url,
|
|
pr_number: result.pr_number,
|
|
message: "GitHub Actions workflow PR created successfully",
|
|
})
|
|
} catch (error: any) {
|
|
// Re-throw AppExceptions to be handled by GlobalExceptionHandler
|
|
if (error && typeof error === "object" && "getHttpStatus" in error) {
|
|
throw error
|
|
}
|
|
|
|
const { owner, repo, baseBranch } = req.body || {}
|
|
console.error(
|
|
`[setup-github-actions.ts:setupGithubActions] Error processing request for ${owner}/${repo} on branch ${baseBranch}:`,
|
|
error,
|
|
)
|
|
Sentry.captureException(error)
|
|
|
|
const userId = (req as any).userId
|
|
dependencies.posthog?.capture({
|
|
distinctId: `github|${userId}`,
|
|
event: "cfapi-setup-github-actions-error",
|
|
properties: {
|
|
error: error.message || "Unknown error",
|
|
},
|
|
})
|
|
|
|
// Check for specific error types and provide helpful messages
|
|
if (error.status === 404 && error.message?.includes("not installed")) {
|
|
throw githubInstallationNotFound(`${owner}/${repo}`)
|
|
}
|
|
|
|
if (error.status === 403 || error.message?.includes("not a collaborator")) {
|
|
throw githubNotCollaborator(`${owner}/${repo}`)
|
|
}
|
|
|
|
if (error.status === 401 || error.message?.includes("Unauthorized")) {
|
|
throw unauthorized(error.message || "Authentication failed")
|
|
}
|
|
|
|
// For other errors, throw internal server error
|
|
throw internalServerError(error.message || "Failed to setup GitHub Actions")
|
|
}
|
|
}
|