Compare commits

..

10 Commits

Author SHA1 Message Date
Ashwin Bhat
32a6163092 tmp 2025-07-15 15:51:39 -07:00
Ashwin Bhat
a9d9ad3612 feat: add settings input support (#276)
- Add settings input to action.yml that accepts JSON string or file path
- Pass settings parameter to claude-code-base-action
- Update README with comprehensive settings documentation
- Add link to official Claude Code settings documentation
- Document precedence rules for model and tool permissions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-15 14:00:26 -07:00
GitHub Actions
4824494f4d chore: update claude-code-base-action to v0.0.35 2025-07-15 18:54:33 +00:00
Ashwin Bhat
c09fc691c5 docs: add custom GitHub App setup instructions (#267)
Add comprehensive section explaining how to create and use a custom GitHub App
instead of the official Claude app. This is particularly useful for users with
restrictive organization policies or those using AWS Bedrock/Google Vertex AI.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-14 17:17:56 -07:00
GitHub Actions
b3c6de94ea chore: update claude-code-base-action to v0.0.34 2025-07-14 15:59:55 +00:00
Jay Derinbogaz
b92e56a96b refactor: update branch naming convention for Kubernetes compatibility (#249)
* refactor: update branch naming convention for Kubernetes compatibility

- Changed timestamp format in branch names to a shorter, Kubernetes-compatible style (lowercase, hyphens only).
- Updated related tests to reflect new branch name format.
- Ensured branch names are limited to a maximum of 50 characters to comply with Kubernetes naming requirements.

* refactor: clean up timestamp formatting in branch naming logic

- Removed unnecessary whitespace and standardized string formatting for the Kubernetes-compatible timestamp in branch names.
- Ensured consistency in the use of double quotes for string literals.
2025-07-12 11:30:49 -07:00
David Wells
b6868bfc27 Expose the created branch for downstream usage (#237)
* Expose the created branch for downstream usage

* run bun format
2025-07-11 10:15:41 -07:00
Allen Li
0f9a2c4dc3 fix: add GITHUB_API_URL to all Octokit client instantiations (#243)
Not all Octokit client instantiations were respecting GITHUB_API_URL, so
these tools would fail on enterprise.
2025-07-11 07:46:23 -07:00
Ashwin Bhat
cefe963a6b feat: defer remote branch creation until first commit (#244)
* feat: defer remote branch creation until first commit

- For commit signing: branches are created remotely by github-file-ops-server on first commit
- For non-signing: branches are created locally with 'git checkout -b' and pushed when needed
- Consolidated duplicate branch creation logic in github-file-ops-server into a shared helper function
- Claude is unaware of these implementation details and simply sees it's on the correct branch
- No branch links are shown in initial comments since branches don't exist remotely yet

* fix: prevent broken branch links in final comment update

- Check if branch exists remotely before adding branch link
- Only add branch links for branches that actually exist on GitHub
- Add test coverage for non-existent remote branches
- Fixes issue where users would see broken branch links for local-only branches

* fix: don't show branch name in comment header when branch doesn't exist remotely

- Only pass branchName to updateCommentBody when branchLink exists
- Prevents showing branch names for branches that only exist locally
- Add test to verify branch name is not shown when branch doesn't exist

* tmp
2025-07-10 12:57:15 -07:00
GitHub Actions
eda5af4e69 chore: update claude-code-base-action to v0.0.33 2025-07-10 17:05:41 +00:00
14 changed files with 463 additions and 141 deletions

140
README.md
View File

@@ -35,6 +35,86 @@ This command will guide you through setting up the GitHub app and required secre
- Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally) - Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally)
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/` 3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
### Using a Custom GitHub App
If you prefer not to install the official Claude app, you can create your own GitHub App to use with this action. This gives you complete control over permissions and access.
**When you may want to use a custom GitHub App:**
- You need more restrictive permissions than the official app
- Organization policies prevent installing third-party apps
- You're using AWS Bedrock or Google Vertex AI
**Steps to create and use a custom GitHub App:**
1. **Create a new GitHub App:**
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
- Click "New GitHub App"
- Configure the app with these minimum permissions:
- **Repository permissions:**
- Contents: Read & Write
- Issues: Read & Write
- Pull requests: Read & Write
- **Account permissions:** None required
- Set "Where can this GitHub App be installed?" to your preference
- Create the app
2. **Generate and download a private key:**
- After creating the app, scroll down to "Private keys"
- Click "Generate a private key"
- Download the `.pem` file (keep this secure!)
3. **Install the app on your repository:**
- Go to the app's settings page
- Click "Install App"
- Select the repositories where you want to use Claude
4. **Add the app credentials to your repository secrets:**
- Go to your repository's Settings → Secrets and variables → Actions
- Add these secrets:
- `APP_ID`: Your GitHub App's ID (found in the app settings)
- `APP_PRIVATE_KEY`: The contents of the downloaded `.pem` file
5. **Update your workflow to use the custom app:**
```yaml
name: Claude with Custom App
on:
issue_comment:
types: [created]
# ... other triggers
jobs:
claude-response:
runs-on: ubuntu-latest
steps:
# Generate a token from your custom app
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Use Claude with your custom app's token
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }}
# ... other configuration
```
**Important notes:**
- The custom app must have read/write permissions for Issues, Pull Requests, and Contents
- Your app's token will have the exact permissions you configured, nothing more
For more information on creating GitHub Apps, see the [GitHub documentation](https://docs.github.com/en/apps/creating-github-apps).
## 📚 FAQ ## 📚 FAQ
Having issues or questions? Check out our [Frequently Asked Questions](./FAQ.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations. Having issues or questions? Check out our [Frequently Asked Questions](./FAQ.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations.
@@ -109,6 +189,7 @@ jobs:
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | | `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | | `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" | | `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | | `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex) \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
@@ -491,6 +572,65 @@ Use a specific Claude model:
# ... other inputs # ... other inputs
``` ```
### Claude Code Settings
You can provide Claude Code settings to customize behavior such as model selection, environment variables, permissions, and hooks. Settings can be provided either as a JSON string or a path to a settings file.
#### Option 1: Settings File
```yaml
- uses: anthropics/claude-code-action@beta
with:
settings: "path/to/settings.json"
# ... other inputs
```
#### Option 2: Inline Settings
```yaml
- uses: anthropics/claude-code-action@beta
with:
settings: |
{
"model": "claude-opus-4-20250514",
"env": {
"DEBUG": "true",
"API_URL": "https://api.example.com"
},
"permissions": {
"allow": ["Bash", "Read"],
"deny": ["WebFetch"]
},
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "echo Running bash command..."
}]
}]
}
}
# ... other inputs
```
The settings support all Claude Code settings options including:
- `model`: Override the default model
- `env`: Environment variables for the session
- `permissions`: Tool usage permissions
- `hooks`: Pre/post tool execution hooks
- And more...
For a complete list of available settings and their descriptions, see the [Claude Code settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings).
**Notes**:
- The `enableAllProjectMcpServers` setting is always set to `true` by this action to ensure MCP servers work correctly.
- If both the `model` input parameter and a `model` in settings are provided, the `model` input parameter takes precedence.
- The `allowed_tools` and `disallowed_tools` input parameters take precedence over `permissions` in settings.
- In a future version, we may deprecate individual input parameters in favor of using the settings file for all configuration.
## Cloud Providers ## Cloud Providers
You can authenticate with Claude using any of these three methods: You can authenticate with Claude using any of these three methods:

View File

@@ -8,7 +8,7 @@ Thank you for trying out the beta of our GitHub Action! This document outlines o
- **Cross-repo support** - Enable Claude to work across multiple repositories in a single session - **Cross-repo support** - Enable Claude to work across multiple repositories in a single session
- **Ability to modify workflow files** - Let Claude update GitHub Actions workflows and other CI configuration files - **Ability to modify workflow files** - Let Claude update GitHub Actions workflows and other CI configuration files
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services - **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
- ~**Ability to disable commit signing**~ - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added. - **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback - **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
- **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude - **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data - **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data

View File

@@ -60,6 +60,10 @@ inputs:
description: "Custom environment variables to pass to Claude Code execution (YAML format)" description: "Custom environment variables to pass to Claude Code execution (YAML format)"
required: false required: false
default: "" default: ""
settings:
description: "Claude Code settings as JSON string or path to settings JSON file"
required: false
default: ""
# Auth configuration # Auth configuration
anthropic_api_key: anthropic_api_key:
@@ -101,6 +105,9 @@ outputs:
execution_file: execution_file:
description: "Path to the Claude Code execution output file" description: "Path to the Claude Code execution output file"
value: ${{ steps.claude-code.outputs.execution_file }} value: ${{ steps.claude-code.outputs.execution_file }}
branch_name:
description: "The branch created by Claude Code for this execution"
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
runs: runs:
using: "composite" using: "composite"
@@ -142,7 +149,7 @@ runs:
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true' if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@3560d21b41bd19b1d3ac6c9000af378903d8df0e # v0.0.32 uses: anthropics/claude-code-base-action@503cc7080e62d63d2cc1d80035ed04617d5efb47 # v0.0.35
with: with:
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }} allowed_tools: ${{ env.ALLOWED_TOOLS }}
@@ -157,6 +164,7 @@ runs:
anthropic_api_key: ${{ inputs.anthropic_api_key }} anthropic_api_key: ${{ inputs.anthropic_api_key }}
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
claude_env: ${{ inputs.claude_env }} claude_env: ${{ inputs.claude_env }}
settings: ${{ inputs.settings }}
env: env:
# Model configuration # Model configuration
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }} ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}

View File

@@ -12,7 +12,6 @@ import { checkHumanActor } from "../github/validation/actor";
import { checkWritePermissions } from "../github/validation/permissions"; import { checkWritePermissions } from "../github/validation/permissions";
import { createInitialComment } from "../github/operations/comments/create-initial"; import { createInitialComment } from "../github/operations/comments/create-initial";
import { setupBranch } from "../github/operations/branch"; import { setupBranch } from "../github/operations/branch";
import { updateTrackingComment } from "../github/operations/comments/update-with-branch";
import { configureGitAuth } from "../github/operations/git-config"; import { configureGitAuth } from "../github/operations/git-config";
import { prepareMcpConfig } from "../mcp/install-mcp-server"; import { prepareMcpConfig } from "../mcp/install-mcp-server";
import { createPrompt } from "../create-prompt"; import { createPrompt } from "../create-prompt";
@@ -67,17 +66,7 @@ async function run() {
// Step 8: Setup branch // Step 8: Setup branch
const branchInfo = await setupBranch(octokit, githubData, context); const branchInfo = await setupBranch(octokit, githubData, context);
// Step 9: Update initial comment with branch link (only for issues that created a new branch) // Step 9: Configure git authentication if not using commit signing
if (branchInfo.claudeBranch) {
await updateTrackingComment(
octokit,
context,
commentId,
branchInfo.claudeBranch,
);
}
// Step 10: Configure git authentication if not using commit signing
if (!context.inputs.useCommitSigning) { if (!context.inputs.useCommitSigning) {
try { try {
await configureGitAuth(githubToken, context, commentData.user); await configureGitAuth(githubToken, context, commentData.user);
@@ -87,7 +76,7 @@ async function run() {
} }
} }
// Step 11: Create prompt file // Step 10: Create prompt file
await createPrompt( await createPrompt(
commentId, commentId,
branchInfo.baseBranch, branchInfo.baseBranch,
@@ -96,7 +85,7 @@ async function run() {
context, context,
); );
// Step 12: Get MCP configuration // Step 11: Get MCP configuration
const additionalMcpConfig = process.env.MCP_CONFIG || ""; const additionalMcpConfig = process.env.MCP_CONFIG || "";
const mcpConfig = await prepareMcpConfig({ const mcpConfig = await prepareMcpConfig({
githubToken, githubToken,

View File

@@ -201,7 +201,7 @@ async function run() {
jobUrl, jobUrl,
branchLink, branchLink,
prLink, prLink,
branchName: shouldDeleteBranch ? undefined : claudeBranch, branchName: shouldDeleteBranch || !branchLink ? undefined : claudeBranch,
triggerUsername, triggerUsername,
errorDetails, errorDetails,
}; };

View File

@@ -14,6 +14,31 @@ export async function checkAndCommitOrDeleteBranch(
let shouldDeleteBranch = false; let shouldDeleteBranch = false;
if (claudeBranch) { if (claudeBranch) {
// First check if the branch exists remotely
let branchExistsRemotely = false;
try {
await octokit.rest.repos.getBranch({
owner,
repo,
branch: claudeBranch,
});
branchExistsRemotely = true;
} catch (error: any) {
if (error.status === 404) {
console.log(`Branch ${claudeBranch} does not exist remotely`);
} else {
console.error("Error checking if branch exists:", error);
}
}
// Only proceed if branch exists remotely
if (!branchExistsRemotely) {
console.log(
`Branch ${claudeBranch} does not exist remotely, no branch link will be added`,
);
return { shouldDeleteBranch: false, branchLink: "" };
}
// Check if Claude made any commits to the branch // Check if Claude made any commits to the branch
try { try {
const { data: comparison } = const { data: comparison } =
@@ -81,8 +106,8 @@ export async function checkAndCommitOrDeleteBranch(
branchLink = `\n[View branch](${branchUrl})`; branchLink = `\n[View branch](${branchUrl})`;
} }
} catch (error) { } catch (error) {
console.error("Error checking for commits on Claude branch:", error); console.error("Error comparing commits on Claude branch:", error);
// If we can't check, assume the branch has commits to be safe // If we can't compare but the branch exists remotely, include the branch link
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`; branchLink = `\n[View branch](${branchUrl})`;
} }

View File

@@ -84,23 +84,23 @@ export async function setupBranch(
sourceBranch = repoResponse.data.default_branch; sourceBranch = repoResponse.data.default_branch;
} }
// Creating a new branch for either an issue or closed/merged PR // Generate branch name 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}...`,
);
const timestamp = new Date() // Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format
.toISOString() const now = new Date();
.replace(/[:-]/g, "") 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")}`;
.replace(/\.\d{3}Z/, "")
.split("T")
.join("_");
const newBranch = `${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 branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
const newBranch = branchName.toLowerCase().substring(0, 50);
try { try {
// Get the SHA of the source branch // Get the SHA of the source branch to verify it exists
const sourceBranchRef = await octokits.rest.git.getRef({ const sourceBranchRef = await octokits.rest.git.getRef({
owner, owner,
repo, repo,
@@ -108,23 +108,34 @@ export async function setupBranch(
}); });
const currentSHA = sourceBranchRef.data.object.sha; const currentSHA = sourceBranchRef.data.object.sha;
console.log(`Source branch SHA: ${currentSHA}`);
console.log(`Current 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)`,
);
// Create branch using GitHub API // Set outputs for GitHub Actions
await octokits.rest.git.createRef({ core.setOutput("CLAUDE_BRANCH", newBranch);
owner, core.setOutput("BASE_BRANCH", sourceBranch);
repo, return {
ref: `refs/heads/${newBranch}`, baseBranch: sourceBranch,
sha: currentSHA, claudeBranch: newBranch,
}); currentBranch: sourceBranch, // Stay on source branch for now
};
}
// Checkout the new branch (shallow fetch for performance) // For non-signing case, create and checkout the branch locally only
await $`git fetch origin --depth=1 ${newBranch}`; console.log(
await $`git checkout ${newBranch}`; `Creating local branch ${newBranch} for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
);
// Create and checkout the new branch locally
await $`git checkout -b ${newBranch}`;
console.log( console.log(
`Successfully created and checked out new branch: ${newBranch}`, `Successfully created and checked out local branch: ${newBranch}`,
); );
// Set outputs for GitHub Actions // Set outputs for GitHub Actions
@@ -136,7 +147,7 @@ export async function setupBranch(
currentBranch: newBranch, currentBranch: newBranch,
}; };
} catch (error) { } catch (error) {
console.error("Error creating branch:", error); console.error("Error in branch setup:", error);
process.exit(1); process.exit(1);
} }
} }

View File

@@ -3,6 +3,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; import { z } from "zod";
import { GITHUB_API_URL } from "../github/api/config";
import { mkdir, writeFile } from "fs/promises"; import { mkdir, writeFile } from "fs/promises";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
@@ -54,6 +55,7 @@ server.tool(
try { try {
const client = new Octokit({ const client = new Octokit({
auth: GITHUB_TOKEN, auth: GITHUB_TOKEN,
baseUrl: GITHUB_API_URL,
}); });
// Get the PR to find the head SHA // Get the PR to find the head SHA
@@ -142,6 +144,7 @@ server.tool(
try { try {
const client = new Octokit({ const client = new Octokit({
auth: GITHUB_TOKEN, auth: GITHUB_TOKEN,
baseUrl: GITHUB_API_URL,
}); });
// Get jobs for this workflow run // Get jobs for this workflow run
@@ -209,6 +212,7 @@ server.tool(
try { try {
const client = new Octokit({ const client = new Octokit({
auth: GITHUB_TOKEN, auth: GITHUB_TOKEN,
baseUrl: GITHUB_API_URL,
}); });
const response = await client.actions.downloadJobLogsForWorkflowRun({ const response = await client.actions.downloadJobLogsForWorkflowRun({

View File

@@ -8,6 +8,7 @@ import { join } from "path";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { GITHUB_API_URL } from "../github/api/config"; import { GITHUB_API_URL } from "../github/api/config";
import { retryWithBackoff } from "../utils/retry"; import { retryWithBackoff } from "../utils/retry";
import { Octokit } from "@octokit/rest";
type GitHubRef = { type GitHubRef = {
object: { object: {
@@ -52,6 +53,112 @@ const server = new McpServer({
version: "0.0.1", version: "0.0.1",
}); });
// Helper function to get or create branch reference
async function getOrCreateBranchRef(
owner: string,
repo: string,
branch: string,
githubToken: string,
): Promise<string> {
// Create Octokit instance
const octokit = new Octokit({
auth: githubToken,
baseUrl: GITHUB_API_URL,
});
// Try to get the branch reference
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
const refResponse = await fetch(refUrl, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (refResponse.ok) {
const refData = (await refResponse.json()) as GitHubRef;
return refData.object.sha;
}
if (refResponse.status !== 404) {
throw new Error(`Failed to get branch reference: ${refResponse.status}`);
}
// Get base branch from environment or determine it
const baseBranch = process.env.BASE_BRANCH || "main";
// Get the SHA of the base branch
const baseRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${baseBranch}`;
const baseRefResponse = await fetch(baseRefUrl, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
let baseSha: string;
if (!baseRefResponse.ok) {
// If base branch doesn't exist, try default branch
const repoUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}`;
const repoResponse = await fetch(repoUrl, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!repoResponse.ok) {
throw new Error(`Failed to get repository info: ${repoResponse.status}`);
}
const repoData = (await repoResponse.json()) as {
default_branch: string;
};
const defaultBranch = repoData.default_branch;
// Try default branch
const defaultRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${defaultBranch}`;
const defaultRefResponse = await fetch(defaultRefUrl, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!defaultRefResponse.ok) {
throw new Error(
`Failed to get default branch reference: ${defaultRefResponse.status}`,
);
}
const defaultRefData = (await defaultRefResponse.json()) as GitHubRef;
baseSha = defaultRefData.object.sha;
} else {
const baseRefData = (await baseRefResponse.json()) as GitHubRef;
baseSha = baseRefData.object.sha;
}
// Create the new branch using Octokit
try {
await octokit.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${branch}`,
sha: baseSha,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create branch: ${errorMessage}`);
}
return baseSha;
}
// Commit files tool // Commit files tool
server.tool( server.tool(
"commit_files", "commit_files",
@@ -81,24 +188,13 @@ server.tool(
return filePath; return filePath;
}); });
// 1. Get the branch reference // 1. Get the branch reference (create if doesn't exist)
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`; const baseSha = await getOrCreateBranchRef(
const refResponse = await fetch(refUrl, { owner,
headers: { repo,
Accept: "application/vnd.github+json", branch,
Authorization: `Bearer ${githubToken}`, githubToken,
"X-GitHub-Api-Version": "2022-11-28", );
},
});
if (!refResponse.ok) {
throw new Error(
`Failed to get branch reference: ${refResponse.status}`,
);
}
const refData = (await refResponse.json()) as GitHubRef;
const baseSha = refData.object.sha;
// 2. Get the base commit // 2. Get the base commit
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`; const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
@@ -260,7 +356,6 @@ server.tool(
// Only retry on 403 errors - these are the intermittent failures we're targeting // Only retry on 403 errors - these are the intermittent failures we're targeting
if (updateRefResponse.status === 403) { if (updateRefResponse.status === 403) {
console.log("Received 403 error, will retry...");
throw error; throw error;
} }
@@ -353,24 +448,13 @@ server.tool(
return filePath; return filePath;
}); });
// 1. Get the branch reference // 1. Get the branch reference (create if doesn't exist)
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`; const baseSha = await getOrCreateBranchRef(
const refResponse = await fetch(refUrl, { owner,
headers: { repo,
Accept: "application/vnd.github+json", branch,
Authorization: `Bearer ${githubToken}`, githubToken,
"X-GitHub-Api-Version": "2022-11-28", );
},
});
if (!refResponse.ok) {
throw new Error(
`Failed to get branch reference: ${refResponse.status}`,
);
}
const refData = (await refResponse.json()) as GitHubRef;
const baseSha = refData.object.sha;
// 2. Get the base commit // 2. Get the base commit
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`; const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
@@ -478,7 +562,6 @@ server.tool(
// Only retry on 403 errors - these are the intermittent failures we're targeting // Only retry on 403 errors - these are the intermittent failures we're targeting
if (updateRefResponse.status === 403) { if (updateRefResponse.status === 403) {
console.log("Received 403 error, will retry...");
throw error; throw error;
} }

View File

@@ -20,7 +20,7 @@ async function checkActionsReadPermission(
repo: string, repo: string,
): Promise<boolean> { ): Promise<boolean> {
try { try {
const client = new Octokit({ auth: token }); const client = new Octokit({ auth: token, baseUrl: GITHUB_API_URL });
// Try to list workflow runs - this requires actions:read // Try to list workflow runs - this requires actions:read
// We use per_page=1 to minimize the response size // We use per_page=1 to minimize the response size
@@ -100,6 +100,7 @@ export async function prepareMcpConfig(
REPO_OWNER: owner, REPO_OWNER: owner,
REPO_NAME: repo, REPO_NAME: repo,
BRANCH_NAME: branch, BRANCH_NAME: branch,
BASE_BRANCH: process.env.BASE_BRANCH || "",
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(), REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "", GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
IS_PR: process.env.IS_PR || "false", IS_PR: process.env.IS_PR || "false",

View File

@@ -21,6 +21,7 @@ describe("checkAndCommitOrDeleteBranch", () => {
const createMockOctokit = ( const createMockOctokit = (
compareResponse?: any, compareResponse?: any,
deleteRefError?: Error, deleteRefError?: Error,
branchExists: boolean = true,
): Octokits => { ): Octokits => {
return { return {
rest: { rest: {
@@ -28,6 +29,14 @@ describe("checkAndCommitOrDeleteBranch", () => {
compareCommitsWithBasehead: async () => ({ compareCommitsWithBasehead: async () => ({
data: compareResponse || { total_commits: 0 }, data: compareResponse || { total_commits: 0 },
}), }),
getBranch: async () => {
if (!branchExists) {
const error: any = new Error("Not Found");
error.status = 404;
throw error;
}
return { data: {} };
},
}, },
git: { git: {
deleteRef: async () => { deleteRef: async () => {
@@ -63,7 +72,7 @@ describe("checkAndCommitOrDeleteBranch", () => {
mockOctokit, mockOctokit,
"owner", "owner",
"repo", "repo",
"claude/issue-123-20240101_123456", "claude/issue-123-20240101-1234",
"main", "main",
true, // commit signing enabled true, // commit signing enabled
); );
@@ -71,7 +80,7 @@ describe("checkAndCommitOrDeleteBranch", () => {
expect(result.shouldDeleteBranch).toBe(true); expect(result.shouldDeleteBranch).toBe(true);
expect(result.branchLink).toBe(""); expect(result.branchLink).toBe("");
expect(consoleLogSpy).toHaveBeenCalledWith( expect(consoleLogSpy).toHaveBeenCalledWith(
"Branch claude/issue-123-20240101_123456 has no commits from Claude, will delete it", "Branch claude/issue-123-20240101-1234 has no commits from Claude, will delete it",
); );
}); });
@@ -81,14 +90,14 @@ describe("checkAndCommitOrDeleteBranch", () => {
mockOctokit, mockOctokit,
"owner", "owner",
"repo", "repo",
"claude/issue-123-20240101_123456", "claude/issue-123-20240101-1234",
"main", "main",
false, false,
); );
expect(result.shouldDeleteBranch).toBe(false); expect(result.shouldDeleteBranch).toBe(false);
expect(result.branchLink).toBe( expect(result.branchLink).toBe(
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101_123456)`, `\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101-1234)`,
); );
expect(consoleLogSpy).not.toHaveBeenCalledWith( expect(consoleLogSpy).not.toHaveBeenCalledWith(
expect.stringContaining("has no commits"), expect.stringContaining("has no commits"),
@@ -102,6 +111,7 @@ describe("checkAndCommitOrDeleteBranch", () => {
compareCommitsWithBasehead: async () => { compareCommitsWithBasehead: async () => {
throw new Error("API error"); throw new Error("API error");
}, },
getBranch: async () => ({ data: {} }), // Branch exists
}, },
git: { git: {
deleteRef: async () => ({ data: {} }), deleteRef: async () => ({ data: {} }),
@@ -113,17 +123,17 @@ describe("checkAndCommitOrDeleteBranch", () => {
mockOctokit, mockOctokit,
"owner", "owner",
"repo", "repo",
"claude/issue-123-20240101_123456", "claude/issue-123-20240101-1234",
"main", "main",
false, false,
); );
expect(result.shouldDeleteBranch).toBe(false); expect(result.shouldDeleteBranch).toBe(false);
expect(result.branchLink).toBe( expect(result.branchLink).toBe(
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101_123456)`, `\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101-1234)`,
); );
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
"Error checking for commits on Claude branch:", "Error comparing commits on Claude branch:",
expect.any(Error), expect.any(Error),
); );
}); });
@@ -136,7 +146,7 @@ describe("checkAndCommitOrDeleteBranch", () => {
mockOctokit, mockOctokit,
"owner", "owner",
"repo", "repo",
"claude/issue-123-20240101_123456", "claude/issue-123-20240101-1234",
"main", "main",
true, // commit signing enabled - will try to delete true, // commit signing enabled - will try to delete
); );
@@ -144,8 +154,34 @@ describe("checkAndCommitOrDeleteBranch", () => {
expect(result.shouldDeleteBranch).toBe(true); expect(result.shouldDeleteBranch).toBe(true);
expect(result.branchLink).toBe(""); expect(result.branchLink).toBe("");
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
"Failed to delete branch claude/issue-123-20240101_123456:", "Failed to delete branch claude/issue-123-20240101-1234:",
deleteError, deleteError,
); );
}); });
test("should return no branch link when branch doesn't exist remotely", async () => {
const mockOctokit = createMockOctokit(
{ total_commits: 0 },
undefined,
false, // branch doesn't exist
);
const result = await checkAndCommitOrDeleteBranch(
mockOctokit,
"owner",
"repo",
"claude/issue-123-20240101-1234",
"main",
false,
);
expect(result.shouldDeleteBranch).toBe(false);
expect(result.branchLink).toBe("");
expect(consoleLogSpy).toHaveBeenCalledWith(
"Branch claude/issue-123-20240101-1234 does not exist remotely",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
"Branch claude/issue-123-20240101-1234 does not exist remotely, no branch link will be added",
);
});
}); });

View File

@@ -1,5 +1,8 @@
import { describe, it, expect } from "bun:test"; import { describe, it, expect } from "bun:test";
import { updateCommentBody } from "../src/github/operations/comment-logic"; import {
updateCommentBody,
type CommentUpdateInput,
} from "../src/github/operations/comment-logic";
describe("updateCommentBody", () => { describe("updateCommentBody", () => {
const baseInput = { const baseInput = {
@@ -100,12 +103,12 @@ describe("updateCommentBody", () => {
it("adds branch name with link to header when provided", () => { it("adds branch name with link to header when provided", () => {
const input = { const input = {
...baseInput, ...baseInput,
branchName: "claude/issue-123-20240101_120000", branchName: "claude/issue-123-20240101-1200",
}; };
const result = updateCommentBody(input); const result = updateCommentBody(input);
expect(result).toContain( expect(result).toContain(
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)", "• [`claude/issue-123-20240101-1200`](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
); );
}); });
@@ -381,9 +384,9 @@ describe("updateCommentBody", () => {
const input = { const input = {
...baseInput, ...baseInput,
currentBody: "Claude Code is working… <img src='spinner.gif' />", currentBody: "Claude Code is working… <img src='spinner.gif' />",
branchName: "claude/pr-456-20240101_120000", branchName: "claude/pr-456-20240101-1200",
prLink: prLink:
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)", "\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101-1200)",
triggerUsername: "jane-doe", triggerUsername: "jane-doe",
}; };
@@ -391,7 +394,7 @@ describe("updateCommentBody", () => {
// Should include the PR link in the formatted style // Should include the PR link in the formatted style
expect(result).toContain( expect(result).toContain(
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)", "• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101-1200)",
); );
expect(result).toContain("**Claude finished @jane-doe's task**"); expect(result).toContain("**Claude finished @jane-doe's task**");
}); });
@@ -400,22 +403,44 @@ describe("updateCommentBody", () => {
const input = { const input = {
...baseInput, ...baseInput,
currentBody: "Claude Code is working…", currentBody: "Claude Code is working…",
branchName: "claude/issue-123-20240101_120000", branchName: "claude/issue-123-20240101-1200",
branchLink: branchLink:
"\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)", "\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
prLink: prLink:
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)", "\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101-1200)",
}; };
const result = updateCommentBody(input); const result = updateCommentBody(input);
// Should include both links in formatted style // Should include both links in formatted style
expect(result).toContain( expect(result).toContain(
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)", "• [`claude/issue-123-20240101-1200`](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
); );
expect(result).toContain( expect(result).toContain(
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)", "• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101-1200)",
); );
}); });
it("should not show branch name when branch doesn't exist remotely", () => {
const input: CommentUpdateInput = {
currentBody: "@claude can you help with this?",
actionFailed: false,
executionDetails: { duration_ms: 90000 },
jobUrl: "https://github.com/owner/repo/actions/runs/123",
branchLink: "", // Empty branch link means branch doesn't exist remotely
branchName: undefined, // Should be undefined when branchLink is empty
triggerUsername: "claude",
prLink: "",
};
const result = updateCommentBody(input);
expect(result).toContain("Claude finished @claude's task in 1m 30s");
expect(result).toContain(
"[View job](https://github.com/owner/repo/actions/runs/123)",
);
expect(result).not.toContain("claude/issue-123");
expect(result).not.toContain("tree/claude/issue-123");
});
}); });
}); });

View File

@@ -127,7 +127,7 @@ describe("generatePrompt", () => {
commentId: "67890", commentId: "67890",
isPR: false, isPR: false,
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000", claudeBranch: "claude/issue-67890-20240101-1200",
issueNumber: "67890", issueNumber: "67890",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
@@ -183,7 +183,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "789", issueNumber: "789",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000", claudeBranch: "claude/issue-789-20240101-1200",
}, },
}; };
@@ -210,7 +210,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "999", issueNumber: "999",
baseBranch: "develop", baseBranch: "develop",
claudeBranch: "claude/issue-999-20240101_120000", claudeBranch: "claude/issue-999-20240101-1200",
assigneeTrigger: "claude-bot", assigneeTrigger: "claude-bot",
}, },
}; };
@@ -237,7 +237,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "888", issueNumber: "888",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-888-20240101_120000", claudeBranch: "claude/issue-888-20240101-1200",
labelTrigger: "claude-task", labelTrigger: "claude-task",
}, },
}; };
@@ -265,7 +265,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "789", issueNumber: "789",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000", claudeBranch: "claude/issue-789-20240101-1200",
}, },
}; };
@@ -312,7 +312,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "123", issueNumber: "123",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000", claudeBranch: "claude/issue-67890-20240101-1200",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
}; };
@@ -334,7 +334,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "123", issueNumber: "123",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000", claudeBranch: "claude/issue-67890-20240101-1200",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
}; };
@@ -388,7 +388,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "789", issueNumber: "789",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000", claudeBranch: "claude/issue-789-20240101-1200",
}, },
}; };
@@ -396,10 +396,10 @@ describe("generatePrompt", () => {
// Should contain Issue-specific instructions // Should contain Issue-specific instructions
expect(prompt).toContain( expect(prompt).toContain(
"You are already on the correct branch (claude/issue-789-20240101_120000)", "You are already on the correct branch (claude/issue-789-20240101-1200)",
); );
expect(prompt).toContain( expect(prompt).toContain(
"IMPORTANT: You are already on the correct branch (claude/issue-789-20240101_120000)", "IMPORTANT: You are already on the correct branch (claude/issue-789-20240101-1200)",
); );
expect(prompt).toContain("Create a PR](https://github.com/"); expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain( expect(prompt).toContain(
@@ -426,7 +426,7 @@ describe("generatePrompt", () => {
isPR: false, isPR: false,
issueNumber: "123", issueNumber: "123",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-123-20240101_120000", claudeBranch: "claude/issue-123-20240101-1200",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
}; };
@@ -435,13 +435,13 @@ describe("generatePrompt", () => {
// Should contain the actual branch name with timestamp // Should contain the actual branch name with timestamp
expect(prompt).toContain( expect(prompt).toContain(
"You are already on the correct branch (claude/issue-123-20240101_120000)", "You are already on the correct branch (claude/issue-123-20240101-1200)",
); );
expect(prompt).toContain( expect(prompt).toContain(
"IMPORTANT: You are already on the correct branch (claude/issue-123-20240101_120000)", "IMPORTANT: You are already on the correct branch (claude/issue-123-20240101-1200)",
); );
expect(prompt).toContain( expect(prompt).toContain(
"The branch-name is the current branch: claude/issue-123-20240101_120000", "The branch-name is the current branch: claude/issue-123-20240101-1200",
); );
}); });
@@ -456,7 +456,7 @@ describe("generatePrompt", () => {
isPR: true, isPR: true,
prNumber: "456", prNumber: "456",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
claudeBranch: "claude/pr-456-20240101_120000", claudeBranch: "claude/pr-456-20240101-1200",
baseBranch: "main", baseBranch: "main",
}, },
}; };
@@ -465,13 +465,13 @@ describe("generatePrompt", () => {
// Should contain branch-specific instructions like issues // Should contain branch-specific instructions like issues
expect(prompt).toContain( expect(prompt).toContain(
"You are already on the correct branch (claude/pr-456-20240101_120000)", "You are already on the correct branch (claude/pr-456-20240101-1200)",
); );
expect(prompt).toContain( expect(prompt).toContain(
"Create a PR](https://github.com/owner/repo/compare/main", "Create a PR](https://github.com/owner/repo/compare/main",
); );
expect(prompt).toContain( expect(prompt).toContain(
"The branch-name is the current branch: claude/pr-456-20240101_120000", "The branch-name is the current branch: claude/pr-456-20240101-1200",
); );
expect(prompt).toContain("Reference to the original PR"); expect(prompt).toContain("Reference to the original PR");
expect(prompt).toContain( expect(prompt).toContain(
@@ -525,7 +525,7 @@ describe("generatePrompt", () => {
isPR: true, isPR: true,
prNumber: "789", prNumber: "789",
commentBody: "@claude please update this", commentBody: "@claude please update this",
claudeBranch: "claude/pr-789-20240101_123000", claudeBranch: "claude/pr-789-20240101-1230",
baseBranch: "develop", baseBranch: "develop",
}, },
}; };
@@ -534,7 +534,7 @@ describe("generatePrompt", () => {
// Should contain new branch instructions // Should contain new branch instructions
expect(prompt).toContain( expect(prompt).toContain(
"You are already on the correct branch (claude/pr-789-20240101_123000)", "You are already on the correct branch (claude/pr-789-20240101-1230)",
); );
expect(prompt).toContain( expect(prompt).toContain(
"Create a PR](https://github.com/owner/repo/compare/develop", "Create a PR](https://github.com/owner/repo/compare/develop",
@@ -553,7 +553,7 @@ describe("generatePrompt", () => {
prNumber: "999", prNumber: "999",
commentId: "review-comment-123", commentId: "review-comment-123",
commentBody: "@claude fix this issue", commentBody: "@claude fix this issue",
claudeBranch: "claude/pr-999-20240101_140000", claudeBranch: "claude/pr-999-20240101-1400",
baseBranch: "main", baseBranch: "main",
}, },
}; };
@@ -562,7 +562,7 @@ describe("generatePrompt", () => {
// Should contain new branch instructions // Should contain new branch instructions
expect(prompt).toContain( expect(prompt).toContain(
"You are already on the correct branch (claude/pr-999-20240101_140000)", "You are already on the correct branch (claude/pr-999-20240101-1400)",
); );
expect(prompt).toContain("Create a PR](https://github.com/"); expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain("Reference to the original PR"); expect(prompt).toContain("Reference to the original PR");
@@ -581,7 +581,7 @@ describe("generatePrompt", () => {
eventAction: "closed", eventAction: "closed",
isPR: true, isPR: true,
prNumber: "555", prNumber: "555",
claudeBranch: "claude/pr-555-20240101_150000", claudeBranch: "claude/pr-555-20240101-1500",
baseBranch: "main", baseBranch: "main",
}, },
}; };
@@ -590,7 +590,7 @@ describe("generatePrompt", () => {
// Should contain new branch instructions // Should contain new branch instructions
expect(prompt).toContain( expect(prompt).toContain(
"You are already on the correct branch (claude/pr-555-20240101_150000)", "You are already on the correct branch (claude/pr-555-20240101-1500)",
); );
expect(prompt).toContain("Create a PR](https://github.com/"); expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain("Reference to the original PR"); expect(prompt).toContain("Reference to the original PR");
@@ -683,7 +683,7 @@ describe("getEventTypeAndContext", () => {
isPR: false, isPR: false,
issueNumber: "999", issueNumber: "999",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-999-20240101_120000", claudeBranch: "claude/issue-999-20240101-1200",
assigneeTrigger: "claude-bot", assigneeTrigger: "claude-bot",
}, },
}; };
@@ -705,7 +705,7 @@ describe("getEventTypeAndContext", () => {
isPR: false, isPR: false,
issueNumber: "888", issueNumber: "888",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-888-20240101_120000", claudeBranch: "claude/issue-888-20240101-1200",
labelTrigger: "claude-task", labelTrigger: "claude-task",
}, },
}; };
@@ -728,7 +728,7 @@ describe("getEventTypeAndContext", () => {
isPR: false, isPR: false,
issueNumber: "999", issueNumber: "999",
baseBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-999-20240101_120000", claudeBranch: "claude/issue-999-20240101-1200",
// No assigneeTrigger when using directPrompt // No assigneeTrigger when using directPrompt
}, },
}; };

View File

@@ -35,7 +35,7 @@ describe("parseEnvVarsWithContext", () => {
process.env = { process.env = {
...BASE_ENV, ...BASE_ENV,
BASE_BRANCH: "main", BASE_BRANCH: "main",
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000", CLAUDE_BRANCH: "claude/issue-67890-20240101-1200",
}; };
}); });
@@ -44,7 +44,7 @@ describe("parseEnvVarsWithContext", () => {
mockIssueCommentContext, mockIssueCommentContext,
"12345", "12345",
"main", "main",
"claude/issue-67890-20240101_120000", "claude/issue-67890-20240101-1200",
); );
expect(result.repository).toBe("test-owner/test-repo"); expect(result.repository).toBe("test-owner/test-repo");
@@ -60,7 +60,7 @@ describe("parseEnvVarsWithContext", () => {
expect(result.eventData.issueNumber).toBe("55"); expect(result.eventData.issueNumber).toBe("55");
expect(result.eventData.commentId).toBe("12345678"); expect(result.eventData.commentId).toBe("12345678");
expect(result.eventData.claudeBranch).toBe( expect(result.eventData.claudeBranch).toBe(
"claude/issue-67890-20240101_120000", "claude/issue-67890-20240101-1200",
); );
expect(result.eventData.baseBranch).toBe("main"); expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.commentBody).toBe( expect(result.eventData.commentBody).toBe(
@@ -81,7 +81,7 @@ describe("parseEnvVarsWithContext", () => {
mockIssueCommentContext, mockIssueCommentContext,
"12345", "12345",
undefined, undefined,
"claude/issue-67890-20240101_120000", "claude/issue-67890-20240101-1200",
), ),
).toThrow("BASE_BRANCH is required for issue_comment event"); ).toThrow("BASE_BRANCH is required for issue_comment event");
}); });
@@ -152,7 +152,7 @@ describe("parseEnvVarsWithContext", () => {
process.env = { process.env = {
...BASE_ENV, ...BASE_ENV,
BASE_BRANCH: "main", BASE_BRANCH: "main",
CLAUDE_BRANCH: "claude/issue-42-20240101_120000", CLAUDE_BRANCH: "claude/issue-42-20240101-1200",
}; };
}); });
@@ -161,7 +161,7 @@ describe("parseEnvVarsWithContext", () => {
mockIssueOpenedContext, mockIssueOpenedContext,
"12345", "12345",
"main", "main",
"claude/issue-42-20240101_120000", "claude/issue-42-20240101-1200",
); );
expect(result.eventData.eventName).toBe("issues"); expect(result.eventData.eventName).toBe("issues");
@@ -174,7 +174,7 @@ describe("parseEnvVarsWithContext", () => {
expect(result.eventData.issueNumber).toBe("42"); expect(result.eventData.issueNumber).toBe("42");
expect(result.eventData.baseBranch).toBe("main"); expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.claudeBranch).toBe( expect(result.eventData.claudeBranch).toBe(
"claude/issue-42-20240101_120000", "claude/issue-42-20240101-1200",
); );
} }
}); });
@@ -184,7 +184,7 @@ describe("parseEnvVarsWithContext", () => {
mockIssueAssignedContext, mockIssueAssignedContext,
"12345", "12345",
"main", "main",
"claude/issue-123-20240101_120000", "claude/issue-123-20240101-1200",
); );
expect(result.eventData.eventName).toBe("issues"); expect(result.eventData.eventName).toBe("issues");
@@ -197,7 +197,7 @@ describe("parseEnvVarsWithContext", () => {
expect(result.eventData.issueNumber).toBe("123"); expect(result.eventData.issueNumber).toBe("123");
expect(result.eventData.baseBranch).toBe("main"); expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.claudeBranch).toBe( expect(result.eventData.claudeBranch).toBe(
"claude/issue-123-20240101_120000", "claude/issue-123-20240101-1200",
); );
expect(result.eventData.assigneeTrigger).toBe("@claude-bot"); expect(result.eventData.assigneeTrigger).toBe("@claude-bot");
} }
@@ -215,7 +215,7 @@ describe("parseEnvVarsWithContext", () => {
mockIssueOpenedContext, mockIssueOpenedContext,
"12345", "12345",
undefined, undefined,
"claude/issue-42-20240101_120000", "claude/issue-42-20240101-1200",
), ),
).toThrow("BASE_BRANCH is required for issues event"); ).toThrow("BASE_BRANCH is required for issues event");
}); });
@@ -234,7 +234,7 @@ describe("parseEnvVarsWithContext", () => {
contextWithDirectPrompt, contextWithDirectPrompt,
"12345", "12345",
"main", "main",
"claude/issue-123-20240101_120000", "claude/issue-123-20240101-1200",
); );
expect(result.eventData.eventName).toBe("issues"); expect(result.eventData.eventName).toBe("issues");
@@ -264,7 +264,7 @@ describe("parseEnvVarsWithContext", () => {
contextWithoutTriggers, contextWithoutTriggers,
"12345", "12345",
"main", "main",
"claude/issue-123-20240101_120000", "claude/issue-123-20240101-1200",
), ),
).toThrow("ASSIGNEE_TRIGGER is required for issue assigned event"); ).toThrow("ASSIGNEE_TRIGGER is required for issue assigned event");
}); });