mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
feat: persist issue branches for reuse on subsequent invocations
- Check for existing Claude branches before creating new ones for issues - Skip automatic branch deletion for issue branches to allow reuse - Add context awareness so Claude knows when reusing an existing branch - Set IS_REUSED_BRANCH output for tracking branch reuse status This change allows users to iteratively ask Claude to make changes on the same branch before creating a PR, as requested in issue #103. Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
This commit is contained in:
@@ -361,6 +361,7 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
|||||||
export function generatePrompt(
|
export function generatePrompt(
|
||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
|
isReusedBranch?: boolean,
|
||||||
): string {
|
): string {
|
||||||
const {
|
const {
|
||||||
contextData,
|
contextData,
|
||||||
@@ -534,7 +535,7 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.`
|
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.`
|
||||||
: `
|
: `
|
||||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.${isReusedBranch ? `\n - NOTE: This branch (${eventData.claudeBranch}) was reused from a previous Claude invocation on this issue. It may already contain some work.` : ''}
|
||||||
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.
|
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.
|
||||||
@@ -641,6 +642,7 @@ export async function createPrompt(
|
|||||||
claudeBranch: string | undefined,
|
claudeBranch: string | undefined,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
|
isReusedBranch?: boolean,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const preparedContext = prepareContext(
|
const preparedContext = prepareContext(
|
||||||
@@ -653,7 +655,7 @@ export async function createPrompt(
|
|||||||
await mkdir("/tmp/claude-prompts", { recursive: true });
|
await mkdir("/tmp/claude-prompts", { recursive: true });
|
||||||
|
|
||||||
// Generate the prompt
|
// Generate the prompt
|
||||||
const promptContent = generatePrompt(preparedContext, githubData);
|
const promptContent = generatePrompt(preparedContext, githubData, isReusedBranch);
|
||||||
|
|
||||||
// Log the final prompt to console
|
// Log the final prompt to console
|
||||||
console.log("===== FINAL PROMPT =====");
|
console.log("===== FINAL PROMPT =====");
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ async function run() {
|
|||||||
branchInfo.claudeBranch,
|
branchInfo.claudeBranch,
|
||||||
githubData,
|
githubData,
|
||||||
context,
|
context,
|
||||||
|
branchInfo.isReusedBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 11: Get MCP configuration
|
// Step 11: Get MCP configuration
|
||||||
|
|||||||
@@ -87,13 +87,29 @@ async function run() {
|
|||||||
const currentBody = comment.body ?? "";
|
const currentBody = comment.body ?? "";
|
||||||
|
|
||||||
// Check if we need to add branch link for new branches
|
// Check if we need to add branch link for new branches
|
||||||
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
|
// For issues, we don't delete branches anymore to allow reuse
|
||||||
octokit,
|
const skipBranchDeletion = !context.isPR && claudeBranch;
|
||||||
owner,
|
|
||||||
repo,
|
let shouldDeleteBranch = false;
|
||||||
claudeBranch,
|
let branchLink = "";
|
||||||
baseBranch,
|
|
||||||
);
|
if (skipBranchDeletion) {
|
||||||
|
// For issue branches, just add the branch link without checking for deletion
|
||||||
|
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
|
||||||
|
branchLink = `\n[View branch](${branchUrl})`;
|
||||||
|
console.log(`Keeping issue branch ${claudeBranch} for potential reuse`);
|
||||||
|
} else {
|
||||||
|
// For PR branches, use the existing cleanup logic
|
||||||
|
const result = await checkAndDeleteEmptyBranch(
|
||||||
|
octokit,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
claudeBranch,
|
||||||
|
baseBranch,
|
||||||
|
);
|
||||||
|
shouldDeleteBranch = result.shouldDeleteBranch;
|
||||||
|
branchLink = result.branchLink;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we need to add PR URL when we have a new branch
|
// Check if we need to add PR URL when we have a new branch
|
||||||
let prLink = "";
|
let prLink = "";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type BranchInfo = {
|
|||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
currentBranch: string;
|
currentBranch: string;
|
||||||
|
isReusedBranch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function setupBranch(
|
export async function setupBranch(
|
||||||
@@ -79,57 +80,110 @@ export async function setupBranch(
|
|||||||
|
|
||||||
// Creating a new branch for either an issue or closed/merged PR
|
// Creating a new branch for either an issue or closed/merged PR
|
||||||
const entityType = isPR ? "pr" : "issue";
|
const entityType = isPR ? "pr" : "issue";
|
||||||
console.log(
|
|
||||||
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
// For issues, check if a Claude branch already exists
|
||||||
);
|
let branchToUse: string | null = null;
|
||||||
|
let isReusedBranch = false;
|
||||||
const timestamp = new Date()
|
|
||||||
.toISOString()
|
if (!isPR) {
|
||||||
.replace(/[:-]/g, "")
|
// Check for existing Claude branches for this issue
|
||||||
.replace(/\.\d{3}Z/, "")
|
try {
|
||||||
.split("T")
|
const { data: branches } = await octokits.rest.repos.listBranches({
|
||||||
.join("_");
|
owner,
|
||||||
|
repo,
|
||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
per_page: 100,
|
||||||
|
});
|
||||||
try {
|
|
||||||
// Get the SHA of the source branch
|
// Look for existing branches with pattern claude/issue-{entityNumber}-*
|
||||||
const sourceBranchRef = await octokits.rest.git.getRef({
|
const existingBranch = branches.find(branch =>
|
||||||
owner,
|
branch.name.startsWith(`claude/issue-${entityNumber}-`)
|
||||||
repo,
|
);
|
||||||
ref: `heads/${sourceBranch}`,
|
|
||||||
});
|
if (existingBranch) {
|
||||||
|
branchToUse = existingBranch.name;
|
||||||
const currentSHA = sourceBranchRef.data.object.sha;
|
isReusedBranch = true;
|
||||||
|
console.log(`Found existing Claude branch for issue #${entityNumber}: ${branchToUse}`);
|
||||||
console.log(`Current SHA: ${currentSHA}`);
|
}
|
||||||
|
} catch (error) {
|
||||||
// Create branch using GitHub API
|
console.error("Error checking for existing branches:", error);
|
||||||
await octokits.rest.git.createRef({
|
// Continue with new branch creation if check fails
|
||||||
owner,
|
}
|
||||||
repo,
|
}
|
||||||
ref: `refs/heads/${newBranch}`,
|
|
||||||
sha: currentSHA,
|
// If no existing branch found or this is a PR, create a new branch
|
||||||
});
|
if (!branchToUse) {
|
||||||
|
|
||||||
// Checkout the new branch (shallow fetch for performance)
|
|
||||||
await $`git fetch origin --depth=1 ${newBranch}`;
|
|
||||||
await $`git checkout ${newBranch}`;
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Successfully created and checked out new branch: ${newBranch}`,
|
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[:-]/g, "")
|
||||||
|
.replace(/\.\d{3}Z/, "")
|
||||||
|
.split("T")
|
||||||
|
.join("_");
|
||||||
|
|
||||||
|
branchToUse = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isReusedBranch) {
|
||||||
|
// For existing branches, just checkout
|
||||||
|
console.log(`Checking out existing branch: ${branchToUse}`);
|
||||||
|
|
||||||
|
// Fetch the branch with more depth to allow for context
|
||||||
|
await $`git fetch origin --depth=20 ${branchToUse}`;
|
||||||
|
await $`git checkout ${branchToUse}`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Successfully checked out existing branch: ${branchToUse}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Note: This is a reused branch from a previous Claude invocation on issue #${entityNumber}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Get the SHA of the source branch
|
||||||
|
const sourceBranchRef = await octokits.rest.git.getRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `heads/${sourceBranch}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentSHA = sourceBranchRef.data.object.sha;
|
||||||
|
|
||||||
|
console.log(`Current SHA: ${currentSHA}`);
|
||||||
|
|
||||||
|
// Create branch using GitHub API
|
||||||
|
await octokits.rest.git.createRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `refs/heads/${branchToUse}`,
|
||||||
|
sha: currentSHA,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Checkout the new branch (shallow fetch for performance)
|
||||||
|
await $`git fetch origin --depth=1 ${branchToUse}`;
|
||||||
|
await $`git checkout ${branchToUse}`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Successfully created and checked out new branch: ${branchToUse}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Set outputs for GitHub Actions
|
// Set outputs for GitHub Actions
|
||||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
core.setOutput("CLAUDE_BRANCH", branchToUse);
|
||||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||||
|
if (isReusedBranch) {
|
||||||
|
core.setOutput("IS_REUSED_BRANCH", "true");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
baseBranch: sourceBranch,
|
baseBranch: sourceBranch,
|
||||||
claudeBranch: newBranch,
|
claudeBranch: branchToUse,
|
||||||
currentBranch: newBranch,
|
currentBranch: branchToUse,
|
||||||
|
isReusedBranch,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating branch:", error);
|
console.error("Error setting up branch:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user