diff --git a/README.md b/README.md index 646387f..08d9d90 100644 --- a/README.md +++ b/README.md @@ -167,41 +167,79 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- | -| `mode` | Execution mode for the action. Currently supports 'tag' (default). Future modes: 'review', 'freeform' | No | `tag` | -| `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\* | - | -| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | -| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | 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` | -| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | -| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | -| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | -| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - | -| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | -| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | -| `disallowed_tools` | Tools that Claude should never use | No | "" | -| `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 | "" | -| `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 | "" | -| `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` | +| Input | Description | Required | Default | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- | --------- | +| `mode` | Execution mode: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking) | No | `tag` | +| `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\* | - | +| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | +| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | 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` | +| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | +| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | +| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | +| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - | +| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | +| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | +| `disallowed_tools` | Tools that Claude should never use | No | "" | +| `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 | "" | +| `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 | "" | +| `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` | \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex) > **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration. +## Execution Modes + +The action supports two execution modes, each optimized for different use cases: + +### Tag Mode (Default) + +The traditional implementation mode that responds to @claude mentions, issue assignments, or labels. + +- **Triggers**: `@claude` mentions, issue assignment, label application +- **Features**: Creates tracking comments with progress checkboxes, full implementation capabilities +- **Use case**: General-purpose code implementation and Q&A + +```yaml +- uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # mode: tag is the default +``` + +### Agent Mode + +For automation and scheduled tasks without trigger checking. + +- **Triggers**: Always runs (no trigger checking) +- **Features**: Perfect for scheduled tasks, works with `override_prompt` +- **Use case**: Maintenance tasks, automated reporting, scheduled checks + +```yaml +- uses: anthropics/claude-code-action@beta + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Check for outdated dependencies and create an issue if any are found. +``` + +See [`examples/claude-modes.yml`](./examples/claude-modes.yml) for complete examples of each mode. + ### Using Custom MCP Configuration The `mcp_config` input allows you to add custom MCP (Model Context Protocol) servers to extend Claude's capabilities. These servers merge with the built-in GitHub MCP servers. diff --git a/action.yml b/action.yml index 0704ba5..fb54de0 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,7 @@ inputs: # Mode configuration mode: - description: "Execution mode for the action. Currently only 'tag' mode is supported (traditional implementation triggered by mentions/assignments)" + description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)" required: false default: "tag" diff --git a/examples/claude-modes.yml b/examples/claude-modes.yml new file mode 100644 index 0000000..5809e24 --- /dev/null +++ b/examples/claude-modes.yml @@ -0,0 +1,56 @@ +name: Claude Mode Examples + +on: + # Common events for both modes + issue_comment: + types: [created] + issues: + types: [opened, labeled] + pull_request: + types: [opened] + +jobs: + # Tag Mode (Default) - Traditional implementation + tag-mode-example: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + steps: + - uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Tag mode (default) behavior: + # - Scans for @claude mentions in comments, issues, and PRs + # - Only acts when trigger phrase is found + # - Creates tracking comments with progress checkboxes + # - Perfect for: Interactive Q&A, on-demand code changes + + # Agent Mode - Automation without triggers + agent-mode-auto-review: + # Automatically review every new PR + if: github.event_name == 'pull_request' && github.event.action == 'opened' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + steps: + - uses: anthropics/claude-code-action@beta + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Review this PR for code quality. Focus on: + - Potential bugs or logic errors + - Security concerns + - Performance issues + + Provide specific, actionable feedback. + # Agent mode behavior: + # - NO @claude mention needed - runs immediately + # - Enables true automation (impossible with tag mode) + # - Perfect for: CI/CD integration, automatic reviews, label-based workflows diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index f9ff35d..27b3281 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -840,14 +840,29 @@ export async function createPrompt( const hasActionsReadPermission = context.inputs.additionalPermissions.get("actions") === "read" && context.isPR; + + // Get mode-specific tools + const modeAllowedTools = mode.getAllowedTools(); + const modeDisallowedTools = mode.getDisallowedTools(); + + // Combine with existing allowed tools + const combinedAllowedTools = [ + ...context.inputs.allowedTools, + ...modeAllowedTools, + ]; + const combinedDisallowedTools = [ + ...context.inputs.disallowedTools, + ...modeDisallowedTools, + ]; + const allAllowedTools = buildAllowedToolsString( - context.inputs.allowedTools, + combinedAllowedTools, hasActionsReadPermission, context.inputs.useCommitSigning, ); const allDisallowedTools = buildDisallowedToolsString( - context.inputs.disallowedTools, - context.inputs.allowedTools, + combinedDisallowedTools, + combinedAllowedTools, ); core.exportVariable("ALLOWED_TOOLS", allAllowedTools); diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 3e5a956..6653c06 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -55,7 +55,7 @@ async function run() { await checkHumanActor(octokit.rest, context); // Step 6: Create initial tracking comment (mode-aware) - // Some modes (e.g., future review/freeform modes) may not need tracking comments + // Some modes (e.g., agent mode) may not need tracking comments let commentId: number | undefined; let commentData: | Awaited> diff --git a/src/github/context.ts b/src/github/context.ts index 961ac7e..4e0d866 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -7,9 +7,8 @@ import type { PullRequestReviewEvent, PullRequestReviewCommentEvent, } from "@octokit/webhooks-types"; -import type { ModeName } from "../modes/registry"; -import { DEFAULT_MODE } from "../modes/registry"; -import { isValidMode } from "../modes/registry"; +import type { ModeName } from "../modes/types"; +import { DEFAULT_MODE, isValidMode } from "../modes/registry"; export type ParsedGitHubContext = { runId: string; diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts new file mode 100644 index 0000000..fd78356 --- /dev/null +++ b/src/modes/agent/index.ts @@ -0,0 +1,42 @@ +import type { Mode } from "../types"; + +/** + * Agent mode implementation. + * + * This mode is designed for automation and workflow_dispatch scenarios. + * It always triggers (no checking), allows highly flexible configurations, + * and works well with override_prompt for custom workflows. + * + * In the future, this mode could restrict certain tools for safety in automation contexts, + * e.g., disallowing WebSearch or limiting file system operations. + */ +export const agentMode: Mode = { + name: "agent", + description: "Automation mode that always runs without trigger checking", + + shouldTrigger() { + return true; + }, + + prepareContext(context, data) { + return { + mode: "agent", + githubContext: context, + commentId: data?.commentId, + baseBranch: data?.baseBranch, + claudeBranch: data?.claudeBranch, + }; + }, + + getAllowedTools() { + return []; + }, + + getDisallowedTools() { + return []; + }, + + shouldCreateTrackingComment() { + return false; + }, +}; diff --git a/src/modes/registry.ts b/src/modes/registry.ts index 37aadd4..043137a 100644 --- a/src/modes/registry.ts +++ b/src/modes/registry.ts @@ -5,17 +5,17 @@ * * To add a new mode: * 1. Add the mode name to VALID_MODES below - * 2. Create the mode implementation in a new directory (e.g., src/modes/review/) + * 2. Create the mode implementation in a new directory (e.g., src/modes/new-mode/) * 3. Import and add it to the modes object below * 4. Update action.yml description to mention the new mode */ -import type { Mode } from "./types"; -import { tagMode } from "./tag/index"; +import type { Mode, ModeName } from "./types"; +import { tagMode } from "./tag"; +import { agentMode } from "./agent"; export const DEFAULT_MODE = "tag" as const; -export const VALID_MODES = ["tag"] as const; -export type ModeName = (typeof VALID_MODES)[number]; +export const VALID_MODES = ["tag", "agent"] as const; /** * All available modes. @@ -23,6 +23,7 @@ export type ModeName = (typeof VALID_MODES)[number]; */ const modes = { tag: tagMode, + agent: agentMode, } as const satisfies Record; /** diff --git a/src/modes/types.ts b/src/modes/types.ts index 2cb2a75..cd3d1b7 100644 --- a/src/modes/types.ts +++ b/src/modes/types.ts @@ -1,5 +1,6 @@ import type { ParsedGitHubContext } from "../github/context"; -import type { ModeName } from "./registry"; + +export type ModeName = "tag" | "agent"; export type ModeContext = { mode: ModeName; @@ -20,9 +21,9 @@ export type ModeData = { * Each mode defines its own behavior for trigger detection, prompt generation, * and tracking comment creation. * - * Future modes might include: - * - 'review': Optimized for code reviews without tracking comments - * - 'freeform': For automation with no trigger checking + * Current modes include: + * - 'tag': Traditional implementation triggered by mentions/assignments + * - 'agent': For automation with no trigger checking */ export type Mode = { name: ModeName; @@ -39,13 +40,12 @@ export type Mode = { prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext; /** - * Returns additional tools that should be allowed for this mode - * (base GitHub tools are always included) + * Returns the list of tools that should be allowed for this mode */ getAllowedTools(): string[]; /** - * Returns tools that should be disallowed for this mode + * Returns the list of tools that should be disallowed for this mode */ getDisallowedTools(): string[]; diff --git a/test/modes/agent.test.ts b/test/modes/agent.test.ts new file mode 100644 index 0000000..d6583c8 --- /dev/null +++ b/test/modes/agent.test.ts @@ -0,0 +1,82 @@ +import { describe, test, expect, beforeEach } from "bun:test"; +import { agentMode } from "../../src/modes/agent"; +import type { ParsedGitHubContext } from "../../src/github/context"; +import { createMockContext } from "../mockContext"; + +describe("Agent Mode", () => { + let mockContext: ParsedGitHubContext; + + beforeEach(() => { + mockContext = createMockContext({ + eventName: "workflow_dispatch", + isPR: false, + }); + }); + + test("agent mode has correct properties and behavior", () => { + // Basic properties + expect(agentMode.name).toBe("agent"); + expect(agentMode.description).toBe( + "Automation mode that always runs without trigger checking", + ); + expect(agentMode.shouldCreateTrackingComment()).toBe(false); + + // Tool methods return empty arrays + expect(agentMode.getAllowedTools()).toEqual([]); + expect(agentMode.getDisallowedTools()).toEqual([]); + + // Always triggers regardless of context + const contextWithoutTrigger = createMockContext({ + eventName: "workflow_dispatch", + isPR: false, + inputs: { + ...createMockContext().inputs, + triggerPhrase: "@claude", + }, + payload: {} as any, + }); + expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true); + }); + + test("prepareContext includes all required data", () => { + const data = { + commentId: 789, + baseBranch: "develop", + claudeBranch: "claude/automated-task", + }; + + const context = agentMode.prepareContext(mockContext, data); + + expect(context.mode).toBe("agent"); + expect(context.githubContext).toBe(mockContext); + expect(context.commentId).toBe(789); + expect(context.baseBranch).toBe("develop"); + expect(context.claudeBranch).toBe("claude/automated-task"); + }); + + test("prepareContext works without data", () => { + const context = agentMode.prepareContext(mockContext); + + expect(context.mode).toBe("agent"); + expect(context.githubContext).toBe(mockContext); + expect(context.commentId).toBeUndefined(); + expect(context.baseBranch).toBeUndefined(); + expect(context.claudeBranch).toBeUndefined(); + }); + + test("agent mode triggers for all event types", () => { + const events = [ + "push", + "schedule", + "workflow_dispatch", + "repository_dispatch", + "issue_comment", + "pull_request", + ]; + + events.forEach((eventName) => { + const context = createMockContext({ eventName, isPR: false }); + expect(agentMode.shouldTrigger(context)).toBe(true); + }); + }); +}); diff --git a/test/modes/registry.test.ts b/test/modes/registry.test.ts index 699c3f3..2e7b011 100644 --- a/test/modes/registry.test.ts +++ b/test/modes/registry.test.ts @@ -1,6 +1,8 @@ import { describe, test, expect } from "bun:test"; -import { getMode, isValidMode, type ModeName } from "../../src/modes/registry"; +import { getMode, isValidMode } from "../../src/modes/registry"; +import type { ModeName } from "../../src/modes/types"; import { tagMode } from "../../src/modes/tag"; +import { agentMode } from "../../src/modes/agent"; describe("Mode Registry", () => { test("getMode returns tag mode by default", () => { @@ -9,20 +11,26 @@ describe("Mode Registry", () => { expect(mode.name).toBe("tag"); }); + test("getMode returns agent mode", () => { + const mode = getMode("agent"); + expect(mode).toBe(agentMode); + expect(mode.name).toBe("agent"); + }); + test("getMode throws error for invalid mode", () => { const invalidMode = "invalid" as unknown as ModeName; expect(() => getMode(invalidMode)).toThrow( - "Invalid mode 'invalid'. Valid modes are: 'tag'. Please check your workflow configuration.", + "Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.", ); }); - test("isValidMode returns true for tag mode", () => { + test("isValidMode returns true for all valid modes", () => { expect(isValidMode("tag")).toBe(true); + expect(isValidMode("agent")).toBe(true); }); test("isValidMode returns false for invalid mode", () => { expect(isValidMode("invalid")).toBe(false); expect(isValidMode("review")).toBe(false); - expect(isValidMode("freeform")).toBe(false); }); });