#!/usr/bin/env bun /** * Setup the appropriate branch based on the event type: * - For PRs: Checkout the PR branch * - For Issues: Create a new branch */ import { $ } from "bun"; import * as core from "@actions/core"; import type { GitHubContext } from "../context"; import type { GitHubPullRequest } from "../types"; import type { Octokits } from "../api/client"; import type { FetchDataResult } from "../data/fetcher"; export type BranchInfo = { baseBranch: string; claudeBranch?: string; currentBranch: string; }; export async function setupBranch( octokits: Octokits, githubData: FetchDataResult | null, context: GitHubContext, ): Promise { const { owner, repo } = context.repository; const entityNumber = context.entityNumber; const { baseBranch, branchPrefix } = context.inputs; const isPR = context.isPR; if (isPR && githubData) { const prData = githubData.contextData as GitHubPullRequest; const prState = prData.state; // Check if PR is closed or merged if (prState === "CLOSED" || prState === "MERGED") { console.log( `PR #${entityNumber} is ${prState}, creating new branch from source...`, ); // Fall through to create a new branch like we do for issues } else { // Handle open PR: Checkout the PR branch console.log("This is an open PR, checking out PR branch..."); const branchName = prData.headRefName; // Determine optimal fetch depth based on PR commit count, with a minimum of 20 const commitCount = prData.commits.totalCount; const fetchDepth = Math.max(commitCount, 20); console.log( `PR #${entityNumber}: ${commitCount} commits, using fetch depth ${fetchDepth}`, ); // Execute git commands to checkout PR branch (dynamic depth based on PR size) await $`git fetch origin --depth=${fetchDepth} ${branchName}`; await $`git checkout ${branchName} --`; console.log(`Successfully checked out PR branch for PR #${entityNumber}`); // For open PRs, we need to get the base branch of the PR const baseBranch = prData.baseRefName; return { baseBranch, currentBranch: branchName, }; } } // Determine source branch - use baseBranch if provided, otherwise fetch default let sourceBranch: string; if (baseBranch) { // Use provided base branch for source sourceBranch = baseBranch; } else { // No base branch provided, fetch the default branch to use as source const repoResponse = await octokits.rest.repos.get({ owner, repo, }); sourceBranch = repoResponse.data.default_branch; } // Generate branch name for either an issue, closed/merged PR, or repository_dispatch event let branchName: string; if (context.eventName === "repository_dispatch") { // For repository_dispatch events, use run ID for uniqueness since there's no entity number const now = new Date(); const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`; branchName = `${branchPrefix}dispatch-${context.runId}-${timestamp}`; } else { // For issues and PRs, use the existing logic const entityType = isPR ? "pr" : "issue"; const now = new Date(); const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`; branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`; } // Ensure branch name is Kubernetes-compatible: // - Lowercase only // - Alphanumeric with hyphens // - No underscores // - Max 50 chars (to allow for prefixes) const newBranch = branchName.toLowerCase().substring(0, 50); try { // Get the SHA of the source branch to verify it exists const sourceBranchRef = await octokits.rest.git.getRef({ owner, repo, ref: `heads/${sourceBranch}`, }); const currentSHA = sourceBranchRef.data.object.sha; console.log(`Source branch SHA: ${currentSHA}`); // For commit signing, defer branch creation to the file ops server if (context.inputs.useCommitSigning) { console.log( `Branch name generated: ${newBranch} (will be created by file ops server on first commit)`, ); // Ensure we're on the source branch console.log(`Fetching and checking out source branch: ${sourceBranch}`); await $`git fetch origin ${sourceBranch} --depth=1`; await $`git checkout ${sourceBranch}`; // Set outputs for GitHub Actions core.setOutput("CLAUDE_BRANCH", newBranch); core.setOutput("BASE_BRANCH", sourceBranch); return { baseBranch: sourceBranch, claudeBranch: newBranch, currentBranch: sourceBranch, // Stay on source branch for now }; } // For non-signing case, create and checkout the branch locally only const entityType = context.eventName === "repository_dispatch" ? "dispatch" : isPR ? "pr" : "issue"; const entityId = context.eventName === "repository_dispatch" ? context.runId : entityNumber!.toString(); console.log( `Creating local branch ${newBranch} for ${entityType} ${entityId} from source branch: ${sourceBranch}...`, ); // Fetch and checkout the source branch first to ensure we branch from the correct base console.log(`Fetching and checking out source branch: ${sourceBranch}`); await $`git fetch origin ${sourceBranch} --depth=1`; await $`git checkout ${sourceBranch}`; // Create and checkout the new branch from the source branch await $`git checkout -b ${newBranch}`; console.log( `Successfully created and checked out local branch: ${newBranch}`, ); // Set outputs for GitHub Actions core.setOutput("CLAUDE_BRANCH", newBranch); core.setOutput("BASE_BRANCH", sourceBranch); return { baseBranch: sourceBranch, claudeBranch: newBranch, currentBranch: newBranch, }; } catch (error) { console.error("Error in branch setup:", error); process.exit(1); } }