Files
claude-code-action/src/github/operations/branch.ts
Ashwin Bhat 52c2f5881b feat: add repository_dispatch event support
- Add new progress MCP server for reporting task status via API
- Support repository_dispatch events with task description and progress endpoint
- Introduce isDispatch flag to unify dispatch event handling
- Make GitHub data optional for dispatch events without issues/PRs
- Update prompt generation with dispatch-specific instructions

Enables triggering Claude via repository_dispatch with:
{
  "event_type": "claude_task",
  "client_payload": {
    "description": "Task description",
    "progress_endpoint": "https://api.example.com/progress"
  }
}
2025-08-05 10:56:07 -07:00

182 lines
6.2 KiB
TypeScript

#!/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<BranchInfo> {
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);
}
}