mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
3 Commits
v1.0.5
...
demo/flawe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6325e51611 | ||
|
|
266d8536dc | ||
|
|
550d5c7843 |
5
.github/workflows/issue-triage.yml
vendored
5
.github/workflows/issue-triage.yml
vendored
@@ -11,7 +11,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -79,7 +78,7 @@ jobs:
|
|||||||
4. Select appropriate labels from the available labels list provided above:
|
4. Select appropriate labels from the available labels list provided above:
|
||||||
- Choose labels that accurately reflect the issue's nature
|
- Choose labels that accurately reflect the issue's nature
|
||||||
- Be specific but comprehensive
|
- Be specific but comprehensive
|
||||||
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
|
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
||||||
- Consider platform labels (android, ios) if applicable
|
- Consider platform labels (android, ios) if applicable
|
||||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Run Claude Code for Issue Triage
|
- name: Run Claude Code for Issue Triage
|
||||||
uses: anthropics/claude-code-action@v1
|
uses: anthropics/claude-code-base-action@v1
|
||||||
with:
|
with:
|
||||||
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
|
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|||||||
12
action.yml
12
action.yml
@@ -73,14 +73,6 @@ inputs:
|
|||||||
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
bot_id:
|
|
||||||
description: "GitHub user ID to use for git operations (defaults to Claude's bot ID)"
|
|
||||||
required: false
|
|
||||||
default: "41898282" # Claude's bot ID - see src/github/constants.ts
|
|
||||||
bot_name:
|
|
||||||
description: "GitHub username to use for git operations (defaults to Claude's bot name)"
|
|
||||||
required: false
|
|
||||||
default: "claude[bot]"
|
|
||||||
track_progress:
|
track_progress:
|
||||||
description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events."
|
description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events."
|
||||||
required: false
|
required: false
|
||||||
@@ -152,8 +144,6 @@ runs:
|
|||||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||||
BOT_ID: ${{ inputs.bot_id }}
|
|
||||||
BOT_NAME: ${{ inputs.bot_name }}
|
|
||||||
TRACK_PROGRESS: ${{ inputs.track_progress }}
|
TRACK_PROGRESS: ${{ inputs.track_progress }}
|
||||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||||
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||||
@@ -172,7 +162,7 @@ runs:
|
|||||||
# Install Claude Code if no custom executable is provided
|
# Install Claude Code if no custom executable is provided
|
||||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||||
echo "Installing Claude Code..."
|
echo "Installing Claude Code..."
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.107
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
else
|
else
|
||||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ runs:
|
|||||||
run: |
|
run: |
|
||||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||||
echo "Installing Claude Code..."
|
echo "Installing Claude Code..."
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.107
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
|
||||||
else
|
else
|
||||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||||
# Add the directory containing the custom executable to PATH
|
# Add the directory containing the custom executable to PATH
|
||||||
|
|||||||
27
docs/faq.md
27
docs/faq.md
@@ -28,33 +28,6 @@ permissions:
|
|||||||
|
|
||||||
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
|
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
|
||||||
|
|
||||||
### Why am I getting '403 Resource not accessible by integration' errors?
|
|
||||||
|
|
||||||
This error occurs when the action tries to fetch the authenticated user information using a GitHub App installation token. GitHub App tokens have limited access and cannot access the `/user` endpoint, which causes this 403 error.
|
|
||||||
|
|
||||||
**Solution**: The action now includes `bot_id` and `bot_name` inputs that default to Claude's bot credentials. This avoids the need to fetch user information from the API.
|
|
||||||
|
|
||||||
For the default claude[bot]:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: anthropics/claude-code-action@v1
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
# bot_id and bot_name have sensible defaults, no need to specify
|
|
||||||
```
|
|
||||||
|
|
||||||
For custom bots, specify both:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: anthropics/claude-code-action@v1
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
bot_id: "12345678" # Your bot's GitHub user ID
|
|
||||||
bot_name: "my-bot" # Your bot's username
|
|
||||||
```
|
|
||||||
|
|
||||||
This issue typically only affects agent/automation mode workflows. Interactive workflows (with @claude mentions) don't encounter this issue as they use the comment author's information.
|
|
||||||
|
|
||||||
## Claude's Capabilities and Limitations
|
## Claude's Capabilities and Limitations
|
||||||
|
|
||||||
### Why won't Claude update workflow files when I ask it to?
|
### Why won't Claude update workflow files when I ask it to?
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ This guide helps you migrate from Claude Code Action v0.x to v1.0. The new versi
|
|||||||
The following inputs have been deprecated and replaced:
|
The following inputs have been deprecated and replaced:
|
||||||
|
|
||||||
| Deprecated Input | Replacement | Notes |
|
| Deprecated Input | Replacement | Notes |
|
||||||
| --------------------- | ------------------------------------ | --------------------------------------------- |
|
| --------------------- | -------------------------------- | --------------------------------------------- |
|
||||||
| `mode` | Auto-detected | Action automatically chooses based on context |
|
| `mode` | Auto-detected | Action automatically chooses based on context |
|
||||||
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
||||||
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
||||||
@@ -26,7 +26,6 @@ The following inputs have been deprecated and replaced:
|
|||||||
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
||||||
| `claude_env` | `settings` with env object | Use settings JSON |
|
| `claude_env` | `settings` with env object | Use settings JSON |
|
||||||
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
|
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
|
||||||
| `timeout_minutes` | Use GitHub Actions `timeout-minutes` | Configure at job level instead of input level |
|
|
||||||
|
|
||||||
## Migration Examples
|
## Migration Examples
|
||||||
|
|
||||||
@@ -199,30 +198,6 @@ The `track_progress` input only works with these GitHub events:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Timeout Configuration
|
|
||||||
|
|
||||||
**Before (v0.x):**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: anthropics/claude-code-action@beta
|
|
||||||
with:
|
|
||||||
timeout_minutes: 30
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After (v1.0):**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
claude-task:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30 # Moved to job level
|
|
||||||
steps:
|
|
||||||
- uses: anthropics/claude-code-action@v1
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## How Mode Detection Works
|
## How Mode Detection Works
|
||||||
|
|
||||||
The action now automatically detects the appropriate mode:
|
The action now automatically detects the appropriate mode:
|
||||||
@@ -337,7 +312,6 @@ You can also pass MCP configuration from a file:
|
|||||||
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
|
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
|
||||||
- [ ] Move `claude_env` to `settings` JSON format
|
- [ ] Move `claude_env` to `settings` JSON format
|
||||||
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
|
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
|
||||||
- [ ] Replace `timeout_minutes` with GitHub Actions `timeout-minutes` at job level
|
|
||||||
- [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode
|
- [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode
|
||||||
- [ ] Test workflow in a non-production environment
|
- [ ] Test workflow in a non-production environment
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
|
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||||
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
|
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
|
||||||
@@ -68,8 +68,6 @@ jobs:
|
|||||||
| `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 | "" |
|
||||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||||
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
|
|
||||||
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` |
|
|
||||||
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
||||||
|
|
||||||
### Deprecated Inputs
|
### Deprecated Inputs
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* GitHub-related constants used throughout the application
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude App bot user ID
|
|
||||||
*/
|
|
||||||
export const CLAUDE_APP_BOT_ID = 41898282;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude bot username
|
|
||||||
*/
|
|
||||||
export const CLAUDE_BOT_LOGIN = "claude[bot]";
|
|
||||||
@@ -8,7 +8,6 @@ import type {
|
|||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
WorkflowRunEvent,
|
WorkflowRunEvent,
|
||||||
} from "@octokit/webhooks-types";
|
} from "@octokit/webhooks-types";
|
||||||
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "./constants";
|
|
||||||
// Custom types for GitHub Actions events that aren't webhooks
|
// Custom types for GitHub Actions events that aren't webhooks
|
||||||
export type WorkflowDispatchEvent = {
|
export type WorkflowDispatchEvent = {
|
||||||
action?: never;
|
action?: never;
|
||||||
@@ -75,8 +74,6 @@ type BaseContext = {
|
|||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
botId: string;
|
|
||||||
botName: string;
|
|
||||||
allowedBots: string;
|
allowedBots: string;
|
||||||
trackProgress: boolean;
|
trackProgress: boolean;
|
||||||
};
|
};
|
||||||
@@ -125,8 +122,6 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||||
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
|
||||||
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
|
|
||||||
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
||||||
trackProgress: process.env.TRACK_PROGRESS === "true",
|
trackProgress: process.env.TRACK_PROGRESS === "true",
|
||||||
},
|
},
|
||||||
|
|||||||
178
src/github/operations/__tests__/branch.test.ts
Normal file
178
src/github/operations/__tests__/branch.test.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { mock } from "bun:test";
|
||||||
|
import { setupBranch, type BranchInfo } from "../branch";
|
||||||
|
import type { Octokits } from "../../api/client";
|
||||||
|
import type { FetchDataResult } from "../../data/fetcher";
|
||||||
|
import type { ParsedGitHubContext } from "../../context";
|
||||||
|
import type { GitHubPullRequest, GitHubIssue } from "../../types";
|
||||||
|
|
||||||
|
// Mock process.exit to prevent tests from actually exiting
|
||||||
|
const mockExit = mock(() => {});
|
||||||
|
const originalExit = process.exit;
|
||||||
|
|
||||||
|
describe("setupBranch", () => {
|
||||||
|
let mockOctokits: Octokits;
|
||||||
|
let mockContext: ParsedGitHubContext;
|
||||||
|
let mockGithubData: FetchDataResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Replace process.exit temporarily
|
||||||
|
(process as any).exit = mockExit;
|
||||||
|
mockExit.mockClear();
|
||||||
|
|
||||||
|
// Simple mock objects
|
||||||
|
mockOctokits = {
|
||||||
|
rest: {
|
||||||
|
repos: {
|
||||||
|
get: mock(() => Promise.resolve({ data: { default_branch: "main" } })),
|
||||||
|
},
|
||||||
|
git: {
|
||||||
|
getRef: mock(() => Promise.resolve({
|
||||||
|
data: { object: { sha: "abc123def456" } }
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
graphql: mock(() => Promise.resolve({})),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockContext = {
|
||||||
|
repository: {
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
full_name: "test-owner/test-repo",
|
||||||
|
},
|
||||||
|
isPR: false,
|
||||||
|
entityNumber: 123,
|
||||||
|
inputs: {
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useCommitSigning: false,
|
||||||
|
},
|
||||||
|
} as ParsedGitHubContext;
|
||||||
|
|
||||||
|
// Default mock data for issues
|
||||||
|
mockGithubData = {
|
||||||
|
contextData: {
|
||||||
|
title: "Test Issue",
|
||||||
|
body: "Test issue body",
|
||||||
|
state: "OPEN",
|
||||||
|
} as GitHubIssue,
|
||||||
|
comments: [],
|
||||||
|
changedFiles: [],
|
||||||
|
changedFilesWithSHA: [],
|
||||||
|
reviewData: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original process.exit
|
||||||
|
process.exit = originalExit;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Issue branch creation", () => {
|
||||||
|
test("should create new branch for issue using default branch as source", async () => {
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.baseBranch).toBe("main");
|
||||||
|
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
expect(result.currentBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use provided base branch as source", async () => {
|
||||||
|
mockContext.inputs.baseBranch = "develop";
|
||||||
|
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.baseBranch).toBe("develop");
|
||||||
|
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle commit signing mode", async () => {
|
||||||
|
mockContext.inputs.useCommitSigning = true;
|
||||||
|
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.baseBranch).toBe("main");
|
||||||
|
expect(result.currentBranch).toBe("main"); // Should stay on source branch
|
||||||
|
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PR branch handling", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockContext.isPR = true;
|
||||||
|
mockGithubData.contextData = {
|
||||||
|
title: "Test PR",
|
||||||
|
body: "Test PR body",
|
||||||
|
state: "OPEN",
|
||||||
|
baseRefName: "main",
|
||||||
|
headRefName: "feature/test",
|
||||||
|
commits: { totalCount: 5 },
|
||||||
|
} as GitHubPullRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should checkout existing PR branch for open PR", async () => {
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.baseBranch).toBe("main");
|
||||||
|
expect(result.currentBranch).toBe("feature/test");
|
||||||
|
expect(result.claudeBranch).toBeUndefined(); // No claude branch for open PRs
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create new branch for closed PR", async () => {
|
||||||
|
const closedPR = mockGithubData.contextData as GitHubPullRequest;
|
||||||
|
closedPR.state = "CLOSED";
|
||||||
|
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.baseBranch).toBe("main");
|
||||||
|
expect(result.claudeBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
|
||||||
|
expect(result.currentBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create new branch for merged PR", async () => {
|
||||||
|
const mergedPR = mockGithubData.contextData as GitHubPullRequest;
|
||||||
|
mergedPR.state = "MERGED";
|
||||||
|
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.baseBranch).toBe("main");
|
||||||
|
expect(result.claudeBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error handling", () => {
|
||||||
|
test("should exit with code 1 when source branch doesn't exist", async () => {
|
||||||
|
mockOctokits.rest.git.getRef = mock(() => Promise.reject(new Error("Branch not found")));
|
||||||
|
|
||||||
|
await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should exit with code 1 when repository fetch fails", async () => {
|
||||||
|
mockOctokits.rest.repos.get = mock(() => Promise.reject(new Error("Repository not found")));
|
||||||
|
|
||||||
|
await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Branch naming", () => {
|
||||||
|
test("should generate kubernetes-compatible branch names", async () => {
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
// Branch name should be lowercase, use hyphens, and include timestamp
|
||||||
|
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
expect(result.claudeBranch?.length).toBeLessThanOrEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use custom branch prefix", async () => {
|
||||||
|
mockContext.inputs.branchPrefix = "ai/";
|
||||||
|
|
||||||
|
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
|
||||||
|
|
||||||
|
expect(result.claudeBranch).toMatch(/^ai\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,7 +17,7 @@ type GitUser = {
|
|||||||
export async function configureGitAuth(
|
export async function configureGitAuth(
|
||||||
githubToken: string,
|
githubToken: string,
|
||||||
context: GitHubContext,
|
context: GitHubContext,
|
||||||
user: GitUser,
|
user: GitUser | null,
|
||||||
) {
|
) {
|
||||||
console.log("Configuring git authentication for non-signing mode");
|
console.log("Configuring git authentication for non-signing mode");
|
||||||
|
|
||||||
@@ -28,14 +28,20 @@ export async function configureGitAuth(
|
|||||||
? "users.noreply.github.com"
|
? "users.noreply.github.com"
|
||||||
: `users.noreply.${serverUrl.hostname}`;
|
: `users.noreply.${serverUrl.hostname}`;
|
||||||
|
|
||||||
// Configure git user
|
// Configure git user based on the comment creator
|
||||||
console.log("Configuring git user...");
|
console.log("Configuring git user...");
|
||||||
|
if (user) {
|
||||||
const botName = user.login;
|
const botName = user.login;
|
||||||
const botId = user.id;
|
const botId = user.id;
|
||||||
console.log(`Setting git user as ${botName}...`);
|
console.log(`Setting git user as ${botName}...`);
|
||||||
await $`git config user.name "${botName}"`;
|
await $`git config user.name "${botName}"`;
|
||||||
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
|
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
|
||||||
console.log(`✓ Set git user as ${botName}`);
|
console.log(`✓ Set git user as ${botName}`);
|
||||||
|
} else {
|
||||||
|
console.log("No user data in comment, using default bot user");
|
||||||
|
await $`git config user.name "github-actions[bot]"`;
|
||||||
|
await $`git config user.email "41898282+github-actions[bot]@${noreplyDomain}"`;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the authorization header that actions/checkout sets
|
// Remove the authorization header that actions/checkout sets
|
||||||
console.log("Removing existing git authentication headers...");
|
console.log("Removing existing git authentication headers...");
|
||||||
|
|||||||
@@ -77,16 +77,22 @@ export const agentMode: Mode = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
async prepare({
|
||||||
|
context,
|
||||||
|
githubToken,
|
||||||
|
octokit,
|
||||||
|
}: ModeOptions): Promise<ModeResult> {
|
||||||
// Configure git authentication for agent mode (same as tag mode)
|
// Configure git authentication for agent mode (same as tag mode)
|
||||||
if (!context.inputs.useCommitSigning) {
|
if (!context.inputs.useCommitSigning) {
|
||||||
// Use bot_id and bot_name from inputs directly
|
try {
|
||||||
|
// Get the authenticated user (will be claude[bot] when using Claude App token)
|
||||||
|
const { data: authenticatedUser } =
|
||||||
|
await octokit.rest.users.getAuthenticated();
|
||||||
const user = {
|
const user = {
|
||||||
login: context.inputs.botName,
|
login: authenticatedUser.login,
|
||||||
id: parseInt(context.inputs.botId),
|
id: authenticatedUser.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
// Use the shared git configuration function
|
// Use the shared git configuration function
|
||||||
await configureGitAuth(githubToken, context, user);
|
await configureGitAuth(githubToken, context, user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
|
|||||||
|
|
||||||
// Issue events
|
// Issue events
|
||||||
if (isEntityContext(context) && isIssuesEvent(context)) {
|
if (isEntityContext(context) && isIssuesEvent(context)) {
|
||||||
|
// If prompt is provided, use agent mode (same as PR events)
|
||||||
|
if (context.inputs.prompt) {
|
||||||
|
return "agent";
|
||||||
|
}
|
||||||
// Check for @claude mentions or labels/assignees
|
// Check for @claude mentions or labels/assignees
|
||||||
if (checkContainsTrigger(context)) {
|
if (checkContainsTrigger(context)) {
|
||||||
return "tag";
|
return "tag";
|
||||||
|
|||||||
@@ -89,14 +89,8 @@ export const tagMode: Mode = {
|
|||||||
|
|
||||||
// Configure git authentication if not using commit signing
|
// Configure git authentication if not using commit signing
|
||||||
if (!context.inputs.useCommitSigning) {
|
if (!context.inputs.useCommitSigning) {
|
||||||
// Use bot_id and bot_name from inputs directly
|
|
||||||
const user = {
|
|
||||||
login: context.inputs.botName,
|
|
||||||
id: parseInt(context.inputs.botId),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await configureGitAuth(githubToken, context, user);
|
await configureGitAuth(githubToken, context, commentData.user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to configure git authentication:", error);
|
console.error("Failed to configure git authentication:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
|||||||
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import type { ParsedGitHubContext } from "../src/github/context";
|
import type { ParsedGitHubContext } from "../src/github/context";
|
||||||
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants";
|
|
||||||
|
|
||||||
describe("prepareMcpConfig", () => {
|
describe("prepareMcpConfig", () => {
|
||||||
let consoleInfoSpy: any;
|
let consoleInfoSpy: any;
|
||||||
@@ -32,8 +31,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
branchPrefix: "",
|
branchPrefix: "",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
botId: String(CLAUDE_APP_BOT_ID),
|
|
||||||
botName: CLAUDE_BOT_LOGIN,
|
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
PullRequestReviewEvent,
|
PullRequestReviewEvent,
|
||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
} from "@octokit/webhooks-types";
|
} from "@octokit/webhooks-types";
|
||||||
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants";
|
|
||||||
|
|
||||||
const defaultInputs = {
|
const defaultInputs = {
|
||||||
prompt: "",
|
prompt: "",
|
||||||
@@ -19,8 +18,6 @@ const defaultInputs = {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
botId: String(CLAUDE_APP_BOT_ID),
|
|
||||||
botName: CLAUDE_BOT_LOGIN,
|
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import {
|
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||||
describe,
|
|
||||||
test,
|
|
||||||
expect,
|
|
||||||
beforeEach,
|
|
||||||
afterEach,
|
|
||||||
spyOn,
|
|
||||||
mock,
|
|
||||||
} from "bun:test";
|
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
import type { GitHubContext } from "../../src/github/context";
|
import type { GitHubContext } from "../../src/github/context";
|
||||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as gitConfig from "../../src/github/operations/git-config";
|
|
||||||
|
|
||||||
describe("Agent Mode", () => {
|
describe("Agent Mode", () => {
|
||||||
let mockContext: GitHubContext;
|
let mockContext: GitHubContext;
|
||||||
let exportVariableSpy: any;
|
let exportVariableSpy: any;
|
||||||
let setOutputSpy: any;
|
let setOutputSpy: any;
|
||||||
let configureGitAuthSpy: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockAutomationContext({
|
mockContext = createMockAutomationContext({
|
||||||
@@ -27,22 +17,13 @@ describe("Agent Mode", () => {
|
|||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
||||||
// Mock configureGitAuth to prevent actual git commands from running
|
|
||||||
configureGitAuthSpy = spyOn(
|
|
||||||
gitConfig,
|
|
||||||
"configureGitAuth",
|
|
||||||
).mockImplementation(async () => {
|
|
||||||
// Do nothing - prevent actual git config modifications
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
exportVariableSpy?.mockClear();
|
exportVariableSpy?.mockClear();
|
||||||
setOutputSpy?.mockClear();
|
setOutputSpy?.mockClear();
|
||||||
configureGitAuthSpy?.mockClear();
|
|
||||||
exportVariableSpy?.mockRestore();
|
exportVariableSpy?.mockRestore();
|
||||||
setOutputSpy?.mockRestore();
|
setOutputSpy?.mockRestore();
|
||||||
configureGitAuthSpy?.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode has correct properties", () => {
|
test("agent mode has correct properties", () => {
|
||||||
@@ -132,22 +113,7 @@ describe("Agent Mode", () => {
|
|||||||
// Set CLAUDE_ARGS environment variable
|
// Set CLAUDE_ARGS environment variable
|
||||||
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
||||||
|
|
||||||
const mockOctokit = {
|
const mockOctokit = {} as any;
|
||||||
rest: {
|
|
||||||
users: {
|
|
||||||
getAuthenticated: mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: { login: "test-user", id: 12345 },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
getByUsername: mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: { login: "test-user", id: 12345 },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
const result = await agentMode.prepare({
|
const result = await agentMode.prepare({
|
||||||
context: contextWithCustomArgs,
|
context: contextWithCustomArgs,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
@@ -186,22 +152,7 @@ describe("Agent Mode", () => {
|
|||||||
// In v1-dev, we only have the unified prompt field
|
// In v1-dev, we only have the unified prompt field
|
||||||
contextWithPrompts.inputs.prompt = "Custom prompt content";
|
contextWithPrompts.inputs.prompt = "Custom prompt content";
|
||||||
|
|
||||||
const mockOctokit = {
|
const mockOctokit = {} as any;
|
||||||
rest: {
|
|
||||||
users: {
|
|
||||||
getAuthenticated: mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: { login: "test-user", id: 12345 },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
getByUsername: mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: { login: "test-user", id: 12345 },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
await agentMode.prepare({
|
await agentMode.prepare({
|
||||||
context: contextWithPrompts,
|
context: contextWithPrompts,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { checkWritePermissions } from "../src/github/validation/permissions";
|
import { checkWritePermissions } from "../src/github/validation/permissions";
|
||||||
import type { ParsedGitHubContext } from "../src/github/context";
|
import type { ParsedGitHubContext } from "../src/github/context";
|
||||||
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants";
|
|
||||||
|
|
||||||
describe("checkWritePermissions", () => {
|
describe("checkWritePermissions", () => {
|
||||||
let coreInfoSpy: any;
|
let coreInfoSpy: any;
|
||||||
@@ -68,8 +67,6 @@ describe("checkWritePermissions", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
botId: String(CLAUDE_APP_BOT_ID),
|
|
||||||
botName: CLAUDE_BOT_LOGIN,
|
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -113,6 +113,33 @@ describe("detectMode with enhanced routing", () => {
|
|||||||
|
|
||||||
expect(detectMode(context)).toBe("agent");
|
expect(detectMode(context)).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use agent mode for issues with explicit prompt", () => {
|
||||||
|
const context: GitHubContext = {
|
||||||
|
...baseContext,
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "opened",
|
||||||
|
payload: { issue: { number: 1, body: "Test issue" } } as any,
|
||||||
|
entityNumber: 1,
|
||||||
|
isPR: false,
|
||||||
|
inputs: { ...baseContext.inputs, prompt: "Analyze this issue" },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(detectMode(context)).toBe("agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use tag mode for issues with @claude mention and no prompt", () => {
|
||||||
|
const context: GitHubContext = {
|
||||||
|
...baseContext,
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "opened",
|
||||||
|
payload: { issue: { number: 1, body: "@claude help" } } as any,
|
||||||
|
entityNumber: 1,
|
||||||
|
isPR: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(detectMode(context)).toBe("tag");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Comment Events (unchanged behavior)", () => {
|
describe("Comment Events (unchanged behavior)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user