Compare commits

..

1 Commits

Author SHA1 Message Date
Hidetake Iwata
d9e73dc5d4 Accept multiline input for allowed_tools and disallowed_tools 2025-06-13 09:04:08 +09:00
18 changed files with 44 additions and 542 deletions

View File

@@ -32,7 +32,7 @@ jobs:
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-6d69797"
"ghcr.io/github/github-mcp-server:sha-7aced2b"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,138 +0,0 @@
name: Create Release
on:
workflow_dispatch:
inputs:
dry_run:
description: "Dry run (only show what would be created)"
required: false
type: boolean
default: false
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
next_version: ${{ steps.next_version.outputs.next_version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get latest tag
id: get_latest_tag
run: |
# Get only version tags (v + number pattern)
latest_tag=$(git tag -l 'v[0-9]*' | sort -V | tail -1 || echo "v0.0.0")
if [ -z "$latest_tag" ]; then
latest_tag="v0.0.0"
fi
echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT
echo "Latest tag: $latest_tag"
- name: Calculate next version
id: next_version
run: |
latest_tag="${{ steps.get_latest_tag.outputs.latest_tag }}"
# Remove 'v' prefix and split by dots
version=${latest_tag#v}
IFS='.' read -ra VERSION_PARTS <<< "$version"
# Increment patch version
major=${VERSION_PARTS[0]:-0}
minor=${VERSION_PARTS[1]:-0}
patch=${VERSION_PARTS[2]:-0}
patch=$((patch + 1))
next_version="v${major}.${minor}.${patch}"
echo "next_version=$next_version" >> $GITHUB_OUTPUT
echo "Next version: $next_version"
- name: Display dry run info
if: ${{ inputs.dry_run }}
run: |
echo "🔍 DRY RUN MODE"
echo "Would create tag: ${{ steps.next_version.outputs.next_version }}"
echo "From commit: ${{ github.sha }}"
echo "Previous tag: ${{ steps.get_latest_tag.outputs.latest_tag }}"
- name: Create and push tag
if: ${{ !inputs.dry_run }}
run: |
next_version="${{ steps.next_version.outputs.next_version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$next_version" -m "Release $next_version"
git push origin "$next_version"
- name: Create Release
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
run: |
next_version="${{ steps.next_version.outputs.next_version }}"
gh release create "$next_version" \
--title "$next_version" \
--generate-notes \
--latest=false # We want to keep beta as the latest
update-beta-tag:
needs: create-release
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update beta tag
run: |
# Get the latest version tag
VERSION=$(git tag -l 'v[0-9]*' | sort -V | tail -1)
# Update the beta tag to point to this release
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa beta -m "Update beta tag to ${VERSION}"
git push origin beta --force
- name: Update beta release to be latest
env:
GH_TOKEN: ${{ github.token }}
run: |
# Update beta release to be marked as latest
gh release edit beta --latest
update-major-tag:
needs: create-release
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update major version tag
run: |
next_version="${{ needs.create-release.outputs.next_version }}"
# Extract major version (e.g., v0 from v0.0.20)
major_version=$(echo "$next_version" | cut -d. -f1)
# Update the major version tag to point to this release
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa "$major_version" -m "Update $major_version tag to $next_version"
git push origin "$major_version" --force
echo "Updated $major_version tag to point to $next_version"

View File

@@ -50,6 +50,20 @@ Thank you for your interest in contributing to Claude Code Action! This document
bun test
```
2. **Integration Tests** (using GitHub Actions locally):
```bash
./test-local.sh
```
This script:
- Installs `act` if not present (requires Homebrew on macOS)
- Runs the GitHub Action workflow locally using Docker
- Requires your `ANTHROPIC_API_KEY` to be set
On Apple Silicon Macs, the script automatically adds the `--container-architecture linux/amd64` flag to avoid compatibility issues.
## Pull Request Process
1. Create a new branch from `main`:
@@ -89,7 +103,13 @@ Thank you for your interest in contributing to Claude Code Action! This document
When modifying the action:
1. Test in a real GitHub Actions workflow by:
1. Test locally with the test script:
```bash
./test-local.sh
```
2. Test in a real GitHub Actions workflow by:
- Creating a test repository
- Using your branch as the action source:
```yaml

4
FAQ.md
View File

@@ -12,10 +12,6 @@ The `github-actions` user cannot trigger subsequent GitHub Actions workflows. Th
Only users with **write permissions** to the repository can trigger Claude. This is a security feature to prevent unauthorized use. Make sure the user commenting has at least write access to the repository.
### Why can't I assign @claude to an issue on my repository?
If you're in a public repository, you should be able to assign to Claude without issue. If it's a private organization repository, you can only assign to users in your own organization, which Claude isn't. In this case, you'll need to make a custom user in that case.
### Why am I getting OIDC authentication errors?
If you're using the default GitHub App authentication, you must add the `id-token: write` permission to your workflow:

View File

@@ -49,7 +49,7 @@ on:
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
types: [opened, assigned]
pull_request_review:
types: [submitted]
@@ -65,8 +65,6 @@ jobs:
# trigger_phrase: "/claude"
# Optional: add assignee trigger for issues
# assignee_trigger: "claude"
# Optional: add label trigger for issues
# label_trigger: "claude"
# Optional: add custom environment variables (YAML format)
# claude_env: |
# NODE_ENV: test
@@ -82,7 +80,6 @@ jobs:
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
@@ -95,9 +92,7 @@ jobs:
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `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/` |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
@@ -154,40 +149,6 @@ For MCP servers that require sensitive information like API keys or tokens, use
# ... other inputs
```
#### Using Python MCP Servers with uv
For Python-based MCP servers managed with `uv`, you need to specify the directory containing your server:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
mcp_config: |
{
"mcpServers": {
"my-python-server": {
"type": "stdio",
"command": "uv",
"args": [
"--directory",
"${{ github.workspace }}/path/to/server/",
"run",
"server_file.py"
]
}
}
}
allowed_tools: "my-python-server__<tool_name>" # Replace <tool_name> with your server's tool names
# ... other inputs
```
For example, if your Python MCP server is at `mcp_servers/weather.py`, you would use:
```yaml
"args":
["--directory", "${{ github.workspace }}/mcp_servers/", "run", "weather.py"]
```
**Important**:
- Always use GitHub Secrets (`${{ secrets.SECRET_NAME }}`) for sensitive values like API keys, tokens, or passwords. Never hardcode secrets directly in the workflow file.

View File

@@ -12,17 +12,9 @@ inputs:
assignee_trigger:
description: "The assignee username that triggers the action (e.g. @claude)"
required: false
label_trigger:
description: "The label that triggers the action (e.g. claude)"
required: false
default: "claude"
base_branch:
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
required: false
branch_prefix:
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
required: false
default: "claude/"
# Claude Code configuration
model:
@@ -106,9 +98,7 @@ runs:
env:
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
LABEL_TRIGGER: ${{ inputs.label_trigger }}
BASE_BRANCH: ${{ inputs.base_branch }}
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
@@ -120,7 +110,7 @@ runs:
- name: Run Claude Code
id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@f6ef8c1000c0197b625af70349f68cb212e34fc1 # v0.0.28
uses: anthropics/claude-code-base-action@ebd8558e902b3db132e89863de49565fcb9aec46 # v0.0.19
with:
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }}

View File

@@ -81,7 +81,6 @@ export function prepareContext(
const eventAction = context.eventAction;
const triggerPhrase = context.inputs.triggerPhrase || "@claude";
const assigneeTrigger = context.inputs.assigneeTrigger;
const labelTrigger = context.inputs.labelTrigger;
const customInstructions = context.inputs.customInstructions;
const allowedTools = context.inputs.allowedTools;
const disallowedTools = context.inputs.disallowedTools;
@@ -243,7 +242,7 @@ export function prepareContext(
}
if (eventAction === "assigned") {
if (!assigneeTrigger && !directPrompt) {
if (!assigneeTrigger) {
throw new Error(
"ASSIGNEE_TRIGGER is required for issue assigned event",
);
@@ -255,20 +254,7 @@ export function prepareContext(
issueNumber,
baseBranch,
claudeBranch,
...(assigneeTrigger && { assigneeTrigger }),
};
} else if (eventAction === "labeled") {
if (!labelTrigger) {
throw new Error("LABEL_TRIGGER is required for issue labeled event");
}
eventData = {
eventName: "issues",
eventAction: "labeled",
isPR: false,
issueNumber,
baseBranch,
claudeBranch,
labelTrigger,
assigneeTrigger,
};
} else if (eventAction === "opened") {
eventData = {
@@ -342,17 +328,10 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
eventType: "ISSUE_CREATED",
triggerContext: `new issue with '${envVars.triggerPhrase}' in body`,
};
} else if (eventData.eventAction === "labeled") {
return {
eventType: "ISSUE_LABELED",
triggerContext: `issue labeled with '${eventData.labelTrigger}'`,
};
}
return {
eventType: "ISSUE_ASSIGNED",
triggerContext: eventData.assigneeTrigger
? `issue assigned to '${eventData.assigneeTrigger}'`
: `issue assigned event`,
triggerContext: `issue assigned to '${eventData.assigneeTrigger}'`,
};
case "pull_request":
@@ -486,7 +465,6 @@ Follow these steps:
- Analyze the pre-fetched data provided above.
- For ISSUE_CREATED: Read the issue body to find the request after the trigger phrase.
- For ISSUE_ASSIGNED: Read the entire issue body to understand the task.
- For ISSUE_LABELED: Read the entire issue body to understand the task.
${eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? ` - For comment/review events: Your instructions are in the <trigger_comment> tag above.` : ""}
${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was provided and is shown in the <direct_prompt> tag above. This is not from any GitHub comment but a direct instruction to execute.` : ""}
- IMPORTANT: Only the comment/issue containing '${context.triggerPhrase}' has your instructions.

View File

@@ -65,17 +65,7 @@ type IssueAssignedEvent = {
issueNumber: string;
baseBranch: string;
claudeBranch: string;
assigneeTrigger?: string;
};
type IssueLabeledEvent = {
eventName: "issues";
eventAction: "labeled";
isPR: false;
issueNumber: string;
baseBranch: string;
claudeBranch: string;
labelTrigger: string;
assigneeTrigger: string;
};
type PullRequestEvent = {
@@ -95,7 +85,6 @@ export type EventData =
| IssueCommentEvent
| IssueOpenedEvent
| IssueAssignedEvent
| IssueLabeledEvent
| PullRequestEvent;
// Combined type with separate eventData field

View File

@@ -1,7 +1,6 @@
import * as github from "@actions/github";
import type {
IssuesEvent,
IssuesAssignedEvent,
IssueCommentEvent,
PullRequestEvent,
PullRequestReviewEvent,
@@ -29,13 +28,11 @@ export type ParsedGitHubContext = {
inputs: {
triggerPhrase: string;
assigneeTrigger: string;
labelTrigger: string;
allowedTools: string[];
disallowedTools: string[];
customInstructions: string;
directPrompt: string;
baseBranch?: string;
branchPrefix: string;
};
};
@@ -55,13 +52,11 @@ export function parseGitHubContext(): ParsedGitHubContext {
inputs: {
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
labelTrigger: process.env.LABEL_TRIGGER ?? "",
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
directPrompt: process.env.DIRECT_PROMPT ?? "",
baseBranch: process.env.BASE_BRANCH,
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
},
};
@@ -152,9 +147,3 @@ export function isPullRequestReviewCommentEvent(
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
return context.eventName === "pull_request_review_comment";
}
export function isIssuesAssignedEvent(
context: ParsedGitHubContext,
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
return isIssuesEvent(context) && context.eventAction === "assigned";
}

View File

@@ -26,7 +26,7 @@ export async function setupBranch(
): Promise<BranchInfo> {
const { owner, repo } = context.repository;
const entityNumber = context.entityNumber;
const { baseBranch, branchPrefix } = context.inputs;
const { baseBranch } = context.inputs;
const isPR = context.isPR;
if (isPR) {
@@ -45,16 +45,9 @@ export async function setupBranch(
const branchName = prData.headRefName;
// Determine optimal fetch depth based on PR commit count, with a minimum of 20
const commitCount = prData.commits.totalCount;
const fetchDepth = Math.max(commitCount, 20);
console.log(
`PR #${entityNumber}: ${commitCount} commits, using fetch depth ${fetchDepth}`,
);
// Execute git commands to checkout PR branch (dynamic depth based on PR size)
await $`git fetch origin --depth=${fetchDepth} ${branchName}`;
// Execute git commands to checkout PR branch (shallow fetch for performance)
// Fetch the branch with a depth of 20 to avoid fetching too much history, while still allowing for some context
await $`git fetch origin --depth=20 ${branchName}`;
await $`git checkout ${branchName}`;
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
@@ -97,7 +90,7 @@ export async function setupBranch(
.split("T")
.join("_");
const newBranch = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
try {
// Get the SHA of the source branch

View File

@@ -3,7 +3,6 @@
import * as core from "@actions/core";
import {
isIssuesEvent,
isIssuesAssignedEvent,
isIssueCommentEvent,
isPullRequestEvent,
isPullRequestReviewEvent,
@@ -13,7 +12,7 @@ import type { ParsedGitHubContext } from "../context";
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
const {
inputs: { assigneeTrigger, labelTrigger, triggerPhrase, directPrompt },
inputs: { assigneeTrigger, triggerPhrase, directPrompt },
} = context;
// If direct prompt is provided, always trigger
@@ -23,10 +22,10 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
}
// Check for assignee trigger
if (isIssuesAssignedEvent(context)) {
if (isIssuesEvent(context) && context.eventAction === "assigned") {
// Remove @ symbol from assignee_trigger if present
let triggerUser = assigneeTrigger.replace(/^@/, "");
const assigneeUsername = context.payload.assignee?.login || "";
const assigneeUsername = context.payload.issue.assignee?.login || "";
if (triggerUser && assigneeUsername === triggerUser) {
console.log(`Issue assigned to trigger user '${triggerUser}'`);
@@ -34,16 +33,6 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
}
}
// Check for label trigger
if (isIssuesEvent(context) && context.eventAction === "labeled") {
const labelName = (context.payload as any).label?.name || "";
if (labelTrigger && labelName === labelTrigger) {
console.log(`Issue labeled with trigger label '${labelTrigger}'`);
return true;
}
}
// Check for issue body and title trigger on issue creation
if (isIssuesEvent(context) && context.eventAction === "opened") {
const issueBody = context.payload.issue.body || "";

View File

@@ -125,58 +125,13 @@ server.tool(
? filePath
: join(REPO_DIR, filePath);
// Check if file is binary (images, etc.)
const isBinaryFile =
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
filePath,
);
if (isBinaryFile) {
// For binary files, create a blob first using the Blobs API
const binaryContent = await readFile(fullPath);
// Create blob using Blobs API (supports encoding parameter)
const blobUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/blobs`;
const blobResponse = await fetch(blobUrl, {
method: "POST",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: binaryContent.toString("base64"),
encoding: "base64",
}),
});
if (!blobResponse.ok) {
const errorText = await blobResponse.text();
throw new Error(
`Failed to create blob for ${filePath}: ${blobResponse.status} - ${errorText}`,
);
}
const blobData = (await blobResponse.json()) as { sha: string };
// Return tree entry with blob SHA
return {
path: filePath,
mode: "100644",
type: "blob",
sha: blobData.sha,
};
} else {
// For text files, include content directly in tree
const content = await readFile(fullPath, "utf-8");
return {
path: filePath,
mode: "100644",
type: "blob",
content: content,
};
}
const content = await readFile(fullPath, "utf-8");
return {
path: filePath,
mode: "100644",
type: "blob",
content: content,
};
}),
);

View File

@@ -62,7 +62,7 @@ export async function prepareMcpConfig(
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-6d69797", // https://github.com/github/github-mcp-server/releases/tag/v0.5.0
"ghcr.io/github/github-mcp-server:sha-e9f748f", // https://github.com/github/github-mcp-server/releases/tag/v0.4.0
],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,

View File

@@ -226,33 +226,6 @@ describe("generatePrompt", () => {
);
});
test("should generate prompt for issue labeled event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "labeled",
isPR: false,
issueNumber: "888",
baseBranch: "main",
claudeBranch: "claude/issue-888-20240101_120000",
labelTrigger: "claude-task",
},
};
const prompt = generatePrompt(envVars, mockGitHubData);
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
expect(prompt).toContain(
"<trigger_context>issue labeled with 'claude-task'</trigger_context>",
);
expect(prompt).toContain(
"[Create a PR](https://github.com/owner/repo/compare/main",
);
});
test("should include direct prompt when provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
@@ -641,51 +614,6 @@ describe("getEventTypeAndContext", () => {
expect(result.eventType).toBe("ISSUE_ASSIGNED");
expect(result.triggerContext).toBe("issue assigned to 'claude-bot'");
});
test("should return correct type and context for issue labeled", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "labeled",
isPR: false,
issueNumber: "888",
baseBranch: "main",
claudeBranch: "claude/issue-888-20240101_120000",
labelTrigger: "claude-task",
},
};
const result = getEventTypeAndContext(envVars);
expect(result.eventType).toBe("ISSUE_LABELED");
expect(result.triggerContext).toBe("issue labeled with 'claude-task'");
});
test("should return correct type and context for issue assigned without assigneeTrigger", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
directPrompt: "Please assess this issue",
eventData: {
eventName: "issues",
eventAction: "assigned",
isPR: false,
issueNumber: "999",
baseBranch: "main",
claudeBranch: "claude/issue-999-20240101_120000",
// No assigneeTrigger when using directPrompt
},
};
const result = getEventTypeAndContext(envVars);
expect(result.eventType).toBe("ISSUE_ASSIGNED");
expect(result.triggerContext).toBe("issue assigned event");
});
});
describe("buildAllowedToolsString", () => {

View File

@@ -10,7 +10,6 @@ import type {
const defaultInputs = {
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
anthropicModel: "claude-3-7-sonnet-20250219",
allowedTools: [] as string[],
disallowedTools: [] as string[],
@@ -19,7 +18,6 @@ const defaultInputs = {
useBedrock: false,
useVertex: false,
timeoutMinutes: 30,
branchPrefix: "claude/",
};
const defaultRepository = {
@@ -93,12 +91,6 @@ export const mockIssueAssignedContext: ParsedGitHubContext = {
actor: "admin-user",
payload: {
action: "assigned",
assignee: {
login: "claude-bot",
id: 11111,
avatar_url: "https://avatars.githubusercontent.com/u/11111",
html_url: "https://github.com/claude-bot",
},
issue: {
number: 123,
title: "Feature: Add dark mode support",
@@ -130,46 +122,6 @@ export const mockIssueAssignedContext: ParsedGitHubContext = {
inputs: { ...defaultInputs, assigneeTrigger: "@claude-bot" },
};
export const mockIssueLabeledContext: ParsedGitHubContext = {
runId: "1234567890",
eventName: "issues",
eventAction: "labeled",
repository: defaultRepository,
actor: "admin-user",
payload: {
action: "labeled",
issue: {
number: 1234,
title: "Enhancement: Improve search functionality",
body: "The current search is too slow and needs optimization",
user: {
login: "alice-wonder",
id: 54321,
avatar_url: "https://avatars.githubusercontent.com/u/54321",
html_url: "https://github.com/alice-wonder",
},
assignee: null,
},
label: {
id: 987654321,
name: "claude-task",
color: "f29513",
description: "Label for Claude AI interactions",
},
repository: {
name: "test-repo",
full_name: "test-owner/test-repo",
private: false,
owner: {
login: "test-owner",
},
},
} as IssuesEvent,
entityNumber: 1234,
isPR: false,
inputs: { ...defaultInputs, labelTrigger: "claude-task" },
};
// Issue comment on issue event
export const mockIssueCommentContext: ParsedGitHubContext = {
runId: "1234567890",

View File

@@ -62,12 +62,10 @@ describe("checkWritePermissions", () => {
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
directPrompt: "",
branchPrefix: "claude/",
},
});

View File

@@ -219,55 +219,6 @@ describe("parseEnvVarsWithContext", () => {
),
).toThrow("BASE_BRANCH is required for issues event");
});
test("should allow issue assigned event with direct_prompt and no assigneeTrigger", () => {
const contextWithDirectPrompt = createMockContext({
...mockIssueAssignedContext,
inputs: {
...mockIssueAssignedContext.inputs,
assigneeTrigger: "", // No assignee trigger
directPrompt: "Please assess this issue", // But direct prompt is provided
},
});
const result = prepareContext(
contextWithDirectPrompt,
"12345",
"main",
"claude/issue-123-20240101_120000",
);
expect(result.eventData.eventName).toBe("issues");
expect(result.eventData.isPR).toBe(false);
expect(result.directPrompt).toBe("Please assess this issue");
if (
result.eventData.eventName === "issues" &&
result.eventData.eventAction === "assigned"
) {
expect(result.eventData.issueNumber).toBe("123");
expect(result.eventData.assigneeTrigger).toBeUndefined();
}
});
test("should throw error when neither assigneeTrigger nor directPrompt provided for issue assigned event", () => {
const contextWithoutTriggers = createMockContext({
...mockIssueAssignedContext,
inputs: {
...mockIssueAssignedContext.inputs,
assigneeTrigger: "", // No assignee trigger
directPrompt: "", // No direct prompt
},
});
expect(() =>
prepareContext(
contextWithoutTriggers,
"12345",
"main",
"claude/issue-123-20240101_120000",
),
).toThrow("ASSIGNEE_TRIGGER is required for issue assigned event");
});
});
describe("optional fields", () => {

View File

@@ -6,7 +6,6 @@ import { describe, it, expect } from "bun:test";
import {
createMockContext,
mockIssueAssignedContext,
mockIssueLabeledContext,
mockIssueCommentContext,
mockIssueOpenedContext,
mockPullRequestReviewContext,
@@ -30,12 +29,10 @@ describe("checkContainsTrigger", () => {
inputs: {
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "Fix the bug in the login form",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -58,12 +55,10 @@ describe("checkContainsTrigger", () => {
inputs: {
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
},
});
expect(checkContainsTrigger(context)).toBe(false);
@@ -92,11 +87,6 @@ describe("checkContainsTrigger", () => {
...mockIssueAssignedContext,
payload: {
...mockIssueAssignedContext.payload,
assignee: {
...(mockIssueAssignedContext.payload as IssuesAssignedEvent)
.assignee,
login: "otherUser",
},
issue: {
...(mockIssueAssignedContext.payload as IssuesAssignedEvent).issue,
assignee: {
@@ -112,39 +102,6 @@ describe("checkContainsTrigger", () => {
});
});
describe("label trigger", () => {
it("should return true when issue is labeled with the trigger label", () => {
const context = mockIssueLabeledContext;
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return false when issue is labeled with a different label", () => {
const context = {
...mockIssueLabeledContext,
payload: {
...mockIssueLabeledContext.payload,
label: {
...(mockIssueLabeledContext.payload as any).label,
name: "bug",
},
},
} as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(false);
});
it("should return false for non-labeled events", () => {
const context = {
...mockIssueLabeledContext,
eventAction: "opened",
payload: {
...mockIssueLabeledContext.payload,
action: "opened",
},
} as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(false);
});
});
describe("issue body and title trigger", () => {
it("should return true when issue body contains trigger phrase", () => {
const context = mockIssueOpenedContext;
@@ -270,12 +227,10 @@ describe("checkContainsTrigger", () => {
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -299,12 +254,10 @@ describe("checkContainsTrigger", () => {
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -328,12 +281,10 @@ describe("checkContainsTrigger", () => {
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
},
});
expect(checkContainsTrigger(context)).toBe(false);