mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
3 Commits
v0.0.18
...
ashwin/mul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9e73dc5d4 | ||
|
|
41dd0aa695 | ||
|
|
55966a1dc0 |
11
README.md
11
README.md
@@ -347,8 +347,15 @@ Claude does **not** have access to execute arbitrary Bash commands by default. I
|
|||||||
```yaml
|
```yaml
|
||||||
- uses: anthropics/claude-code-action@beta
|
- uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
allowed_tools: "Bash(npm install),Bash(npm run test),Edit,Replace,NotebookEditCell"
|
allowed_tools: |
|
||||||
disallowed_tools: "TaskOutput,KillTask"
|
Bash(npm install)
|
||||||
|
Bash(npm run test)
|
||||||
|
Edit
|
||||||
|
Replace
|
||||||
|
NotebookEditCell
|
||||||
|
disallowed_tools: |
|
||||||
|
TaskOutput
|
||||||
|
KillTask
|
||||||
# ... other inputs
|
# ... other inputs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ runs:
|
|||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude-code
|
id: claude-code
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||||
uses: anthropics/claude-code-base-action@3933d9c3c25f2b027392a370be6f0bbd5989b271 # v0.0.18
|
uses: anthropics/claude-code-base-action@ebd8558e902b3db132e89863de49565fcb9aec46 # v0.0.19
|
||||||
with:
|
with:
|
||||||
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ ${
|
|||||||
}
|
}
|
||||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||||
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
||||||
|
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
||||||
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
||||||
${
|
${
|
||||||
(eventData.eventName === "issue_comment" ||
|
(eventData.eventName === "issue_comment" ||
|
||||||
@@ -503,12 +504,14 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
? `
|
? `
|
||||||
- Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files).
|
- Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files).
|
||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.`
|
- When pushing changes with this tool and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||||
|
- Use: "Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>"`
|
||||||
: `
|
: `
|
||||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||||
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.
|
- When pushing changes and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||||
|
- Use: "Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>"
|
||||||
${
|
${
|
||||||
eventData.claudeBranch
|
eventData.claudeBranch
|
||||||
? `- Provide a URL to create a PR manually in this format:
|
? `- Provide a URL to create a PR manually in this format:
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ async function run() {
|
|||||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||||
prNumber: context.entityNumber.toString(),
|
prNumber: context.entityNumber.toString(),
|
||||||
isPR: context.isPR,
|
isPR: context.isPR,
|
||||||
|
triggerUsername: context.actor,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 8: Setup branch
|
// Step 8: Setup branch
|
||||||
|
|||||||
@@ -104,3 +104,11 @@ export const ISSUE_QUERY = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const USER_QUERY = `
|
||||||
|
query($login: String!) {
|
||||||
|
user(login: $login) {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -52,14 +52,8 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
||||||
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
||||||
allowedTools: (process.env.ALLOWED_TOOLS ?? "")
|
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
|
||||||
.split(",")
|
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
|
||||||
.map((tool) => tool.trim())
|
|
||||||
.filter((tool) => tool.length > 0),
|
|
||||||
disallowedTools: (process.env.DISALLOWED_TOOLS ?? "")
|
|
||||||
.split(",")
|
|
||||||
.map((tool) => tool.trim())
|
|
||||||
.filter((tool) => tool.length > 0),
|
|
||||||
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
||||||
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
||||||
baseBranch: process.env.BASE_BRANCH,
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
@@ -116,6 +110,14 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseMultilineInput(s: string): string[] {
|
||||||
|
return s
|
||||||
|
.split(/,|[\n\r]+/)
|
||||||
|
.map((tool) => tool.replace(/#.+$/, ""))
|
||||||
|
.map((tool) => tool.trim())
|
||||||
|
.filter((tool) => tool.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
export function isIssuesEvent(
|
export function isIssuesEvent(
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import type { Octokits } from "../api/client";
|
import type { Octokits } from "../api/client";
|
||||||
import { ISSUE_QUERY, PR_QUERY } from "../api/queries/github";
|
import { ISSUE_QUERY, PR_QUERY, USER_QUERY } from "../api/queries/github";
|
||||||
import type {
|
import type {
|
||||||
GitHubComment,
|
GitHubComment,
|
||||||
GitHubFile,
|
GitHubFile,
|
||||||
@@ -18,6 +18,7 @@ type FetchDataParams = {
|
|||||||
repository: string;
|
repository: string;
|
||||||
prNumber: string;
|
prNumber: string;
|
||||||
isPR: boolean;
|
isPR: boolean;
|
||||||
|
triggerUsername?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GitHubFileWithSHA = GitHubFile & {
|
export type GitHubFileWithSHA = GitHubFile & {
|
||||||
@@ -31,6 +32,7 @@ export type FetchDataResult = {
|
|||||||
changedFilesWithSHA: GitHubFileWithSHA[];
|
changedFilesWithSHA: GitHubFileWithSHA[];
|
||||||
reviewData: { nodes: GitHubReview[] } | null;
|
reviewData: { nodes: GitHubReview[] } | null;
|
||||||
imageUrlMap: Map<string, string>;
|
imageUrlMap: Map<string, string>;
|
||||||
|
triggerDisplayName?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchGitHubData({
|
export async function fetchGitHubData({
|
||||||
@@ -38,6 +40,7 @@ export async function fetchGitHubData({
|
|||||||
repository,
|
repository,
|
||||||
prNumber,
|
prNumber,
|
||||||
isPR,
|
isPR,
|
||||||
|
triggerUsername,
|
||||||
}: FetchDataParams): Promise<FetchDataResult> {
|
}: FetchDataParams): Promise<FetchDataResult> {
|
||||||
const [owner, repo] = repository.split("/");
|
const [owner, repo] = repository.split("/");
|
||||||
if (!owner || !repo) {
|
if (!owner || !repo) {
|
||||||
@@ -191,6 +194,12 @@ export async function fetchGitHubData({
|
|||||||
allComments,
|
allComments,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fetch trigger user display name if username is provided
|
||||||
|
let triggerDisplayName: string | null | undefined;
|
||||||
|
if (triggerUsername) {
|
||||||
|
triggerDisplayName = await fetchUserDisplayName(octokits, triggerUsername);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contextData,
|
contextData,
|
||||||
comments,
|
comments,
|
||||||
@@ -198,5 +207,27 @@ export async function fetchGitHubData({
|
|||||||
changedFilesWithSHA,
|
changedFilesWithSHA,
|
||||||
reviewData,
|
reviewData,
|
||||||
imageUrlMap,
|
imageUrlMap,
|
||||||
|
triggerDisplayName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserQueryResponse = {
|
||||||
|
user: {
|
||||||
|
name: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchUserDisplayName(
|
||||||
|
octokits: Octokits,
|
||||||
|
login: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const result = await octokits.graphql<UserQueryResponse>(USER_QUERY, {
|
||||||
|
login,
|
||||||
|
});
|
||||||
|
return result.user.name;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch user display name for ${login}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Types for GitHub GraphQL query responses
|
// Types for GitHub GraphQL query responses
|
||||||
export type GitHubAuthor = {
|
export type GitHubAuthor = {
|
||||||
login: string;
|
login: string;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GitHubComment = {
|
export type GitHubComment = {
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ describe("generatePrompt", () => {
|
|||||||
|
|
||||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"Co-authored-by: johndoe <johndoe@users.noreply.github.com>",
|
'Use: "Co-authored-by: johndoe <johndoe@users.noreply.github.com>"',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
57
test/github/context.test.ts
Normal file
57
test/github/context.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { parseMultilineInput } from "../../src/github/context";
|
||||||
|
|
||||||
|
describe("parseMultilineInput", () => {
|
||||||
|
it("should parse a comma-separated string", () => {
|
||||||
|
const input = `Bash(bun install),Bash(bun test:*),Bash(bun typecheck)`;
|
||||||
|
const result = parseMultilineInput(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"Bash(bun install)",
|
||||||
|
"Bash(bun test:*)",
|
||||||
|
"Bash(bun typecheck)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiline string", () => {
|
||||||
|
const input = `Bash(bun install)
|
||||||
|
Bash(bun test:*)
|
||||||
|
Bash(bun typecheck)`;
|
||||||
|
const result = parseMultilineInput(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"Bash(bun install)",
|
||||||
|
"Bash(bun test:*)",
|
||||||
|
"Bash(bun typecheck)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse comma-separated multiline line", () => {
|
||||||
|
const input = `Bash(bun install),Bash(bun test:*)
|
||||||
|
Bash(bun typecheck)`;
|
||||||
|
const result = parseMultilineInput(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"Bash(bun install)",
|
||||||
|
"Bash(bun test:*)",
|
||||||
|
"Bash(bun typecheck)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore comments", () => {
|
||||||
|
const input = `Bash(bun install),
|
||||||
|
Bash(bun test:*) # For testing
|
||||||
|
# For type checking
|
||||||
|
Bash(bun typecheck)
|
||||||
|
`;
|
||||||
|
const result = parseMultilineInput(input);
|
||||||
|
expect(result).toEqual([
|
||||||
|
"Bash(bun install)",
|
||||||
|
"Bash(bun test:*)",
|
||||||
|
"Bash(bun typecheck)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse an empty string", () => {
|
||||||
|
const input = "";
|
||||||
|
const result = parseMultilineInput(input);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user