Compare commits

...

4 Commits

Author SHA1 Message Date
claude[bot]
b0ca8faf6b fix: replace REST API branch listing with GraphQL query for better performance
- Replaced REST API listBranches call with GraphQL query that filters branches by prefix
- This fixes the issue where repositories with >100 branches could miss existing Claude branches
- The GraphQL query directly filters by prefix, making it more efficient and accurate

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-05-31 05:37:04 +00:00
claude[bot]
e8d2b8d5df 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>
2025-05-31 05:27:07 +00:00
Ashwin Bhat
a8a36ced96 fix mistake in FAQ (#100) 2025-05-30 12:33:15 -07:00
Ashwin Bhat
180a1b6680 switch to opus for this repo's claude workflow (#97)
* switch to opus for this repo's claude workflow

* prettier
2025-05-30 08:14:11 -07:00
6 changed files with 143 additions and 52 deletions

View File

@@ -36,3 +36,4 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)" allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck." custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
model: "claude-opus-4-20250514"

2
FAQ.md
View File

@@ -6,7 +6,7 @@ This FAQ addresses common questions and gotchas when using the Claude Code GitHu
### Why doesn't tagging @claude from my automated workflow work? ### Why doesn't tagging @claude from my automated workflow work?
The `github-actions` user (and other GitHub Apps/bots) cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow. The `github-actions` user cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user, or use a separate app token of your own. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
### Why does Claude say I don't have permission to trigger it? ### Why does Claude say I don't have permission to trigger it?

View File

@@ -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 =====");

View File

@@ -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

View File

@@ -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 = "";

View File

@@ -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,127 @@ 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") // Use GraphQL to efficiently search for branches with a specific prefix
.join("_"); const query = `
query($owner: String!, $repo: String!, $prefix: String!) {
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`; repository(owner: $owner, name: $repo) {
refs(refPrefix: "refs/heads/", query: $prefix, first: 100) {
try { nodes {
// Get the SHA of the source branch name
const sourceBranchRef = await octokits.rest.git.getRef({ }
owner, }
repo, }
ref: `heads/${sourceBranch}`, }
}); `;
const currentSHA = sourceBranchRef.data.object.sha; const response = await octokits.graphql<{
repository: {
console.log(`Current SHA: ${currentSHA}`); refs: {
nodes: Array<{ name: string }>;
// Create branch using GitHub API };
await octokits.rest.git.createRef({ };
owner, }>(query, {
repo, owner,
ref: `refs/heads/${newBranch}`, repo,
sha: currentSHA, prefix: `claude/issue-${entityNumber}-`,
}); });
// Checkout the new branch (shallow fetch for performance) const branches = response.repository.refs.nodes;
await $`git fetch origin --depth=1 ${newBranch}`;
await $`git checkout ${newBranch}`; if (branches.length > 0) {
// Use the first matching branch (could be sorted by date in future)
branchToUse = branches[0].name;
isReusedBranch = true;
console.log(`Found existing Claude branch for issue #${entityNumber}: ${branchToUse}`);
}
} catch (error) {
console.error("Error checking for existing branches:", error);
// Continue with new branch creation if check fails
}
}
// If no existing branch found or this is a PR, create a new branch
if (!branchToUse) {
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);
} }
} }