Skip to content

Commit b2cb4f8

Browse files
authored
Merge branch 'anthropics:main' into feat/allow-bot-actor
2 parents 8f1983f + c09fc69 commit b2cb4f8

9 files changed

Lines changed: 160 additions & 69 deletions

File tree

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,86 @@ This command will guide you through setting up the GitHub app and required secre
3535
- Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally)
3636
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
3737

38+
### Using a Custom GitHub App
39+
40+
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.
41+
42+
**When you may want to use a custom GitHub App:**
43+
44+
- You need more restrictive permissions than the official app
45+
- Organization policies prevent installing third-party apps
46+
- You're using AWS Bedrock or Google Vertex AI
47+
48+
**Steps to create and use a custom GitHub App:**
49+
50+
1. **Create a new GitHub App:**
51+
52+
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
53+
- Click "New GitHub App"
54+
- Configure the app with these minimum permissions:
55+
- **Repository permissions:**
56+
- Contents: Read & Write
57+
- Issues: Read & Write
58+
- Pull requests: Read & Write
59+
- **Account permissions:** None required
60+
- Set "Where can this GitHub App be installed?" to your preference
61+
- Create the app
62+
63+
2. **Generate and download a private key:**
64+
65+
- After creating the app, scroll down to "Private keys"
66+
- Click "Generate a private key"
67+
- Download the `.pem` file (keep this secure!)
68+
69+
3. **Install the app on your repository:**
70+
71+
- Go to the app's settings page
72+
- Click "Install App"
73+
- Select the repositories where you want to use Claude
74+
75+
4. **Add the app credentials to your repository secrets:**
76+
77+
- Go to your repository's Settings → Secrets and variables → Actions
78+
- Add these secrets:
79+
- `APP_ID`: Your GitHub App's ID (found in the app settings)
80+
- `APP_PRIVATE_KEY`: The contents of the downloaded `.pem` file
81+
82+
5. **Update your workflow to use the custom app:**
83+
84+
```yaml
85+
name: Claude with Custom App
86+
on:
87+
issue_comment:
88+
types: [created]
89+
# ... other triggers
90+
91+
jobs:
92+
claude-response:
93+
runs-on: ubuntu-latest
94+
steps:
95+
# Generate a token from your custom app
96+
- name: Generate GitHub App token
97+
id: app-token
98+
uses: actions/create-github-app-token@v1
99+
with:
100+
app-id: ${{ secrets.APP_ID }}
101+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
102+
103+
# Use Claude with your custom app's token
104+
- uses: anthropics/claude-code-action@beta
105+
with:
106+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
107+
github_token: ${{ steps.app-token.outputs.token }}
108+
# ... other configuration
109+
```
110+
111+
**Important notes:**
112+
113+
- The custom app must have read/write permissions for Issues, Pull Requests, and Contents
114+
- Your app's token will have the exact permissions you configured, nothing more
115+
116+
For more information on creating GitHub Apps, see the [GitHub documentation](https://docs.github.com/en/apps/creating-github-apps).
117+
38118
## 📚 FAQ
39119

40120
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.

action.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ outputs:
105105
execution_file:
106106
description: "Path to the Claude Code execution output file"
107107
value: ${{ steps.claude-code.outputs.execution_file }}
108+
branch_name:
109+
description: "The branch created by Claude Code for this execution"
110+
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
108111

109112
runs:
110113
using: "composite"
@@ -147,7 +150,7 @@ runs:
147150
- name: Run Claude Code
148151
id: claude-code
149152
if: steps.prepare.outputs.contains_trigger == 'true'
150-
uses: anthropics/claude-code-base-action@0f7a229cb06f840f77f49df0b711ee0060868c2c # v0.0.33
153+
uses: anthropics/claude-code-base-action@ca8aaa8335d12ada79d9336739b03e24b4aa5ae3 # v0.0.34
151154
with:
152155
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
153156
allowed_tools: ${{ env.ALLOWED_TOOLS }}

src/github/operations/branch.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,18 @@ export async function setupBranch(
8686

8787
// Generate branch name for either an issue or closed/merged PR
8888
const entityType = isPR ? "pr" : "issue";
89-
const timestamp = new Date()
90-
.toISOString()
91-
.replace(/[:-]/g, "")
92-
.replace(/\.\d{3}Z/, "")
93-
.split("T")
94-
.join("_");
95-
96-
const newBranch = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
89+
90+
// Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format
91+
const now = new Date();
92+
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
93+
94+
// Ensure branch name is Kubernetes-compatible:
95+
// - Lowercase only
96+
// - Alphanumeric with hyphens
97+
// - No underscores
98+
// - Max 50 chars (to allow for prefixes)
99+
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
100+
const newBranch = branchName.toLowerCase().substring(0, 50);
97101

98102
try {
99103
// Get the SHA of the source branch to verify it exists

src/mcp/github-actions-server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
import { z } from "zod";
6+
import { GITHUB_API_URL } from "../github/api/config";
67
import { mkdir, writeFile } from "fs/promises";
78
import { Octokit } from "@octokit/rest";
89

@@ -54,6 +55,7 @@ server.tool(
5455
try {
5556
const client = new Octokit({
5657
auth: GITHUB_TOKEN,
58+
baseUrl: GITHUB_API_URL,
5759
});
5860

5961
// Get the PR to find the head SHA
@@ -142,6 +144,7 @@ server.tool(
142144
try {
143145
const client = new Octokit({
144146
auth: GITHUB_TOKEN,
147+
baseUrl: GITHUB_API_URL,
145148
});
146149

147150
// Get jobs for this workflow run
@@ -209,6 +212,7 @@ server.tool(
209212
try {
210213
const client = new Octokit({
211214
auth: GITHUB_TOKEN,
215+
baseUrl: GITHUB_API_URL,
212216
});
213217

214218
const response = await client.actions.downloadJobLogsForWorkflowRun({

src/mcp/install-mcp-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ async function checkActionsReadPermission(
2020
repo: string,
2121
): Promise<boolean> {
2222
try {
23-
const client = new Octokit({ auth: token });
23+
const client = new Octokit({ auth: token, baseUrl: GITHUB_API_URL });
2424

2525
// Try to list workflow runs - this requires actions:read
2626
// We use per_page=1 to minimize the response size

test/branch-cleanup.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ describe("checkAndCommitOrDeleteBranch", () => {
7272
mockOctokit,
7373
"owner",
7474
"repo",
75-
"claude/issue-123-20240101_123456",
75+
"claude/issue-123-20240101-1234",
7676
"main",
7777
true, // commit signing enabled
7878
);
7979

8080
expect(result.shouldDeleteBranch).toBe(true);
8181
expect(result.branchLink).toBe("");
8282
expect(consoleLogSpy).toHaveBeenCalledWith(
83-
"Branch claude/issue-123-20240101_123456 has no commits from Claude, will delete it",
83+
"Branch claude/issue-123-20240101-1234 has no commits from Claude, will delete it",
8484
);
8585
});
8686

@@ -90,14 +90,14 @@ describe("checkAndCommitOrDeleteBranch", () => {
9090
mockOctokit,
9191
"owner",
9292
"repo",
93-
"claude/issue-123-20240101_123456",
93+
"claude/issue-123-20240101-1234",
9494
"main",
9595
false,
9696
);
9797

9898
expect(result.shouldDeleteBranch).toBe(false);
9999
expect(result.branchLink).toBe(
100-
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101_123456)`,
100+
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101-1234)`,
101101
);
102102
expect(consoleLogSpy).not.toHaveBeenCalledWith(
103103
expect.stringContaining("has no commits"),
@@ -123,14 +123,14 @@ describe("checkAndCommitOrDeleteBranch", () => {
123123
mockOctokit,
124124
"owner",
125125
"repo",
126-
"claude/issue-123-20240101_123456",
126+
"claude/issue-123-20240101-1234",
127127
"main",
128128
false,
129129
);
130130

131131
expect(result.shouldDeleteBranch).toBe(false);
132132
expect(result.branchLink).toBe(
133-
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101_123456)`,
133+
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101-1234)`,
134134
);
135135
expect(consoleErrorSpy).toHaveBeenCalledWith(
136136
"Error comparing commits on Claude branch:",
@@ -146,15 +146,15 @@ describe("checkAndCommitOrDeleteBranch", () => {
146146
mockOctokit,
147147
"owner",
148148
"repo",
149-
"claude/issue-123-20240101_123456",
149+
"claude/issue-123-20240101-1234",
150150
"main",
151151
true, // commit signing enabled - will try to delete
152152
);
153153

154154
expect(result.shouldDeleteBranch).toBe(true);
155155
expect(result.branchLink).toBe("");
156156
expect(consoleErrorSpy).toHaveBeenCalledWith(
157-
"Failed to delete branch claude/issue-123-20240101_123456:",
157+
"Failed to delete branch claude/issue-123-20240101-1234:",
158158
deleteError,
159159
);
160160
});
@@ -170,18 +170,18 @@ describe("checkAndCommitOrDeleteBranch", () => {
170170
mockOctokit,
171171
"owner",
172172
"repo",
173-
"claude/issue-123-20240101_123456",
173+
"claude/issue-123-20240101-1234",
174174
"main",
175175
false,
176176
);
177177

178178
expect(result.shouldDeleteBranch).toBe(false);
179179
expect(result.branchLink).toBe("");
180180
expect(consoleLogSpy).toHaveBeenCalledWith(
181-
"Branch claude/issue-123-20240101_123456 does not exist remotely",
181+
"Branch claude/issue-123-20240101-1234 does not exist remotely",
182182
);
183183
expect(consoleLogSpy).toHaveBeenCalledWith(
184-
"Branch claude/issue-123-20240101_123456 does not exist remotely, no branch link will be added",
184+
"Branch claude/issue-123-20240101-1234 does not exist remotely, no branch link will be added",
185185
);
186186
});
187187
});

test/comment-logic.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ describe("updateCommentBody", () => {
103103
it("adds branch name with link to header when provided", () => {
104104
const input = {
105105
...baseInput,
106-
branchName: "claude/issue-123-20240101_120000",
106+
branchName: "claude/issue-123-20240101-1200",
107107
};
108108

109109
const result = updateCommentBody(input);
110110
expect(result).toContain(
111-
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)",
111+
"• [`claude/issue-123-20240101-1200`](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
112112
);
113113
});
114114

@@ -384,17 +384,17 @@ describe("updateCommentBody", () => {
384384
const input = {
385385
...baseInput,
386386
currentBody: "Claude Code is working… <img src='spinner.gif' />",
387-
branchName: "claude/pr-456-20240101_120000",
387+
branchName: "claude/pr-456-20240101-1200",
388388
prLink:
389-
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
389+
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101-1200)",
390390
triggerUsername: "jane-doe",
391391
};
392392

393393
const result = updateCommentBody(input);
394394

395395
// Should include the PR link in the formatted style
396396
expect(result).toContain(
397-
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
397+
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101-1200)",
398398
);
399399
expect(result).toContain("**Claude finished @jane-doe's task**");
400400
});
@@ -403,21 +403,21 @@ describe("updateCommentBody", () => {
403403
const input = {
404404
...baseInput,
405405
currentBody: "Claude Code is working…",
406-
branchName: "claude/issue-123-20240101_120000",
406+
branchName: "claude/issue-123-20240101-1200",
407407
branchLink:
408-
"\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)",
408+
"\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
409409
prLink:
410-
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
410+
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101-1200)",
411411
};
412412

413413
const result = updateCommentBody(input);
414414

415415
// Should include both links in formatted style
416416
expect(result).toContain(
417-
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)",
417+
"• [`claude/issue-123-20240101-1200`](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
418418
);
419419
expect(result).toContain(
420-
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
420+
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101-1200)",
421421
);
422422
});
423423

0 commit comments

Comments
 (0)