diff --git a/README.md b/README.md index 359a9f8..3528fa1 100644 --- a/README.md +++ b/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) 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 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` | | `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 | "" | | `allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | @@ -557,6 +638,65 @@ For GitHub Enterprise users, replace the GitHub.com domains above with your ente To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use. +### 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 You can authenticate with Claude using any of these three methods: diff --git a/action.yml b/action.yml index 7c9493d..02fa466 100644 --- a/action.yml +++ b/action.yml @@ -60,6 +60,10 @@ inputs: description: "Custom environment variables to pass to Claude Code execution (YAML format)" required: false default: "" + settings: + description: "Claude Code settings as JSON string or path to settings JSON file" + required: false + default: "" # Auth configuration anthropic_api_key: @@ -181,7 +185,7 @@ runs: - name: Run Claude Code id: claude-code if: steps.prepare.outputs.contains_trigger == 'true' - uses: anthropics/claude-code-base-action@ca8aaa8335d12ada79d9336739b03e24b4aa5ae3 # v0.0.34 + uses: anthropics/claude-code-base-action@503cc7080e62d63d2cc1d80035ed04617d5efb47 # v0.0.35 with: prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt allowed_tools: ${{ env.ALLOWED_TOOLS }} @@ -196,6 +200,7 @@ runs: anthropic_api_key: ${{ inputs.anthropic_api_key }} claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} claude_env: ${{ inputs.claude_env }} + settings: ${{ inputs.settings }} env: # Model configuration ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }} diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 3af5c6b..d5e968f 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -91,7 +91,8 @@ async function run() { githubToken, owner: context.repository.owner, repo: context.repository.repo, - branch: branchInfo.currentBranch, + branch: branchInfo.claudeBranch || branchInfo.currentBranch, + baseBranch: branchInfo.baseBranch, additionalMcpConfig, claudeCommentId: commentId.toString(), allowedTools: context.inputs.allowedTools, diff --git a/src/mcp/github-file-ops-server.ts b/src/mcp/github-file-ops-server.ts index f71abd2..e3da6f4 100644 --- a/src/mcp/github-file-ops-server.ts +++ b/src/mcp/github-file-ops-server.ts @@ -78,11 +78,7 @@ async function getOrCreateBranchRef( throw new Error(`Failed to get branch reference: ${refResponse.status}`); } - // Branch doesn't exist, need to create it - console.log(`Branch ${branch} does not exist, creating it...`); - - // Get base branch from environment or determine it - const baseBranch = process.env.BASE_BRANCH || "main"; + const baseBranch = process.env.BASE_BRANCH!; // Get the SHA of the base branch const baseRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${baseBranch}`; @@ -139,7 +135,7 @@ async function getOrCreateBranchRef( baseSha = baseRefData.object.sha; } - // Create the new branch + // Create the new branch using the same pattern as octokit const createRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs`; const createRefResponse = await fetch(createRefUrl, { method: "POST", diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 30482af..31c57dd 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -8,6 +8,7 @@ type PrepareConfigParams = { owner: string; repo: string; branch: string; + baseBranch: string; additionalMcpConfig?: string; claudeCommentId?: string; allowedTools: string[]; @@ -54,6 +55,7 @@ export async function prepareMcpConfig( owner, repo, branch, + baseBranch, additionalMcpConfig, claudeCommentId, allowedTools, @@ -100,7 +102,7 @@ export async function prepareMcpConfig( REPO_OWNER: owner, REPO_NAME: repo, BRANCH_NAME: branch, - BASE_BRANCH: process.env.BASE_BRANCH || "", + BASE_BRANCH: baseBranch, REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(), GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "", IS_PR: process.env.IS_PR || "false", diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 7c63fb2..3f14a6e 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -88,6 +88,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: mockContext, }); @@ -118,6 +119,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: contextWithSigning, }); @@ -143,6 +145,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [ "mcp__github__create_issue", "mcp__github_file_ops__commit_files", @@ -174,6 +177,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [ "mcp__github_file_ops__commit_files", "mcp__github_file_ops__update_claude_comment", @@ -193,6 +197,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: ["Edit", "Read", "Write"], context: mockContext, }); @@ -210,6 +215,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: "", allowedTools: [], context: mockContext, @@ -228,6 +234,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: " \n\t ", allowedTools: [], context: mockContext, @@ -258,6 +265,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: additionalConfig, allowedTools: [ "mcp__github__create_issue", @@ -296,6 +304,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: additionalConfig, allowedTools: [ "mcp__github__create_issue", @@ -337,6 +346,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: additionalConfig, allowedTools: [], context: mockContextWithSigning, @@ -357,6 +367,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: invalidJson, allowedTools: [], context: mockContextWithSigning, @@ -378,6 +389,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: nonObjectJson, allowedTools: [], context: mockContextWithSigning, @@ -402,6 +414,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: nullJson, allowedTools: [], context: mockContextWithSigning, @@ -426,6 +439,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: arrayJson, allowedTools: [], context: mockContextWithSigning, @@ -473,6 +487,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", additionalMcpConfig: additionalConfig, allowedTools: [], context: mockContextWithSigning, @@ -496,6 +511,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: mockContextWithSigning, }); @@ -517,6 +533,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: mockContextWithSigning, }); @@ -545,6 +562,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: contextWithPermissions, }); @@ -564,6 +582,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: mockContextWithSigning, }); @@ -582,6 +601,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: mockPRContextWithSigning, }); @@ -613,6 +633,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: contextWithPermissions, }); @@ -641,6 +662,7 @@ describe("prepareMcpConfig", () => { owner: "test-owner", repo: "test-repo", branch: "test-branch", + baseBranch: "main", allowedTools: [], context: contextWithPermissions, });