codeflash-internal/js/cf-api/endpoints/setup-github-actions.ts
Kevin Turcios d7a8b8f227
perf: fix CI build + lazy-load heavy libs + parallelize DB queries (#2601)
## 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
2026-04-13 11:03:05 -05:00

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")
}
}