Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
5b1f984703 tmp 2025-09-04 11:44:18 -07:00
github-actions[bot]
59a49d170c tmp 2025-09-04 11:28:30 -07:00
github-actions[bot]
cc47c8baef tmp 2025-09-04 11:11:56 -07:00
github-actions[bot]
222c780563 tmp 2025-09-04 11:09:57 -07:00
github-actions[bot]
574f279fb0 tmp 2025-09-04 10:43:51 -07:00
github-actions[bot]
3fa4ea2174 refactor: replace magic number with GITHUB_ACTIONS_BOT_ID constant
- Create new constants file at src/github/constants.ts with GITHUB_ACTIONS_BOT_ID and GITHUB_ACTIONS_BOT_LOGIN
- Replace all occurrences of magic number 41898282 with the constant
- Update imports across source and test files to use the new constants
- Add comment in action.yml referencing the constants file for the default bot_id value

This improves code maintainability by centralizing the GitHub Actions bot ID definition.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 10:36:22 -07:00
github-actions[bot]
532c5e257d feat: add bot_id input to handle GitHub App authentication errors
Adds a new optional bot_id input parameter that defaults to the github-actions[bot] ID (41898282). This resolves the "403 Resource not accessible by integration" error that occurs when using GitHub App installation tokens, which cannot access the /user endpoint.

Changes:
- Add bot_id input to action.yml with default value
- Update context parsing to include bot_id from environment
- Modify agent mode to use bot_id when available, avoiding API calls that fail with GitHub App tokens
- Add clear error handling for GitHub App token limitations
- Update documentation in usage.md and faq.md
- Fix test mocks to include bot_id field

This allows users to specify a custom bot user ID or use the default github-actions[bot] ID automatically, preventing 403 errors in automation workflows.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 09:12:52 -07:00
19 changed files with 51 additions and 368 deletions

View File

@@ -78,7 +78,7 @@ jobs:
4. Select appropriate labels from the available labels list provided above: 4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature - Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive - Be specific but comprehensive
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list - Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable - Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. - If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
@@ -97,12 +97,10 @@ jobs:
EOF EOF
- name: Run Claude Code for Issue Triage - name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@v1 uses: anthropics/claude-code-base-action@v1
with: with:
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt) prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
claude_args: | claude_args: |
--allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues --allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues
--mcp-config /tmp/mcp-config/mcp-servers.json --mcp-config /tmp/mcp-config/mcp-servers.json

View File

@@ -27,10 +27,6 @@ inputs:
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots." description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
required: false required: false
default: "" default: ""
allowed_non_write_users:
description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
required: false
default: ""
# Claude Code configuration # Claude Code configuration
prompt: prompt:
@@ -152,7 +148,6 @@ runs:
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 }} ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
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 }}
@@ -177,7 +172,7 @@ runs:
# Install Claude Code if no custom executable is provided # Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..." echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.108 curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"

View File

@@ -99,7 +99,7 @@ runs:
run: | run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..." echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.108 curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH # Add the directory containing the custom executable to PATH

View File

@@ -21,7 +21,7 @@ This action supports the following GitHub events ([learn more GitHub event trigg
- `issues` - When issues are opened or assigned - `issues` - When issues are opened or assigned
- `pull_request_review` - When PR reviews are submitted - `pull_request_review` - When PR reviews are submitted
- `pull_request_review_comment` - When comments are made on PR reviews - `pull_request_review_comment` - When comments are made on PR reviews
- `repository_dispatch` - Custom events triggered via API - `repository_dispatch` - Custom events triggered via API (coming soon)
- `workflow_dispatch` - Manual workflow triggers (coming soon) - `workflow_dispatch` - Manual workflow triggers (coming soon)
## Automated Documentation Updates ## Automated Documentation Updates

View File

@@ -14,19 +14,18 @@ This guide helps you migrate from Claude Code Action v0.x to v1.0. The new versi
The following inputs have been deprecated and replaced: The following inputs have been deprecated and replaced:
| Deprecated Input | Replacement | Notes | | Deprecated Input | Replacement | Notes |
| --------------------- | ------------------------------------ | --------------------------------------------- | | --------------------- | -------------------------------- | --------------------------------------------- |
| `mode` | Auto-detected | Action automatically chooses based on context | | `mode` | Auto-detected | Action automatically chooses based on context |
| `direct_prompt` | `prompt` | Direct drop-in replacement | | `direct_prompt` | `prompt` | Direct drop-in replacement |
| `override_prompt` | `prompt` | Use GitHub context variables instead | | `override_prompt` | `prompt` | Use GitHub context variables instead |
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments | | `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
| `max_turns` | `claude_args: --max-turns` | Use CLI format | | `max_turns` | `claude_args: --max-turns` | Use CLI format |
| `model` | `claude_args: --model` | Specify via CLI | | `model` | `claude_args: --model` | Specify via CLI |
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format | | `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format | | `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
| `claude_env` | `settings` with env object | Use settings JSON | | `claude_env` | `settings` with env object | Use settings JSON |
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments | | `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
| `timeout_minutes` | Use GitHub Actions `timeout-minutes` | Configure at job level instead of input level |
## Migration Examples ## Migration Examples
@@ -199,30 +198,6 @@ The `track_progress` input only works with these GitHub events:
} }
``` ```
### Timeout Configuration
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
timeout_minutes: 30
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
**After (v1.0):**
```yaml
jobs:
claude-task:
runs-on: ubuntu-latest
timeout-minutes: 30 # Moved to job level
steps:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
## How Mode Detection Works ## How Mode Detection Works
The action now automatically detects the appropriate mode: The action now automatically detects the appropriate mode:
@@ -337,7 +312,6 @@ You can also pass MCP configuration from a file:
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools` - [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
- [ ] Move `claude_env` to `settings` JSON format - [ ] Move `claude_env` to `settings` JSON format
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config` - [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
- [ ] Replace `timeout_minutes` with GitHub Actions `timeout-minutes` at job level
- [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode - [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode
- [ ] Test workflow in a non-production environment - [ ] Test workflow in a non-production environment

View File

@@ -4,11 +4,6 @@
- **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
- **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 - **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
- **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature:
- Only works when `github_token` is provided as input (not with GitHub App authentication)
- Accepts either a comma-separated list of specific usernames or `*` to allow all users
- **Should be used with extreme caution** as it bypasses the primary security mechanism of this action
- Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope
- **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

View File

@@ -47,31 +47,30 @@ jobs:
## Inputs ## Inputs
| Input | Description | Required | Default | | Input | Description | Required | Default |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------- | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | | `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\* | - | | `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | | `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | | `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` |
| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | | `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | | `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | | `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 | - | | `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | | `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` | | `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | 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 | - | | `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 | - | | `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` | | `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/` | | `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | 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 | "" | | `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` |
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | | `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | | `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` |
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | | `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" |
### Deprecated Inputs ### Deprecated Inputs

View File

@@ -10,6 +10,7 @@ jobs:
permissions: permissions:
contents: read contents: read
issues: write issues: write
id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -71,6 +72,5 @@ jobs:
- It's okay to not add any labels if none are clearly applicable - It's okay to not add any labels if none are clearly applicable
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
claude_args: | claude_args: |
--allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues" --allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"

View File

@@ -30,13 +30,9 @@ async function run() {
// Step 3: Check write permissions (only for entity contexts) // Step 3: Check write permissions (only for entity contexts)
if (isEntityContext(context)) { if (isEntityContext(context)) {
// Check if github_token was provided as input (not from app)
const githubTokenProvided = !!process.env.OVERRIDE_GITHUB_TOKEN;
const hasWritePermissions = await checkWritePermissions( const hasWritePermissions = await checkWritePermissions(
octokit.rest, octokit.rest,
context, context,
context.inputs.allowedNonWriteUsers,
githubTokenProvided,
); );
if (!hasWritePermissions) { if (!hasWritePermissions) {
throw new Error( throw new Error(

View File

@@ -26,20 +26,6 @@ export type WorkflowDispatchEvent = {
workflow: string; workflow: string;
}; };
export type RepositoryDispatchEvent = {
action: string;
client_payload?: Record<string, any>;
repository: {
name: string;
owner: {
login: string;
};
};
sender: {
login: string;
};
};
export type ScheduleEvent = { export type ScheduleEvent = {
action?: never; action?: never;
schedule?: string; schedule?: string;
@@ -62,7 +48,6 @@ const ENTITY_EVENT_NAMES = [
const AUTOMATION_EVENT_NAMES = [ const AUTOMATION_EVENT_NAMES = [
"workflow_dispatch", "workflow_dispatch",
"repository_dispatch",
"schedule", "schedule",
"workflow_run", "workflow_run",
] as const; ] as const;
@@ -93,7 +78,6 @@ type BaseContext = {
botId: string; botId: string;
botName: string; botName: string;
allowedBots: string; allowedBots: string;
allowedNonWriteUsers: string;
trackProgress: boolean; trackProgress: boolean;
}; };
}; };
@@ -111,14 +95,10 @@ export type ParsedGitHubContext = BaseContext & {
isPR: boolean; isPR: boolean;
}; };
// Context for automation events (workflow_dispatch, repository_dispatch, schedule, workflow_run) // Context for automation events (workflow_dispatch, schedule, workflow_run)
export type AutomationContext = BaseContext & { export type AutomationContext = BaseContext & {
eventName: AutomationEventName; eventName: AutomationEventName;
payload: payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
| WorkflowDispatchEvent
| RepositoryDispatchEvent
| ScheduleEvent
| WorkflowRunEvent;
}; };
// Union type for all contexts // Union type for all contexts
@@ -148,7 +128,6 @@ export function parseGitHubContext(): GitHubContext {
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN, botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
allowedBots: process.env.ALLOWED_BOTS ?? "", allowedBots: process.env.ALLOWED_BOTS ?? "",
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
trackProgress: process.env.TRACK_PROGRESS === "true", trackProgress: process.env.TRACK_PROGRESS === "true",
}, },
}; };
@@ -211,13 +190,6 @@ export function parseGitHubContext(): GitHubContext {
payload: context.payload as unknown as WorkflowDispatchEvent, payload: context.payload as unknown as WorkflowDispatchEvent,
}; };
} }
case "repository_dispatch": {
return {
...commonFields,
eventName: "repository_dispatch",
payload: context.payload as unknown as RepositoryDispatchEvent,
};
}
case "schedule": { case "schedule": {
return { return {
...commonFields, ...commonFields,

View File

@@ -6,43 +6,17 @@ import type { Octokit } from "@octokit/rest";
* Check if the actor has write permissions to the repository * Check if the actor has write permissions to the repository
* @param octokit - The Octokit REST client * @param octokit - The Octokit REST client
* @param context - The GitHub context * @param context - The GitHub context
* @param allowedNonWriteUsers - Comma-separated list of users allowed without write permissions, or '*' for all
* @param githubTokenProvided - Whether github_token was provided as input (not from app)
* @returns true if the actor has write permissions, false otherwise * @returns true if the actor has write permissions, false otherwise
*/ */
export async function checkWritePermissions( export async function checkWritePermissions(
octokit: Octokit, octokit: Octokit,
context: ParsedGitHubContext, context: ParsedGitHubContext,
allowedNonWriteUsers?: string,
githubTokenProvided?: boolean,
): Promise<boolean> { ): Promise<boolean> {
const { repository, actor } = context; const { repository, actor } = context;
try { try {
core.info(`Checking permissions for actor: ${actor}`); core.info(`Checking permissions for actor: ${actor}`);
// Check if we should bypass permission checks for this user
if (allowedNonWriteUsers && githubTokenProvided) {
const allowedUsers = allowedNonWriteUsers.trim();
if (allowedUsers === "*") {
core.warning(
`⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.`,
);
return true;
} else if (allowedUsers) {
const allowedUserList = allowedUsers
.split(",")
.map((u) => u.trim())
.filter((u) => u.length > 0);
if (allowedUserList.includes(actor)) {
core.warning(
`⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.`,
);
return true;
}
}
}
// Check if the actor is a GitHub App (bot user) // Check if the actor is a GitHub App (bot user)
if (actor.endsWith("[bot]")) { if (actor.endsWith("[bot]")) {
core.info(`Actor is a GitHub App: ${actor}`); core.info(`Actor is a GitHub App: ${actor}`);

View File

@@ -74,6 +74,10 @@ export async function prepareMcpConfig(
tool.startsWith("mcp__github_inline_comment__"), tool.startsWith("mcp__github_inline_comment__"),
); );
const hasGitHubCommentTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_comment__"),
);
const hasGitHubCITools = allowedToolsList.some((tool) => const hasGitHubCITools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_ci__"), tool.startsWith("mcp__github_ci__"),
); );
@@ -85,7 +89,7 @@ export async function prepareMcpConfig(
// Include comment server: // Include comment server:
// - Always in tag mode (for updating Claude comments) // - Always in tag mode (for updating Claude comments)
// - Only with explicit tools in agent mode // - Only with explicit tools in agent mode
const shouldIncludeCommentServer = !isAgentMode; const shouldIncludeCommentServer = !isAgentMode || hasGitHubCommentTools;
if (shouldIncludeCommentServer) { if (shouldIncludeCommentServer) {
baseMcpConfig.mcpServers.github_comment = { baseMcpConfig.mcpServers.github_comment = {

View File

@@ -44,10 +44,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
// Issue events // Issue events
if (isEntityContext(context) && isIssuesEvent(context)) { if (isEntityContext(context) && isIssuesEvent(context)) {
// If prompt is provided, use agent mode (same as PR events)
if (context.inputs.prompt) {
return "agent";
}
// Check for @claude mentions or labels/assignees // Check for @claude mentions or labels/assignees
if (checkContainsTrigger(context)) { if (checkContainsTrigger(context)) {
return "tag"; return "tag";

View File

@@ -35,7 +35,6 @@ describe("prepareMcpConfig", () => {
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}, },
}; };

View File

@@ -1,7 +1,6 @@
import type { import type {
ParsedGitHubContext, ParsedGitHubContext,
AutomationContext, AutomationContext,
RepositoryDispatchEvent,
} from "../src/github/context"; } from "../src/github/context";
import type { import type {
IssuesEvent, IssuesEvent,
@@ -23,7 +22,6 @@ const defaultInputs = {
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}; };
@@ -83,33 +81,6 @@ export const createMockAutomationContext = (
return { ...baseContext, ...overrides, inputs: mergedInputs }; return { ...baseContext, ...overrides, inputs: mergedInputs };
}; };
export const mockRepositoryDispatchContext: AutomationContext = {
runId: "1234567890",
eventName: "repository_dispatch",
eventAction: undefined,
repository: defaultRepository,
actor: "automation-user",
payload: {
action: "trigger-analysis",
client_payload: {
source: "issue-detective",
issue_number: 42,
repository_name: "test-owner/test-repo",
analysis_type: "bug-report",
},
repository: {
name: "test-repo",
owner: {
login: "test-owner",
},
},
sender: {
login: "automation-user",
},
} as RepositoryDispatchEvent,
inputs: defaultInputs,
};
export const mockIssueOpenedContext: ParsedGitHubContext = { export const mockIssueOpenedContext: ParsedGitHubContext = {
runId: "1234567890", runId: "1234567890",
eventName: "issues", eventName: "issues",

View File

@@ -76,11 +76,6 @@ describe("Agent Mode", () => {
}); });
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false); expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
const repositoryDispatchContext = createMockAutomationContext({
eventName: "repository_dispatch",
});
expect(agentMode.shouldTrigger(repositoryDispatchContext)).toBe(false);
// Should NOT trigger for entity events without prompt // Should NOT trigger for entity events without prompt
const entityEvents = [ const entityEvents = [
"issue_comment", "issue_comment",
@@ -97,7 +92,6 @@ describe("Agent Mode", () => {
// Should trigger for ANY event when prompt is provided // Should trigger for ANY event when prompt is provided
const allEvents = [ const allEvents = [
"workflow_dispatch", "workflow_dispatch",
"repository_dispatch",
"schedule", "schedule",
"issue_comment", "issue_comment",
"pull_request", "pull_request",
@@ -107,9 +101,7 @@ describe("Agent Mode", () => {
allEvents.forEach((eventName) => { allEvents.forEach((eventName) => {
const contextWithPrompt = const contextWithPrompt =
eventName === "workflow_dispatch" || eventName === "workflow_dispatch" || eventName === "schedule"
eventName === "repository_dispatch" ||
eventName === "schedule"
? createMockAutomationContext({ ? createMockAutomationContext({
eventName, eventName,
inputs: { prompt: "Do something" }, inputs: { prompt: "Do something" },

View File

@@ -2,11 +2,7 @@ import { describe, test, expect } from "bun:test";
import { getMode, isValidMode } from "../../src/modes/registry"; import { getMode, isValidMode } from "../../src/modes/registry";
import { agentMode } from "../../src/modes/agent"; import { agentMode } from "../../src/modes/agent";
import { tagMode } from "../../src/modes/tag"; import { tagMode } from "../../src/modes/tag";
import { import { createMockContext, createMockAutomationContext } from "../mockContext";
createMockContext,
createMockAutomationContext,
mockRepositoryDispatchContext,
} from "../mockContext";
describe("Mode Registry", () => { describe("Mode Registry", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
@@ -54,34 +50,6 @@ describe("Mode Registry", () => {
expect(mode.name).toBe("agent"); expect(mode.name).toBe("agent");
}); });
test("getMode auto-detects agent for repository_dispatch event", () => {
const mode = getMode(mockRepositoryDispatchContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode auto-detects agent for repository_dispatch with client_payload", () => {
const contextWithPayload = createMockAutomationContext({
eventName: "repository_dispatch",
payload: {
action: "trigger-analysis",
client_payload: {
source: "external-system",
metadata: { priority: "high" },
},
repository: {
name: "test-repo",
owner: { login: "test-owner" },
},
sender: { login: "automation-user" },
},
});
const mode = getMode(contextWithPayload);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
// Removed test - legacy mode names no longer supported in v1.0 // Removed test - legacy mode names no longer supported in v1.0
test("getMode auto-detects agent mode for PR opened", () => { test("getMode auto-detects agent mode for PR opened", () => {

View File

@@ -71,7 +71,6 @@ describe("checkWritePermissions", () => {
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}, },
}); });
@@ -176,126 +175,4 @@ describe("checkWritePermissions", () => {
username: "test-user", username: "test-user",
}); });
}); });
describe("allowed_non_write_users bypass", () => {
test("should bypass permission check for specific user when github_token provided", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"test-user,other-user",
true,
);
expect(result).toBe(true);
expect(coreWarningSpy).toHaveBeenCalledWith(
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.",
);
});
test("should bypass permission check for all users with wildcard", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"*",
true,
);
expect(result).toBe(true);
expect(coreWarningSpy).toHaveBeenCalledWith(
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.",
);
});
test("should NOT bypass permission check when user not in allowed list", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"other-user,another-user",
true,
);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should NOT bypass permission check when github_token not provided", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"test-user",
false,
);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should NOT bypass permission check when allowed_non_write_users is empty", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"",
true,
);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should handle whitespace in allowed_non_write_users list", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
" test-user , other-user ",
true,
);
expect(result).toBe(true);
expect(coreWarningSpy).toHaveBeenCalledWith(
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.",
);
});
test("should bypass for bot users even when allowed_non_write_users is set", async () => {
const mockOctokit = createMockOctokit("none");
const context = createContext();
context.actor = "test-bot[bot]";
const result = await checkWritePermissions(
mockOctokit,
context,
"some-user",
true,
);
expect(result).toBe(true);
expect(coreInfoSpy).toHaveBeenCalledWith(
"Actor is a GitHub App: test-bot[bot]",
);
});
});
}); });

View File

@@ -113,33 +113,6 @@ describe("detectMode with enhanced routing", () => {
expect(detectMode(context)).toBe("agent"); expect(detectMode(context)).toBe("agent");
}); });
it("should use agent mode for issues with explicit prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test issue" } } as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, prompt: "Analyze this issue" },
};
expect(detectMode(context)).toBe("agent");
});
it("should use tag mode for issues with @claude mention and no prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "@claude help" } } as any,
entityNumber: 1,
isPR: false,
};
expect(detectMode(context)).toBe("tag");
});
}); });
describe("Comment Events (unchanged behavior)", () => { describe("Comment Events (unchanged behavior)", () => {