Merge pull request 'chore: merge 7 upstream commits from anthropics/claude-code-action' (#54) from chore/upstream-sync-20260316 into main
Reviewed-on: https://git.wxs.ro/actions/claude-code-action/pulls/54
This commit is contained in:
commit
c8a67b6d5e
25 files changed
+428
-57
No files matched your search
+18
-1
@@ -28,7 +28,7 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
allowed_bots:
|
allowed_bots:
|
||||||
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. WARNING: On public repos with '*', external Apps may be able to invoke this action with prompts they control. See docs/security.md."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
allowed_non_write_users:
|
allowed_non_write_users:
|
||||||
@@ -97,6 +97,10 @@ inputs:
|
|||||||
description: "Use just one comment to deliver issue/PR comments"
|
description: "Use just one comment to deliver issue/PR comments"
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
|
classify_inline_comments:
|
||||||
|
description: "Buffer inline comments without confirmed=true and classify them (real review vs test/probe) before posting after the session ends. Set to 'false' to post all inline comments immediately (pre-buffering behavior)."
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
use_commit_signing:
|
use_commit_signing:
|
||||||
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
||||||
required: false
|
required: false
|
||||||
@@ -252,6 +256,7 @@ runs:
|
|||||||
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
|
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
|
||||||
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 }}
|
||||||
|
CLASSIFY_INLINE_COMMENTS: ${{ inputs.classify_inline_comments }}
|
||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||||
SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }}
|
SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }}
|
||||||
@@ -334,6 +339,18 @@ runs:
|
|||||||
run: |
|
run: |
|
||||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts
|
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts
|
||||||
|
|
||||||
|
- name: Post buffered inline comments
|
||||||
|
if: always() && inputs.classify_inline_comments != 'false'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.run.outputs.github_token || inputs.github_token || github.token }}
|
||||||
|
REPO_OWNER: ${{ github.event.repository.owner.login }}
|
||||||
|
REPO_NAME: ${{ github.event.repository.name }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||||
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||||
|
run: |
|
||||||
|
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/post-buffered-inline-comments.ts
|
||||||
|
|
||||||
- name: Revoke app token
|
- name: Revoke app token
|
||||||
if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' && steps.detect-platform.outputs.platform != 'forgejo'
|
if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' && steps.detect-platform.outputs.platform != 'forgejo'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ runs:
|
|||||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||||
CLAUDE_CODE_VERSION="2.1.71"
|
CLAUDE_CODE_VERSION="2.1.76"
|
||||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "Installation attempt $attempt..."
|
echo "Installation attempt $attempt..."
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"name": "@anthropic-ai/claude-code-base-action",
|
"name": "@anthropic-ai/claude-code-base-action",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.71",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.76",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.71", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pIsQJnM7Y+cJHL7aFY6SCCW3FIni218gVEpPqG8XGowfYxboFNBbNssWiUNRwthT8bp9jypcX7q5kx0Xsw14xg=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.71",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.76",
|
||||||
"shell-quote": "^1.8.3"
|
"shell-quote": "^1.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.71",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.76",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.71", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pIsQJnM7Y+cJHL7aFY6SCCW3FIni218gVEpPqG8XGowfYxboFNBbNssWiUNRwthT8bp9jypcX7q5kx0Xsw14xg=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
- **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
|
||||||
|
- **⚠️ Allowed bots are not checked for repository permissions.** A bot that matches an entry does **not** need to be installed on your repository or have write access. On a **public repository**, external parties — including GitHub Apps created by anyone — may be able to trigger workflow events such as opening issues, commenting, or reviewing pull requests. If your workflow listens on those events and `allowed_bots` is set to `'*'`, any such App can invoke this action with a prompt it controls.
|
||||||
|
- Prefer an explicit list over `'*'`
|
||||||
|
- Only list App names you trust
|
||||||
|
- If you need `'*'`, scope workflow `permissions:` to the minimum required
|
||||||
- **⚠️ 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:
|
- **⚠️ 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)
|
- 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
|
- Accepts either a comma-separated list of specific usernames or `*` to allow all users
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ jobs:
|
|||||||
Note: The PR branch is already checked out in the current working directory.
|
Note: The PR branch is already checked out in the current working directory.
|
||||||
|
|
||||||
Use `gh pr comment` for top-level feedback.
|
Use `gh pr comment` for top-level feedback.
|
||||||
Use `mcp__github_inline_comment__create_inline_comment` to highlight specific code issues.
|
Use `mcp__github_inline_comment__create_inline_comment` (with `confirmed: true`) to highlight specific code issues.
|
||||||
Only post GitHub comments - don't submit review text as messages.
|
Only post GitHub comments - don't submit review text as messages.
|
||||||
|
|
||||||
claude_args: |
|
claude_args: |
|
||||||
@@ -585,7 +585,7 @@ prompt: |
|
|||||||
### Common Tool Permissions
|
### Common Tool Permissions
|
||||||
|
|
||||||
- **PR Comments**: `Bash(gh pr comment:*)`
|
- **PR Comments**: `Bash(gh pr comment:*)`
|
||||||
- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment`
|
- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment` — pass `confirmed: true` to post immediately. When omitted, the comment is buffered and classified after the session ends (real review comments post, test/probe comments are filtered). This prevents subagent test comments from reaching PRs. To disable classification entirely, set `classify_inline_comments: 'false'` on the action.
|
||||||
- **File Operations**: `Read,Write,Edit`
|
- **File Operations**: `Read,Write,Edit`
|
||||||
- **Git Operations**: `Bash(git:*)`
|
- **Git Operations**: `Bash(git:*)`
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -53,7 +53,7 @@ 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 | - |
|
||||||
@@ -62,6 +62,7 @@ jobs:
|
|||||||
| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" |
|
| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (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` |
|
||||||
|
| `classify_inline_comments` | Buffer inline comments without `confirmed: true` and classify them (real review vs test/probe) via Haiku before posting after the session ends. Prevents subagent test comments. Set `'false'` to post all inline comments immediately | No | `true` |
|
||||||
| `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` |
|
||||||
@@ -75,7 +76,7 @@ jobs:
|
|||||||
| `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" |
|
| `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" |
|
||||||
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` |
|
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` |
|
||||||
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | No | `claude[bot]` |
|
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | 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. **⚠️ On public repos with `'*'`, external Apps may be able to invoke this action.** See [Security](./security.md) | 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 | "" |
|
| `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 | "" |
|
||||||
| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" |
|
| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" |
|
||||||
| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
|
| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.71",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.76",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
|
|||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Wrapper around `git push` that only allows `origin <ref>` with no flags.
|
||||||
|
# Defends against --receive-pack / --exec RCE and arbitrary-remote exfiltration
|
||||||
|
# (H1 #3556799). `git push:*` in allowedTools permits `git push --receive-pack='sh -c ...' ext::sh`
|
||||||
|
# which runs arbitrary shell on the Actions runner. This wrapper closes that.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# git-push.sh origin HEAD
|
||||||
|
# git-push.sh origin claude/issue-123-20260304
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
echo "Error: exactly two arguments required: origin <ref>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == -* ]]; then
|
||||||
|
echo "Error: flags are not allowed (got: $arg)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$1" != "origin" ]]; then
|
||||||
|
echo "Error: remote must be 'origin' (got: $1)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REF="$2"
|
||||||
|
if [[ "$REF" != "HEAD" ]] && ! git check-ref-format --branch "$REF" >/dev/null 2>&1; then
|
||||||
|
echo "Error: invalid ref: $REF" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec git push origin "$REF"
|
||||||
+10
-14
@@ -26,19 +26,15 @@ import { extractUserRequest } from "../utils/extract-user-request";
|
|||||||
import { parseToolsInput } from "../utils/parse-tools-input";
|
import { parseToolsInput } from "../utils/parse-tools-input";
|
||||||
export type { CommonFields, PreparedContext } from "./types";
|
export type { CommonFields, PreparedContext } from "./types";
|
||||||
|
|
||||||
|
const GIT_PUSH_WRAPPER = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`;
|
||||||
|
|
||||||
/** Filename for the user request file, read by the SDK runner */
|
/** Filename for the user request file, read by the SDK runner */
|
||||||
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
||||||
|
|
||||||
// Tag mode defaults - these tools are needed for tag mode to function
|
// Tag mode defaults - these tools are needed for tag mode to function.
|
||||||
const BASE_ALLOWED_TOOLS = [
|
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode
|
||||||
"Edit",
|
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside it.
|
||||||
"MultiEdit",
|
const BASE_ALLOWED_TOOLS = ["Glob", "Grep", "LS", "Read"];
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"LS",
|
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function buildAllowedToolsString(
|
export function buildAllowedToolsString(
|
||||||
customAllowedTools?: string[],
|
customAllowedTools?: string[],
|
||||||
@@ -62,7 +58,7 @@ export function buildAllowedToolsString(
|
|||||||
baseTools.push(
|
baseTools.push(
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
"Bash(git push:*)",
|
`Bash(${GIT_PUSH_WRAPPER}:*)`,
|
||||||
"Bash(git status:*)",
|
"Bash(git status:*)",
|
||||||
"Bash(git diff:*)",
|
"Bash(git diff:*)",
|
||||||
"Bash(git log:*)",
|
"Bash(git log:*)",
|
||||||
@@ -443,7 +439,7 @@ function getCommitInstructions(
|
|||||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
- Push to the remote: Bash(git push origin HEAD)`;
|
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin HEAD)`;
|
||||||
} else {
|
} else {
|
||||||
const branchName = eventData.claudeBranch || eventData.baseBranch;
|
const branchName = eventData.claudeBranch || eventData.baseBranch;
|
||||||
return `
|
return `
|
||||||
@@ -457,7 +453,7 @@ function getCommitInstructions(
|
|||||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
- Push to the remote: Bash(git push origin ${branchName})`;
|
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin ${branchName})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -840,7 +836,7 @@ ${
|
|||||||
: `- Use git commands via the Bash tool for version control (remember that you have access to these git commands):
|
: `- Use git commands via the Bash tool for version control (remember that you have access to these git commands):
|
||||||
- Stage files: Bash(git add <files>)
|
- Stage files: Bash(git add <files>)
|
||||||
- Commit changes: Bash(git commit -m "<message>")
|
- Commit changes: Bash(git commit -m "<message>")
|
||||||
- Push to remote: Bash(git push origin <branch>) (NEVER force push)
|
- Push to remote: Bash(${GIT_PUSH_WRAPPER} origin <branch>)
|
||||||
- Delete files: Bash(git rm <files>) followed by commit and push
|
- Delete files: Bash(git rm <files>) followed by commit and push
|
||||||
- Check status: Bash(git status)
|
- Check status: Bash(git status)
|
||||||
- View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}`
|
- View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}`
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function collectActionInputsPresence(): string {
|
|||||||
github_token: "",
|
github_token: "",
|
||||||
max_turns: "",
|
max_turns: "",
|
||||||
use_sticky_comment: "false",
|
use_sticky_comment: "false",
|
||||||
|
classify_inline_comments: "true",
|
||||||
use_commit_signing: "false",
|
use_commit_signing: "false",
|
||||||
ssh_signing_key: "",
|
ssh_signing_key: "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Reads buffered inline-comment calls from /tmp/inline-comments-buffer.jsonl,
|
||||||
|
* classifies each as "real review" vs "test/probe" using Haiku, and posts
|
||||||
|
* only the real ones. Calls with confirmed=false are never posted.
|
||||||
|
*
|
||||||
|
* If the Anthropic API is unavailable (Bedrock/Vertex users without a direct
|
||||||
|
* key), falls back to posting everything with confirmed !== false. This
|
||||||
|
* preserves backward compatibility — before this change, all unconfirmed
|
||||||
|
* calls posted immediately.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { createOctokit } from "../github/api/client";
|
||||||
|
|
||||||
|
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
|
||||||
|
|
||||||
|
type BufferedComment = {
|
||||||
|
ts: string;
|
||||||
|
path: string;
|
||||||
|
line?: number;
|
||||||
|
startLine?: number;
|
||||||
|
side?: "LEFT" | "RIGHT";
|
||||||
|
commit_id?: string;
|
||||||
|
body: string;
|
||||||
|
confirmed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLASSIFICATION_PROMPT = `You are classifying PR inline comments as either REAL code review feedback or TEST/PROBE calls.
|
||||||
|
|
||||||
|
A TEST/PROBE call is when an automated agent is checking whether a commenting tool works. These typically:
|
||||||
|
- Start with phrases like "Test comment", "Testing if", "Can I", "Does this work", "Checking if"
|
||||||
|
- Have generic/placeholder content not specific to any code
|
||||||
|
- Exist to verify tool functionality, not to provide review feedback
|
||||||
|
|
||||||
|
A REAL review comment:
|
||||||
|
- Discusses specific code, logic, bugs, or style
|
||||||
|
- Provides actionable feedback for the PR author
|
||||||
|
- References concrete aspects of the change
|
||||||
|
|
||||||
|
For each numbered comment body below, respond with ONLY a JSON array of booleans where true = REAL review comment, false = test/probe. No other text.
|
||||||
|
|
||||||
|
Comments:
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function classifyComments(bodies: string[]): Promise<boolean[] | null> {
|
||||||
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
console.log(
|
||||||
|
"ANTHROPIC_API_KEY not set — skipping classification, posting all unconfirmed comments",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt =
|
||||||
|
CLASSIFICATION_PROMPT +
|
||||||
|
bodies.map((b, i) => `${i + 1}. ${JSON.stringify(b)}`).join("\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "claude-haiku-4-5",
|
||||||
|
max_tokens: 1024,
|
||||||
|
messages: [{ role: "user", content: prompt }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.log(
|
||||||
|
`Classification API returned ${res.status} — posting all unconfirmed comments`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
content: { type: string; text: string }[];
|
||||||
|
};
|
||||||
|
const text = data.content.find((c) => c.type === "text")?.text ?? "";
|
||||||
|
const match = text.match(/\[[\s\S]*\]/);
|
||||||
|
if (!match) {
|
||||||
|
console.log(
|
||||||
|
"Could not parse classification response — posting all unconfirmed comments",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(match[0]);
|
||||||
|
if (
|
||||||
|
!Array.isArray(parsed) ||
|
||||||
|
parsed.length !== bodies.length ||
|
||||||
|
!parsed.every((v) => typeof v === "boolean")
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"Classification response shape mismatch — posting all unconfirmed comments",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
`Classification failed (${e instanceof Error ? e.message : String(e)}) — posting all unconfirmed comments`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postComment(
|
||||||
|
octokit: ReturnType<typeof createOctokit>["rest"],
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
pull_number: number,
|
||||||
|
headSha: string,
|
||||||
|
c: BufferedComment,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const params: Parameters<typeof octokit.rest.pulls.createReviewComment>[0] = {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number,
|
||||||
|
body: c.body,
|
||||||
|
path: c.path,
|
||||||
|
side: c.side || "RIGHT",
|
||||||
|
commit_id: c.commit_id || headSha,
|
||||||
|
};
|
||||||
|
if (c.startLine) {
|
||||||
|
params.start_line = c.startLine;
|
||||||
|
params.start_side = c.side || "RIGHT";
|
||||||
|
params.line = c.line;
|
||||||
|
} else {
|
||||||
|
params.line = c.line;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await octokit.rest.pulls.createReviewComment(params);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
` failed ${c.path}:${c.line}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
raw = readFileSync(BUFFER_PATH, "utf8");
|
||||||
|
} catch {
|
||||||
|
console.log("No buffered inline comments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments: BufferedComment[] = raw
|
||||||
|
.split("\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap((line) => {
|
||||||
|
try {
|
||||||
|
return [JSON.parse(line) as BufferedComment];
|
||||||
|
} catch {
|
||||||
|
console.log(`::warning::Skipping malformed JSONL line: ${line.slice(0, 120)}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comments.length === 0) {
|
||||||
|
console.log("No buffered inline comments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${comments.length} buffered inline comment(s)`);
|
||||||
|
|
||||||
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
|
const owner = process.env.REPO_OWNER;
|
||||||
|
const repo = process.env.REPO_NAME;
|
||||||
|
const prNumber = process.env.PR_NUMBER;
|
||||||
|
|
||||||
|
if (!githubToken || !owner || !repo || !prNumber) {
|
||||||
|
console.log(
|
||||||
|
"::warning::Missing GITHUB_TOKEN/REPO_OWNER/REPO_NAME/PR_NUMBER — cannot post buffered comments",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partition: confirmed=false are never posted; the rest are candidates
|
||||||
|
const neverPost = comments.filter((c) => c.confirmed === false);
|
||||||
|
const candidates = comments.filter((c) => c.confirmed !== false);
|
||||||
|
|
||||||
|
if (neverPost.length > 0) {
|
||||||
|
console.log(` ${neverPost.length} with confirmed=false — not posting`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classify candidates
|
||||||
|
const verdicts = await classifyComments(candidates.map((c) => c.body));
|
||||||
|
const toPost =
|
||||||
|
verdicts === null
|
||||||
|
? candidates
|
||||||
|
: candidates.filter((_, i) => verdicts[i] === true);
|
||||||
|
const filtered =
|
||||||
|
verdicts === null ? [] : candidates.filter((_, i) => verdicts[i] === false);
|
||||||
|
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`::warning::${filtered.length} buffered comment(s) classified as test/probe — NOT posted:`,
|
||||||
|
);
|
||||||
|
for (const c of filtered) {
|
||||||
|
console.log(` [${c.path}:${c.line}] ${c.body.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toPost.length === 0) {
|
||||||
|
console.log("No real comments to post");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const octokit = createOctokit(githubToken).rest;
|
||||||
|
const pull_number = parseInt(prNumber, 10);
|
||||||
|
const pr = await octokit.pulls.get({ owner, repo, pull_number });
|
||||||
|
const headSha = pr.data.head.sha;
|
||||||
|
|
||||||
|
console.log(`Posting ${toPost.length} classified-as-real comment(s)`);
|
||||||
|
let posted = 0;
|
||||||
|
for (const c of toPost) {
|
||||||
|
if (await postComment(octokit, owner, repo, pull_number, headSha, c)) {
|
||||||
|
console.log(` posted ${c.path}:${c.line}`);
|
||||||
|
posted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Posted ${posted}/${toPost.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error("post-buffered-inline-comments failed:", e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -54,7 +54,7 @@ async function installClaudeCode(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const claudeCodeVersion = "2.1.71";
|
const claudeCodeVersion = "2.1.76";
|
||||||
console.log(`Installing Claude Code v${claudeCodeVersion}...`);
|
console.log(`Installing Claude Code v${claudeCodeVersion}...`);
|
||||||
|
|
||||||
// On musl/Alpine systems, the native binary may require a newer musl than
|
// On musl/Alpine systems, the native binary may require a newer musl than
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ type BaseContext = {
|
|||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
branchNameTemplate?: string;
|
branchNameTemplate?: string;
|
||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
|
classifyInlineComments: boolean;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
sshSigningKey: string;
|
sshSigningKey: string;
|
||||||
botId: string;
|
botId: string;
|
||||||
@@ -178,6 +179,7 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||||
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
||||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||||
|
classifyInlineComments: process.env.CLASSIFY_INLINE_COMMENTS !== "false",
|
||||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||||
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
||||||
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { appendFileSync } from "fs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
import { sanitizeContent } from "../github/utils/sanitizer";
|
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||||
@@ -10,6 +11,13 @@ const REPO_OWNER = process.env.REPO_OWNER;
|
|||||||
const REPO_NAME = process.env.REPO_NAME;
|
const REPO_NAME = process.env.REPO_NAME;
|
||||||
const PR_NUMBER = process.env.PR_NUMBER;
|
const PR_NUMBER = process.env.PR_NUMBER;
|
||||||
|
|
||||||
|
// Calls without confirmed=true are buffered here instead of posted. This
|
||||||
|
// prevents subagents from posting test/probe comments when they inherit this
|
||||||
|
// tool and probe it after hitting unrelated errors. The action's post-step
|
||||||
|
// reports the buffer count for diagnostics.
|
||||||
|
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
|
||||||
|
const CLASSIFY_ENABLED = process.env.CLASSIFY_INLINE_COMMENTS !== "false";
|
||||||
|
|
||||||
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) {
|
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) {
|
||||||
console.error(
|
console.error(
|
||||||
"Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required",
|
"Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required",
|
||||||
@@ -67,8 +75,17 @@ server.tool(
|
|||||||
.describe(
|
.describe(
|
||||||
"Specific commit SHA to comment on (defaults to latest commit)",
|
"Specific commit SHA to comment on (defaults to latest commit)",
|
||||||
),
|
),
|
||||||
|
confirmed: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Set true to post immediately. When omitted, the call is buffered " +
|
||||||
|
"and classified after the session completes — real review comments " +
|
||||||
|
"post, test/probe comments are dropped. Set false to buffer and " +
|
||||||
|
"never post. Only set true when posting final review comments.",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
async ({ path, body, line, startLine, side, commit_id }) => {
|
async ({ path, body, line, startLine, side, commit_id, confirmed }) => {
|
||||||
try {
|
try {
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
@@ -80,8 +97,6 @@ server.tool(
|
|||||||
const repo = REPO_NAME;
|
const repo = REPO_NAME;
|
||||||
const pull_number = parseInt(PR_NUMBER, 10);
|
const pull_number = parseInt(PR_NUMBER, 10);
|
||||||
|
|
||||||
const octokit = createOctokit(githubToken).rest;
|
|
||||||
|
|
||||||
// Sanitize the comment body to remove any potential GitHub tokens
|
// Sanitize the comment body to remove any potential GitHub tokens
|
||||||
const sanitizedBody = sanitizeContent(body);
|
const sanitizedBody = sanitizeContent(body);
|
||||||
|
|
||||||
@@ -92,10 +107,49 @@ server.tool(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CLASSIFY_ENABLED && confirmed !== true) {
|
||||||
|
appendFileSync(
|
||||||
|
BUFFER_PATH,
|
||||||
|
JSON.stringify({
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
startLine,
|
||||||
|
side,
|
||||||
|
commit_id,
|
||||||
|
body: sanitizedBody,
|
||||||
|
confirmed,
|
||||||
|
}) + "\n",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
buffered: true,
|
||||||
|
message:
|
||||||
|
"Comment buffered. It will be classified and posted after " +
|
||||||
|
"this session completes (real review comments post, " +
|
||||||
|
"test/probe comments are dropped). Set confirmed=true to " +
|
||||||
|
"post immediately. If you are testing whether this tool " +
|
||||||
|
"works: it works — no need to test further.",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// If only line is provided, it's a single-line comment
|
// If only line is provided, it's a single-line comment
|
||||||
// If both startLine and line are provided, it's a multi-line comment
|
// If both startLine and line are provided, it's a multi-line comment
|
||||||
const isSingleLine = !startLine;
|
const isSingleLine = !startLine;
|
||||||
|
|
||||||
|
const octokit = createOctokit(githubToken).rest;
|
||||||
|
|
||||||
const pr = await octokit.pulls.get({
|
const pr = await octokit.pulls.get({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ export async function prepareMcpConfig(
|
|||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
PR_NUMBER: context.entityNumber?.toString() || "",
|
PR_NUMBER: context.entityNumber?.toString() || "",
|
||||||
GITHUB_API_URL: GITHUB_API_URL,
|
GITHUB_API_URL: GITHUB_API_URL,
|
||||||
|
CLASSIFY_INLINE_COMMENTS: context.inputs.classifyInlineComments
|
||||||
|
? "true"
|
||||||
|
: "false",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -128,16 +128,17 @@ export async function prepareTagMode({
|
|||||||
// Merge user-provided allowed_tools input (fixes upstream #181, #287, #724)
|
// Merge user-provided allowed_tools input (fixes upstream #181, #287, #724)
|
||||||
const userAllowedTools = parseToolsInput(process.env.ALLOWED_TOOLS_INPUT);
|
const userAllowedTools = parseToolsInput(process.env.ALLOWED_TOOLS_INPUT);
|
||||||
|
|
||||||
// Build claude_args for tag mode with required tools
|
const gitPushWrapper = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`;
|
||||||
// Tag mode REQUIRES these tools to function properly
|
|
||||||
|
// Build claude_args for tag mode with required tools.
|
||||||
|
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode (set below)
|
||||||
|
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside (e.g. ~/.bashrc).
|
||||||
|
// Listing them here would grant blanket write access to the whole runner (Asana 1213310082312048).
|
||||||
const tagModeTools = [
|
const tagModeTools = [
|
||||||
"Edit",
|
|
||||||
"MultiEdit",
|
|
||||||
"Glob",
|
"Glob",
|
||||||
"Grep",
|
"Grep",
|
||||||
"LS",
|
"LS",
|
||||||
"Read",
|
"Read",
|
||||||
"Write",
|
|
||||||
"mcp__github_comment__update_claude_comment",
|
"mcp__github_comment__update_claude_comment",
|
||||||
"mcp__github_ci__get_ci_status",
|
"mcp__github_ci__get_ci_status",
|
||||||
"mcp__github_ci__get_workflow_run_details",
|
"mcp__github_ci__get_workflow_run_details",
|
||||||
@@ -152,7 +153,7 @@ export async function prepareTagMode({
|
|||||||
tagModeTools.push(
|
tagModeTools.push(
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
"Bash(git push:*)",
|
`Bash(${gitPushWrapper}:*)`,
|
||||||
"Bash(git status:*)",
|
"Bash(git status:*)",
|
||||||
"Bash(git diff:*)",
|
"Bash(git diff:*)",
|
||||||
"Bash(git log:*)",
|
"Bash(git log:*)",
|
||||||
@@ -186,8 +187,10 @@ export async function prepareTagMode({
|
|||||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||||
|
|
||||||
// Add required tools for tag mode
|
// Add required tools for tag mode.
|
||||||
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
|
// acceptEdits: file edits auto-allowed inside cwd ($GITHUB_WORKSPACE), denied outside.
|
||||||
|
// Headless SDK has no prompt handler, so anything that falls through to "ask" is denied.
|
||||||
|
claudeArgs += ` --permission-mode acceptEdits --allowedTools "${tagModeTools.join(",")}"`;
|
||||||
|
|
||||||
// Append user's claude_args (which may have more --mcp-config flags)
|
// Append user's claude_args (which may have more --mcp-config flags)
|
||||||
if (userClaudeArgs) {
|
if (userClaudeArgs) {
|
||||||
|
|||||||
+23
-18
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect, beforeAll } from "bun:test";
|
||||||
import {
|
import {
|
||||||
generatePrompt,
|
generatePrompt,
|
||||||
getEventTypeAndContext,
|
getEventTypeAndContext,
|
||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
} from "../src/create-prompt";
|
} from "../src/create-prompt";
|
||||||
import type { PreparedContext } from "../src/create-prompt";
|
import type { PreparedContext } from "../src/create-prompt";
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
||||||
|
});
|
||||||
|
|
||||||
describe("generatePrompt", () => {
|
describe("generatePrompt", () => {
|
||||||
const mockGitHubData = {
|
const mockGitHubData = {
|
||||||
contextData: {
|
contextData: {
|
||||||
@@ -505,7 +509,7 @@ describe("generatePrompt", () => {
|
|||||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
|
|
||||||
// Should contain PR-specific instructions (git commands when not using signing)
|
// Should contain PR-specific instructions (git commands when not using signing)
|
||||||
expect(prompt).toContain("git push");
|
expect(prompt).toContain("scripts/git-push.sh origin");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"Always push to the existing branch when triggered on a PR",
|
"Always push to the existing branch when triggered on a PR",
|
||||||
);
|
);
|
||||||
@@ -643,7 +647,7 @@ describe("generatePrompt", () => {
|
|||||||
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
|
|
||||||
// Should contain open PR instructions (git commands when not using signing)
|
// Should contain open PR instructions (git commands when not using signing)
|
||||||
expect(prompt).toContain("git push");
|
expect(prompt).toContain("scripts/git-push.sh origin");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"Always push to the existing branch when triggered on a PR",
|
"Always push to the existing branch when triggered on a PR",
|
||||||
);
|
);
|
||||||
@@ -757,7 +761,7 @@ describe("generatePrompt", () => {
|
|||||||
expect(prompt).toContain("Use git commands via the Bash tool");
|
expect(prompt).toContain("Use git commands via the Bash tool");
|
||||||
expect(prompt).toContain("git add");
|
expect(prompt).toContain("git add");
|
||||||
expect(prompt).toContain("git commit");
|
expect(prompt).toContain("git commit");
|
||||||
expect(prompt).toContain("git push");
|
expect(prompt).toContain("scripts/git-push.sh origin");
|
||||||
|
|
||||||
// Should use the minimal comment tool
|
// Should use the minimal comment tool
|
||||||
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
|
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
|
||||||
@@ -886,17 +890,18 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString();
|
const result = buildAllowedToolsString();
|
||||||
|
|
||||||
// The base tools should be in the result
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
// Edit/MultiEdit/Write are NOT in allowedTools — acceptEdits permission mode handles them
|
||||||
|
expect(result).not.toContain("Edit");
|
||||||
|
expect(result).not.toContain("Write");
|
||||||
expect(result).toContain("Glob");
|
expect(result).toContain("Glob");
|
||||||
expect(result).toContain("Grep");
|
expect(result).toContain("Grep");
|
||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
|
||||||
|
|
||||||
// Default is no commit signing, so should have specific Bash git commands
|
// Default is no commit signing, so should have specific Bash git commands
|
||||||
expect(result).toContain("Bash(git add:*)");
|
expect(result).toContain("Bash(git add:*)");
|
||||||
expect(result).toContain("Bash(git commit:*)");
|
expect(result).toContain("Bash(git commit:*)");
|
||||||
expect(result).toContain("Bash(git push:*)");
|
expect(result).toContain("scripts/git-push.sh:*)");
|
||||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||||
|
|
||||||
// Should not have commit signing tools
|
// Should not have commit signing tools
|
||||||
@@ -908,12 +913,12 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString([], false, false);
|
const result = buildAllowedToolsString([], false, false);
|
||||||
|
|
||||||
// The base tools should be in the result
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).not.toContain("Edit");
|
||||||
expect(result).toContain("Glob");
|
expect(result).toContain("Glob");
|
||||||
expect(result).toContain("Grep");
|
expect(result).toContain("Grep");
|
||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
expect(result).not.toContain("Write");
|
||||||
|
|
||||||
// Should have specific Bash git commands for non-signing mode
|
// Should have specific Bash git commands for non-signing mode
|
||||||
expect(result).toContain("Bash(git add:*)");
|
expect(result).toContain("Bash(git add:*)");
|
||||||
@@ -930,7 +935,7 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString(customTools);
|
const result = buildAllowedToolsString(customTools);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Glob");
|
expect(result).toContain("Glob");
|
||||||
|
|
||||||
// Custom tools should be appended
|
// Custom tools should be appended
|
||||||
@@ -950,7 +955,7 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString([], true);
|
const result = buildAllowedToolsString([], true);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Glob");
|
expect(result).toContain("Glob");
|
||||||
|
|
||||||
// GitHub Actions tools should be included
|
// GitHub Actions tools should be included
|
||||||
@@ -964,7 +969,7 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString(customTools, true);
|
const result = buildAllowedToolsString(customTools, true);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Read");
|
||||||
|
|
||||||
// Custom tools should be included
|
// Custom tools should be included
|
||||||
expect(result).toContain("Tool1");
|
expect(result).toContain("Tool1");
|
||||||
@@ -980,12 +985,12 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString([], false, true);
|
const result = buildAllowedToolsString([], false, true);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).not.toContain("Edit");
|
||||||
expect(result).toContain("Glob");
|
expect(result).toContain("Glob");
|
||||||
expect(result).toContain("Grep");
|
expect(result).toContain("Grep");
|
||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
expect(result).not.toContain("Write");
|
||||||
|
|
||||||
// Commit signing tools should be included
|
// Commit signing tools should be included
|
||||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||||
@@ -1001,17 +1006,17 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString([], false, false);
|
const result = buildAllowedToolsString([], false, false);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).not.toContain("Edit");
|
||||||
expect(result).toContain("Glob");
|
expect(result).toContain("Glob");
|
||||||
expect(result).toContain("Grep");
|
expect(result).toContain("Grep");
|
||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
expect(result).not.toContain("Write");
|
||||||
|
|
||||||
// Specific Bash git commands should be included
|
// Specific Bash git commands should be included
|
||||||
expect(result).toContain("Bash(git add:*)");
|
expect(result).toContain("Bash(git add:*)");
|
||||||
expect(result).toContain("Bash(git commit:*)");
|
expect(result).toContain("Bash(git commit:*)");
|
||||||
expect(result).toContain("Bash(git push:*)");
|
expect(result).toContain("scripts/git-push.sh:*)");
|
||||||
expect(result).toContain("Bash(git status:*)");
|
expect(result).toContain("Bash(git status:*)");
|
||||||
expect(result).toContain("Bash(git diff:*)");
|
expect(result).toContain("Bash(git diff:*)");
|
||||||
expect(result).toContain("Bash(git log:*)");
|
expect(result).toContain("Bash(git log:*)");
|
||||||
@@ -1030,7 +1035,7 @@ describe("buildAllowedToolsString", () => {
|
|||||||
const result = buildAllowedToolsString(customTools, true, false);
|
const result = buildAllowedToolsString(customTools, true, false);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Bash(git add:*)");
|
expect(result).toContain("Bash(git add:*)");
|
||||||
|
|
||||||
// Custom tools should be included
|
// Custom tools should be included
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ describe("prepareMcpConfig", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "",
|
branchPrefix: "",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
sshSigningKey: "",
|
sshSigningKey: "",
|
||||||
botId: String(CLAUDE_APP_BOT_ID),
|
botId: String(CLAUDE_APP_BOT_ID),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const defaultInputs = {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
sshSigningKey: "",
|
sshSigningKey: "",
|
||||||
botId: String(CLAUDE_APP_BOT_ID),
|
botId: String(CLAUDE_APP_BOT_ID),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe("detectMode with enhanced routing", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
sshSigningKey: "",
|
sshSigningKey: "",
|
||||||
botId: "123456",
|
botId: "123456",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ describe("checkWritePermissions", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
sshSigningKey: "",
|
sshSigningKey: "",
|
||||||
botId: String(CLAUDE_APP_BOT_ID),
|
botId: String(CLAUDE_APP_BOT_ID),
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ describe("pull_request_target event support", () => {
|
|||||||
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
|
||||||
|
|
||||||
// Should include git commands for non-commit-signing mode
|
// Should include git commands for non-commit-signing mode
|
||||||
expect(prompt).toContain("git push");
|
expect(prompt).toContain("scripts/git-push.sh origin");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"Always push to the existing branch when triggered on a PR",
|
"Always push to the existing branch when triggered on a PR",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
@@ -63,6 +64,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
@@ -275,6 +277,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
@@ -304,6 +307,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
@@ -333,6 +337,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
labelTrigger: "",
|
labelTrigger: "",
|
||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
|
classifyInlineComments: true,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user