mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
10 Commits
v0.0.31
...
ashwin/bra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a6163092 | ||
|
|
a9d9ad3612 | ||
|
|
4824494f4d | ||
|
|
c09fc691c5 | ||
|
|
b3c6de94ea | ||
|
|
b92e56a96b | ||
|
|
b6868bfc27 | ||
|
|
0f9a2c4dc3 | ||
|
|
cefe963a6b | ||
|
|
eda5af4e69 |
140
README.md
140
README.md
@@ -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:
|
||||||
|
|||||||
10
action.yml
10
action.yml
@@ -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 }}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user