diff --git a/.claude/commands/label-issue.md b/.claude/commands/label-issue.md new file mode 100644 index 0000000..1344c5c --- /dev/null +++ b/.claude/commands/label-issue.md @@ -0,0 +1,60 @@ +--- +allowed-tools: Bash(gh label list:*),Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search:*) +description: Apply labels to GitHub issues +--- + +You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list. + +IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels. + +Issue Information: + +- REPO: ${{ github.repository }} +- ISSUE_NUMBER: ${{ github.event.issue.number }} + +TASK OVERVIEW: + +1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else. + +2. Next, use gh commands to get context about the issue: + + - Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details + - Use `gh search issues` to find similar issues that might provide context for proper categorization + - You have access to these Bash commands: + - Bash(gh label list:\*) - to get available labels + - Bash(gh issue view:\*) - to view issue details + - Bash(gh issue edit:\*) - to apply labels to the issue + - Bash(gh search:\*) - to search for similar issues + +3. Analyze the issue content, considering: + + - The issue title and description + - The type of issue (bug report, feature request, question, etc.) + - Technical areas mentioned + - Severity or priority indicators + - User impact + - Components affected + +4. Select appropriate labels from the available labels list provided above: + + - Choose labels that accurately reflect the issue's nature + - Be specific but comprehensive + - IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list + - Consider platform labels (android, ios) if applicable + - If you find similar issues using gh search, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. + +5. Apply the selected labels: + - Use `gh issue edit` to apply your selected labels + - DO NOT post any comments explaining your decision + - DO NOT communicate directly with users + - If no labels are clearly applicable, do not apply any labels + +IMPORTANT GUIDELINES: + +- Be thorough in your analysis +- Only select labels from the provided list above +- DO NOT post any comments to the issue +- Your ONLY action should be to apply labels using gh issue edit +- It's okay to not add any labels if none are clearly applicable + +--- diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index fe092a6..94817d5 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -18,92 +18,10 @@ jobs: with: fetch-depth: 0 - - name: Setup GitHub MCP Server - run: | - mkdir -p /tmp/mcp-config - cat > /tmp/mcp-config/mcp-servers.json << 'EOF' - { - "mcpServers": { - "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-efef8ae" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" - } - } - } - } - EOF - - - name: Create triage prompt - run: | - mkdir -p /tmp/claude-prompts - cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF' - You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list. - - IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels. - - Issue Information: - - REPO: ${{ github.repository }} - - ISSUE_NUMBER: ${{ github.event.issue.number }} - - TASK OVERVIEW: - - 1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else. - - 2. Next, use the GitHub tools to get context about the issue: - - You have access to these tools: - - mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels - - mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments - - mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting) - - mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues - - mcp__github__list_issues: Use this to understand patterns in how other issues are labeled - - Start by using mcp__github__get_issue to get the issue details - - 3. Analyze the issue content, considering: - - The issue title and description - - The type of issue (bug report, feature request, question, etc.) - - Technical areas mentioned - - Severity or priority indicators - - User impact - - Components affected - - 4. Select appropriate labels from the available labels list provided above: - - Choose labels that accurately reflect the issue's nature - - Be specific but comprehensive - - IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list - - 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. - - 5. Apply the selected labels: - - Use mcp__github__update_issue to apply your selected labels - - DO NOT post any comments explaining your decision - - DO NOT communicate directly with users - - If no labels are clearly applicable, do not apply any labels - - IMPORTANT GUIDELINES: - - Be thorough in your analysis - - Only select labels from the provided list above - - DO NOT post any comments to the issue - - Your ONLY action should be to apply labels using mcp__github__update_issue - - It's okay to not add any labels if none are clearly applicable - EOF - - name: Run Claude Code for Issue Triage - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@main with: - prompt: $(cat /tmp/claude-prompts/triage-prompt.txt) + prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues github_token: ${{ secrets.GITHUB_TOKEN }} - 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 - --mcp-config /tmp/mcp-config/mcp-servers.json - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/action.yml b/action.yml index 8e44688..f8ece92 100644 --- a/action.yml +++ b/action.yml @@ -27,6 +27,10 @@ inputs: description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots." required: false 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 prompt: @@ -148,6 +152,7 @@ runs: BRANCH_PREFIX: ${{ inputs.branch_prefix }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }} ALLOWED_BOTS: ${{ inputs.allowed_bots }} + ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }} GITHUB_RUN_ID: ${{ github.run_id }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} DEFAULT_WORKFLOW_TOKEN: ${{ github.token }} diff --git a/docs/security.md b/docs/security.md index 45ea4f2..e23429b 100644 --- a/docs/security.md +++ b/docs/security.md @@ -4,6 +4,11 @@ - **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 +- **⚠️ 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 - **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 diff --git a/docs/usage.md b/docs/usage.md index 58cc1fa..9ceadd7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -47,30 +47,31 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| `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\* | - | -| `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` | -| `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 | - | -| `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 | - | -| `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` | -| `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 | - | -| `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` | -| `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 | "" | -| `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 | "" | -| `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_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 | "" | +| Input | Description | Required | Default | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------- | +| `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\* | - | +| `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` | +| `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 | - | +| `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 | - | +| `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` | +| `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 | - | +| `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` | +| `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 | "" | +| `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 | "" | +| `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_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_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 diff --git a/examples/issue-triage.yml b/examples/issue-triage.yml index de5ce1a..a1f4b64 100644 --- a/examples/issue-triage.yml +++ b/examples/issue-triage.yml @@ -1,4 +1,5 @@ -name: Issue Triage +name: Claude Issue Triage +description: Run Claude Code for issue triage in GitHub Actions on: issues: types: [opened] @@ -17,60 +18,12 @@ jobs: with: fetch-depth: 0 - - name: Triage issue with Claude + - name: Run Claude Code for Issue Triage uses: anthropics/claude-code-action@v1 with: - prompt: | - You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list. - - IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels. - - Issue Information: - - REPO: ${{ github.repository }} - - ISSUE_NUMBER: ${{ github.event.issue.number }} - - TASK OVERVIEW: - - 1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else. - - 2. Next, use the GitHub tools to get context about the issue: - - You have access to these tools: - - mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels - - mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments - - mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting) - - mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues - - mcp__github__list_issues: Use this to understand patterns in how other issues are labeled - - Start by using mcp__github__get_issue to get the issue details - - 3. Analyze the issue content, considering: - - The issue title and description - - The type of issue (bug report, feature request, question, etc.) - - Technical areas mentioned - - Severity or priority indicators - - User impact - - Components affected - - 4. Select appropriate labels from the available labels list provided above: - - Choose labels that accurately reflect the issue's nature - - Be specific but comprehensive - - Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority) - - 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. - - 5. Apply the selected labels: - - Use mcp__github__update_issue to apply your selected labels - - DO NOT post any comments explaining your decision - - DO NOT communicate directly with users - - If no labels are clearly applicable, do not apply any labels - - IMPORTANT GUIDELINES: - - Be thorough in your analysis - - Only select labels from the provided list above - - DO NOT post any comments to the issue - - Your ONLY action should be to apply labels using mcp__github__update_issue - - It's okay to not add any labels if none are clearly applicable + # NOTE: /label-issue here requires a .claude/commands/label-issue.md file in your repo (see this repo's .claude directory for an example) + prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues github_token: ${{ secrets.GITHUB_TOKEN }} - 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" diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 84a31bc..af0ce9d 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -30,9 +30,13 @@ async function run() { // Step 3: Check write permissions (only for entity contexts) 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( octokit.rest, context, + context.inputs.allowedNonWriteUsers, + githubTokenProvided, ); if (!hasWritePermissions) { throw new Error( diff --git a/src/github/context.ts b/src/github/context.ts index de4dd08..56a9233 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -93,6 +93,7 @@ type BaseContext = { botId: string; botName: string; allowedBots: string; + allowedNonWriteUsers: string; trackProgress: boolean; }; }; @@ -147,6 +148,7 @@ export function parseGitHubContext(): GitHubContext { botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN, allowedBots: process.env.ALLOWED_BOTS ?? "", + allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "", trackProgress: process.env.TRACK_PROGRESS === "true", }, }; diff --git a/src/github/validation/permissions.ts b/src/github/validation/permissions.ts index e571e3a..731fcd4 100644 --- a/src/github/validation/permissions.ts +++ b/src/github/validation/permissions.ts @@ -6,17 +6,43 @@ import type { Octokit } from "@octokit/rest"; * Check if the actor has write permissions to the repository * @param octokit - The Octokit REST client * @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 */ export async function checkWritePermissions( octokit: Octokit, context: ParsedGitHubContext, + allowedNonWriteUsers?: string, + githubTokenProvided?: boolean, ): Promise { const { repository, actor } = context; try { 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) if (actor.endsWith("[bot]")) { core.info(`Actor is a GitHub App: ${actor}`); diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 48b54be..41879d6 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -35,6 +35,7 @@ describe("prepareMcpConfig", () => { botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, allowedBots: "", + allowedNonWriteUsers: "", trackProgress: false, }, }; diff --git a/test/mockContext.ts b/test/mockContext.ts index c375f18..73255e6 100644 --- a/test/mockContext.ts +++ b/test/mockContext.ts @@ -23,6 +23,7 @@ const defaultInputs = { botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, allowedBots: "", + allowedNonWriteUsers: "", trackProgress: false, }; diff --git a/test/permissions.test.ts b/test/permissions.test.ts index 6659d62..9aeb301 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -71,6 +71,7 @@ describe("checkWritePermissions", () => { botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, allowedBots: "", + allowedNonWriteUsers: "", trackProgress: false, }, }); @@ -175,4 +176,126 @@ describe("checkWritePermissions", () => { 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]", + ); + }); + }); });