mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
Merge branch 'main' of https://github.com/anthropics/claude-code-action into v1-dev
This commit is contained in:
@@ -10,7 +10,7 @@ Thank you for trying out the beta of our GitHub Action! This document outlines o
|
|||||||
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
|
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
|
||||||
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
|
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
|
||||||
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
|
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
|
||||||
- **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude
|
- ~**Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude~
|
||||||
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
|
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ inputs:
|
|||||||
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||||
required: false
|
required: false
|
||||||
default: "claude/"
|
default: "claude/"
|
||||||
|
allowed_bots:
|
||||||
|
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Claude Code configuration
|
# Claude Code configuration
|
||||||
prompt:
|
prompt:
|
||||||
@@ -110,6 +114,7 @@ runs:
|
|||||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||||
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
|
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
@@ -125,7 +130,7 @@ runs:
|
|||||||
echo "Base-action dependencies installed"
|
echo "Base-action dependencies installed"
|
||||||
cd -
|
cd -
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally
|
||||||
bun install -g @anthropic-ai/claude-code@1.0.70
|
bun install -g @anthropic-ai/claude-code@1.0.71
|
||||||
|
|
||||||
- name: Setup Network Restrictions
|
- name: Setup Network Restrictions
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Claude Code
|
- name: Install Claude Code
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bun install -g @anthropic-ai/claude-code@1.0.70
|
run: bun install -g @anthropic-ai/claude-code@1.0.71
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Access Control
|
## Access Control
|
||||||
|
|
||||||
- **Repository Access**: The action can only be triggered by users with write access to the repository
|
- **Repository Access**: The action can only be triggered by users with write access to the repository
|
||||||
- **No Bot Triggers**: GitHub Apps and bots cannot trigger this action
|
- **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots
|
||||||
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
|
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
|
||||||
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
||||||
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ jobs:
|
|||||||
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
|
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
|
||||||
# additional_permissions: |
|
# additional_permissions: |
|
||||||
# actions: read
|
# actions: read
|
||||||
|
# Optional: allow bot users to trigger the action
|
||||||
|
# allowed_bots: "dependabot[bot],renovate[bot]"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
@@ -76,6 +78,7 @@ 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` |
|
||||||
|
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
||||||
|
|
||||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type BaseContext = {
|
|||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
|
allowedBots: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ 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",
|
||||||
|
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,42 @@ export async function checkHumanActor(
|
|||||||
|
|
||||||
console.log(`Actor type: ${actorType}`);
|
console.log(`Actor type: ${actorType}`);
|
||||||
|
|
||||||
|
// Check bot permissions if actor is not a User
|
||||||
if (actorType !== "User") {
|
if (actorType !== "User") {
|
||||||
|
const allowedBots = githubContext.inputs.allowedBots;
|
||||||
|
|
||||||
|
// Check if all bots are allowed
|
||||||
|
if (allowedBots.trim() === "*") {
|
||||||
|
console.log(
|
||||||
|
`All bots are allowed, skipping human actor check for: ${githubContext.actor}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse allowed bots list
|
||||||
|
const allowedBotsList = allowedBots
|
||||||
|
.split(",")
|
||||||
|
.map((bot) =>
|
||||||
|
bot
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\[bot\]$/, ""),
|
||||||
|
)
|
||||||
|
.filter((bot) => bot.length > 0);
|
||||||
|
|
||||||
|
const botName = githubContext.actor.toLowerCase().replace(/\[bot\]$/, "");
|
||||||
|
|
||||||
|
// Check if specific bot is allowed
|
||||||
|
if (allowedBotsList.includes(botName)) {
|
||||||
|
console.log(
|
||||||
|
`Bot ${botName} is in allowed list, skipping human actor check`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot not allowed
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Workflow initiated by non-human actor: ${githubContext.actor} (type: ${actorType}).`,
|
`Workflow initiated by non-human actor: ${botName} (type: ${actorType}). Add bot to allowed_bots list or use '*' to allow all bots.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export async function checkWritePermissions(
|
|||||||
try {
|
try {
|
||||||
core.info(`Checking permissions for actor: ${actor}`);
|
core.info(`Checking permissions for actor: ${actor}`);
|
||||||
|
|
||||||
|
// Check if the actor is a GitHub App (bot user)
|
||||||
|
if (actor.endsWith("[bot]")) {
|
||||||
|
core.info(`Actor is a GitHub App: ${actor}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check permissions directly using the permission endpoint
|
// Check permissions directly using the permission endpoint
|
||||||
const response = await octokit.repos.getCollaboratorPermissionLevel({
|
const response = await octokit.repos.getCollaboratorPermissionLevel({
|
||||||
owner: repository.owner,
|
owner: repository.owner,
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ server.tool(
|
|||||||
),
|
),
|
||||||
line: z
|
line: z
|
||||||
.number()
|
.number()
|
||||||
|
.nonnegative()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Line number for single-line comments (required if startLine is not provided)",
|
"Line number for single-line comments (required if startLine is not provided)",
|
||||||
),
|
),
|
||||||
startLine: z
|
startLine: z
|
||||||
.number()
|
.number()
|
||||||
|
.nonnegative()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Start line for multi-line comments (use with line parameter for the end line)",
|
"Start line for multi-line comments (use with line parameter for the end line)",
|
||||||
|
|||||||
96
test/actor.test.ts
Normal file
96
test/actor.test.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { describe, test, expect } from "bun:test";
|
||||||
|
import { checkHumanActor } from "../src/github/validation/actor";
|
||||||
|
import type { Octokit } from "@octokit/rest";
|
||||||
|
import { createMockContext } from "./mockContext";
|
||||||
|
|
||||||
|
function createMockOctokit(userType: string): Octokit {
|
||||||
|
return {
|
||||||
|
users: {
|
||||||
|
getByUsername: async () => ({
|
||||||
|
data: {
|
||||||
|
type: userType,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} as unknown as Octokit;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("checkHumanActor", () => {
|
||||||
|
test("should pass for human actor", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("User");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "human-user";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error for bot actor when not allowed", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "";
|
||||||
|
|
||||||
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pass for bot actor when all bots allowed", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "*";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pass for specific bot when in allowed list", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "dependabot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pass for specific bot when in allowed list (without [bot])", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "dependabot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot,renovate";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error for bot not in allowed list", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "other-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
|
||||||
|
|
||||||
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error for bot not in allowed list (without [bot])", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "other-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot,renovate";
|
||||||
|
|
||||||
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,6 +31,7 @@ describe("prepareMcpConfig", () => {
|
|||||||
branchPrefix: "",
|
branchPrefix: "",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const defaultInputs = {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRepository = {
|
const defaultRepository = {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ describe("checkWritePermissions", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,6 +121,16 @@ describe("checkWritePermissions", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should return true for bot user", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("none");
|
||||||
|
const context = createContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(mockOctokit, context);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("should throw error when permission check fails", async () => {
|
test("should throw error when permission check fails", async () => {
|
||||||
const error = new Error("API error");
|
const error = new Error("API error");
|
||||||
const mockOctokit = {
|
const mockOctokit = {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -62,6 +63,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
@@ -273,6 +275,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -301,6 +304,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -329,6 +333,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user