mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 15:34:13 +08:00
Compare commits
14 Commits
v1.0.29
...
ashwin/bum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142a77fa55 | ||
|
|
ba60ef7ba2 | ||
|
|
f3c892ca8d | ||
|
|
6e896a06bb | ||
|
|
a017b830c0 | ||
|
|
75f52e56b2 | ||
|
|
1bbc9e7ff7 | ||
|
|
625ea1519c | ||
|
|
a9171f0ced | ||
|
|
4778aeae4c | ||
|
|
b6e5a9f27a | ||
|
|
5d91d7d217 | ||
|
|
90006bcae7 | ||
|
|
005436f51d |
37
.github/workflows/ci-all.yml
vendored
Normal file
37
.github/workflows/ci-all.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Orchestrates all CI workflows - runs on PRs, pushes to main, and manual dispatch
|
||||||
|
# Individual test workflows are called as reusable workflows
|
||||||
|
name: CI All
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: ./.github/workflows/ci.yml
|
||||||
|
|
||||||
|
test-base-action:
|
||||||
|
uses: ./.github/workflows/test-base-action.yml
|
||||||
|
secrets: inherit # Required for ANTHROPIC_API_KEY
|
||||||
|
|
||||||
|
test-custom-executables:
|
||||||
|
uses: ./.github/workflows/test-custom-executables.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test-mcp-servers:
|
||||||
|
uses: ./.github/workflows/test-mcp-servers.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test-settings:
|
||||||
|
uses: ./.github/workflows/test-settings.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
test-structured-output:
|
||||||
|
uses: ./.github/workflows/test-structured-output.yml
|
||||||
|
secrets: inherit
|
||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -1,9 +1,8 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
106
.github/workflows/release.yml
vendored
106
.github/workflows/release.yml
vendored
@@ -8,10 +8,23 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["CI All"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Run if: manual dispatch OR (CI All succeeded AND commit is a version bump)
|
||||||
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.head_branch == 'main' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
startsWith(github.event.workflow_run.head_commit.message, 'chore: bump Claude Code to'))
|
||||||
environment: production
|
environment: production
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -84,7 +97,8 @@ jobs:
|
|||||||
|
|
||||||
update-major-tag:
|
update-major-tag:
|
||||||
needs: create-release
|
needs: create-release
|
||||||
if: ${{ !inputs.dry_run }}
|
# Skip for dry runs (workflow_run events are never dry runs)
|
||||||
|
if: github.event_name == 'workflow_run' || !inputs.dry_run
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: production
|
environment: production
|
||||||
permissions:
|
permissions:
|
||||||
@@ -109,48 +123,48 @@ jobs:
|
|||||||
|
|
||||||
echo "Updated $major_version tag to point to $next_version"
|
echo "Updated $major_version tag to point to $next_version"
|
||||||
|
|
||||||
release-base-action:
|
# release-base-action:
|
||||||
needs: create-release
|
# needs: create-release
|
||||||
if: ${{ !inputs.dry_run }}
|
# if: ${{ !inputs.dry_run }}
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
environment: production
|
# environment: production
|
||||||
steps:
|
# steps:
|
||||||
- name: Checkout base-action repo
|
# - name: Checkout base-action repo
|
||||||
uses: actions/checkout@v5
|
# uses: actions/checkout@v5
|
||||||
with:
|
# with:
|
||||||
repository: anthropics/claude-code-base-action
|
# repository: anthropics/claude-code-base-action
|
||||||
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
# token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||||
fetch-depth: 0
|
# fetch-depth: 0
|
||||||
|
#
|
||||||
# - name: Create and push tag
|
# - name: Create and push tag
|
||||||
# run: |
|
# run: |
|
||||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||||
|
#
|
||||||
# git config user.name "github-actions[bot]"
|
# git config user.name "github-actions[bot]"
|
||||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
#
|
||||||
# # Create the version tag
|
# # Create the version tag
|
||||||
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||||
# git push origin "$next_version"
|
# git push origin "$next_version"
|
||||||
|
#
|
||||||
# # Update the beta tag
|
# # Update the beta tag
|
||||||
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
||||||
# git push origin beta --force
|
# git push origin beta --force
|
||||||
|
#
|
||||||
# - name: Create GitHub release
|
# - name: Create GitHub release
|
||||||
# env:
|
# env:
|
||||||
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||||
# run: |
|
# run: |
|
||||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||||
|
#
|
||||||
# # Create the release
|
# # Create the release
|
||||||
# gh release create "$next_version" \
|
# gh release create "$next_version" \
|
||||||
# --repo anthropics/claude-code-base-action \
|
# --repo anthropics/claude-code-base-action \
|
||||||
# --title "$next_version" \
|
# --title "$next_version" \
|
||||||
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
|
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
|
||||||
# --latest=false
|
# --latest=false
|
||||||
|
#
|
||||||
# # Update beta release to be latest
|
# # Update beta release to be latest
|
||||||
# gh release edit beta \
|
# gh release edit beta \
|
||||||
# --repo anthropics/claude-code-base-action \
|
# --repo anthropics/claude-code-base-action \
|
||||||
# --latest
|
# --latest
|
||||||
|
|||||||
4
.github/workflows/test-base-action.yml
vendored
4
.github/workflows/test-base-action.yml
vendored
@@ -1,9 +1,6 @@
|
|||||||
name: Test Claude Code Action
|
name: Test Claude Code Action
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -11,6 +8,7 @@ on:
|
|||||||
description: "Test prompt for Claude"
|
description: "Test prompt for Claude"
|
||||||
required: false
|
required: false
|
||||||
default: "List the files in the current directory starting with 'package'"
|
default: "List the files in the current directory starting with 'package'"
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-inline-prompt:
|
test-inline-prompt:
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
name: Test Custom Executables
|
name: Test Custom Executables
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-custom-executables:
|
test-custom-executables:
|
||||||
|
|||||||
4
.github/workflows/test-mcp-servers.yml
vendored
4
.github/workflows/test-mcp-servers.yml
vendored
@@ -1,11 +1,9 @@
|
|||||||
name: Test MCP Servers
|
name: Test MCP Servers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-mcp-integration:
|
test-mcp-integration:
|
||||||
|
|||||||
4
.github/workflows/test-settings.yml
vendored
4
.github/workflows/test-settings.yml
vendored
@@ -1,11 +1,9 @@
|
|||||||
name: Test Settings Feature
|
name: Test Settings Feature
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-settings-inline-allow:
|
test-settings-inline-allow:
|
||||||
|
|||||||
4
.github/workflows/test-structured-output.yml
vendored
4
.github/workflows/test-structured-output.yml
vendored
@@ -1,11 +1,9 @@
|
|||||||
name: Test Structured Outputs
|
name: Test Structured Outputs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Install Bun
|
- name: Install Bun
|
||||||
if: inputs.path_to_bun_executable == ''
|
if: inputs.path_to_bun_executable == ''
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
|
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # https://github.com/oven-sh/setup-bun/releases/tag/v2.1.2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.11
|
bun-version: 2.1.1
|
||||||
|
|
||||||
- name: Setup Custom Bun Path
|
- name: Setup Custom Bun Path
|
||||||
if: inputs.path_to_bun_executable != ''
|
if: inputs.path_to_bun_executable != ''
|
||||||
@@ -213,7 +213,7 @@ runs:
|
|||||||
|
|
||||||
# Install Claude Code if no custom executable is provided
|
# Install Claude Code if no custom executable is provided
|
||||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||||
CLAUDE_CODE_VERSION="2.1.1"
|
CLAUDE_CODE_VERSION="2.1.11"
|
||||||
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..."
|
||||||
|
|||||||
@@ -97,9 +97,9 @@ runs:
|
|||||||
|
|
||||||
- name: Install Bun
|
- name: Install Bun
|
||||||
if: inputs.path_to_bun_executable == ''
|
if: inputs.path_to_bun_executable == ''
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
|
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # https://github.com/oven-sh/setup-bun/releases/tag/v2.1.2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.11
|
bun-version: 2.1.1
|
||||||
|
|
||||||
- name: Setup Custom Bun Path
|
- name: Setup Custom Bun Path
|
||||||
if: inputs.path_to_bun_executable != ''
|
if: inputs.path_to_bun_executable != ''
|
||||||
@@ -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.1"
|
CLAUDE_CODE_VERSION="2.1.11"
|
||||||
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.1",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||||
"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.1", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-ZJO/TWcrFHGQTGHJDJl03mWozirWMBqdNpbuAgxZpLaHj2N5vyMxoeYiJC+7M0+gOSs7bjwKJLKTZcHGtGa34g=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.11", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-50q4vfh57HYTUrwukULp3gvSaOZfRF5zukxhWvW6mmUHuD8+Xwhqn59sZhoDz9ZUw6Um+1lUCgWpJw5/TTMn5w=="],
|
||||||
|
|
||||||
"@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.1",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||||
"shell-quote": "^1.8.3"
|
"shell-quote": "^1.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -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.1",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||||
"@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.1", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-ZJO/TWcrFHGQTGHJDJl03mWozirWMBqdNpbuAgxZpLaHj2N5vyMxoeYiJC+7M0+gOSs7bjwKJLKTZcHGtGa34g=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.11", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-50q4vfh57HYTUrwukULp3gvSaOZfRF5zukxhWvW6mmUHuD8+Xwhqn59sZhoDz9ZUw6Um+1lUCgWpJw5/TTMn5w=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,16 @@
|
|||||||
- **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
|
||||||
|
|
||||||
|
## Pull Request Creation
|
||||||
|
|
||||||
|
In its default configuration, **Claude does not create pull requests automatically** when responding to `@claude` mentions. Instead:
|
||||||
|
|
||||||
|
- Claude commits code changes to a new branch
|
||||||
|
- Claude provides a **link to the GitHub PR creation page** in its response
|
||||||
|
- **The user must click the link and create the PR themselves**, ensuring human oversight before any code is proposed for merging
|
||||||
|
|
||||||
|
This design ensures that users retain full control over what pull requests are created and can review the changes before initiating the PR workflow.
|
||||||
|
|
||||||
## ⚠️ Prompt Injection Risks
|
## ⚠️ Prompt Injection Risks
|
||||||
|
|
||||||
**Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it.
|
**Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it.
|
||||||
|
|||||||
@@ -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.1",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -82,8 +82,13 @@ export async function setupSshSigning(sshSigningKey: string): Promise<void> {
|
|||||||
const sshDir = join(homedir(), ".ssh");
|
const sshDir = join(homedir(), ".ssh");
|
||||||
await mkdir(sshDir, { recursive: true, mode: 0o700 });
|
await mkdir(sshDir, { recursive: true, mode: 0o700 });
|
||||||
|
|
||||||
|
// Ensure key ends with newline (required for ssh-keygen to parse it)
|
||||||
|
const normalizedKey = sshSigningKey.endsWith("\n")
|
||||||
|
? sshSigningKey
|
||||||
|
: sshSigningKey + "\n";
|
||||||
|
|
||||||
// Write the signing key atomically with secure permissions (600)
|
// Write the signing key atomically with secure permissions (600)
|
||||||
await writeFile(SSH_SIGNING_KEY_PATH, sshSigningKey, { mode: 0o600 });
|
await writeFile(SSH_SIGNING_KEY_PATH, normalizedKey, { mode: 0o600 });
|
||||||
console.log(`✓ SSH signing key written to ${SSH_SIGNING_KEY_PATH}`);
|
console.log(`✓ SSH signing key written to ${SSH_SIGNING_KEY_PATH}`);
|
||||||
|
|
||||||
// Configure git to use SSH signing
|
// Configure git to use SSH signing
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
import type { ParsedGitHubContext } from "../context";
|
import type { GitHubContext } from "../context";
|
||||||
|
|
||||||
export async function checkHumanActor(
|
export async function checkHumanActor(
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
githubContext: ParsedGitHubContext,
|
githubContext: GitHubContext,
|
||||||
) {
|
) {
|
||||||
// Fetch user information from GitHub API
|
// Fetch user information from GitHub API
|
||||||
const { data: userData } = await octokit.users.getByUsername({
|
const { data: userData } = await octokit.users.getByUsername({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
configureGitAuth,
|
configureGitAuth,
|
||||||
setupSshSigning,
|
setupSshSigning,
|
||||||
} from "../../github/operations/git-config";
|
} from "../../github/operations/git-config";
|
||||||
|
import { checkHumanActor } from "../../github/validation/actor";
|
||||||
import type { GitHubContext } from "../../github/context";
|
import type { GitHubContext } from "../../github/context";
|
||||||
import { isEntityContext } from "../../github/context";
|
import { isEntityContext } from "../../github/context";
|
||||||
|
|
||||||
@@ -80,7 +81,14 @@ export const agentMode: Mode = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
async prepare({
|
||||||
|
context,
|
||||||
|
octokit,
|
||||||
|
githubToken,
|
||||||
|
}: ModeOptions): Promise<ModeResult> {
|
||||||
|
// Check if actor is human (prevents bot-triggered loops)
|
||||||
|
await checkHumanActor(octokit.rest, context);
|
||||||
|
|
||||||
// Configure git authentication for agent mode (same as tag mode)
|
// Configure git authentication for agent mode (same as tag mode)
|
||||||
// SSH signing takes precedence if provided
|
// SSH signing takes precedence if provided
|
||||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
export function parseAllowedTools(claudeArgs: string): string[] {
|
export function parseAllowedTools(claudeArgs: string): string[] {
|
||||||
// Match --allowedTools or --allowed-tools followed by the value
|
// Match --allowedTools or --allowed-tools followed by the value
|
||||||
// Handle both quoted and unquoted values
|
// Handle both quoted and unquoted values
|
||||||
|
// Use /g flag to find ALL occurrences, not just the first one
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/, // Double quoted
|
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/g, // Double quoted
|
||||||
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/, // Single quoted
|
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/g, // Single quoted
|
||||||
/--(?:allowedTools|allowed-tools)\s+([^\s]+)/, // Unquoted
|
/--(?:allowedTools|allowed-tools)\s+([^'"\s][^\s]*)/g, // Unquoted (must not start with quote)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tools: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
const match = claudeArgs.match(pattern);
|
for (const match of claudeArgs.matchAll(pattern)) {
|
||||||
if (match && match[1]) {
|
if (match[1]) {
|
||||||
// Don't return if the value starts with -- (another flag)
|
// Don't add if the value starts with -- (another flag)
|
||||||
if (match[1].startsWith("--")) {
|
if (match[1].startsWith("--")) {
|
||||||
return [];
|
continue;
|
||||||
|
}
|
||||||
|
for (const tool of match[1].split(",")) {
|
||||||
|
const trimmed = tool.trim();
|
||||||
|
if (trimmed && !seen.has(trimmed)) {
|
||||||
|
seen.add(trimmed);
|
||||||
|
tools.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return match[1].split(",").map((t) => t.trim());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return tools;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,12 +145,12 @@ describe("Agent Mode", () => {
|
|||||||
users: {
|
users: {
|
||||||
getAuthenticated: mock(() =>
|
getAuthenticated: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
getByUsername: mock(() =>
|
getByUsername: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -187,6 +187,65 @@ describe("Agent Mode", () => {
|
|||||||
process.env.GITHUB_REF_NAME = originalRefName;
|
process.env.GITHUB_REF_NAME = originalRefName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prepare method rejects bot actors without allowed_bots", async () => {
|
||||||
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
contextWithPrompts.actor = "claude[bot]";
|
||||||
|
contextWithPrompts.inputs.allowedBots = "";
|
||||||
|
|
||||||
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
users: {
|
||||||
|
getByUsername: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "claude[bot]", id: 12345, type: "Bot" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
agentMode.prepare({
|
||||||
|
context: contextWithPrompts,
|
||||||
|
octokit: mockOctokit,
|
||||||
|
githubToken: "test-token",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: claude (type: Bot)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prepare method allows bot actors when in allowed_bots list", async () => {
|
||||||
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
contextWithPrompts.actor = "dependabot[bot]";
|
||||||
|
contextWithPrompts.inputs.allowedBots = "dependabot";
|
||||||
|
|
||||||
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
users: {
|
||||||
|
getByUsername: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "dependabot[bot]", id: 12345, type: "Bot" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Should not throw - bot is in allowed list
|
||||||
|
await expect(
|
||||||
|
agentMode.prepare({
|
||||||
|
context: contextWithPrompts,
|
||||||
|
octokit: mockOctokit,
|
||||||
|
githubToken: "test-token",
|
||||||
|
}),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
test("prepare method creates prompt file with correct content", async () => {
|
test("prepare method creates prompt file with correct content", async () => {
|
||||||
const contextWithPrompts = createMockAutomationContext({
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
@@ -199,12 +258,12 @@ describe("Agent Mode", () => {
|
|||||||
users: {
|
users: {
|
||||||
getAuthenticated: mock(() =>
|
getAuthenticated: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
getByUsername: mock(() =>
|
getByUsername: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,12 +35,44 @@ describe("parseAllowedTools", () => {
|
|||||||
expect(parseAllowedTools("")).toEqual([]);
|
expect(parseAllowedTools("")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles duplicate --allowedTools flags", () => {
|
test("handles --allowedTools followed by another --allowedTools flag", () => {
|
||||||
const args = "--allowedTools --allowedTools mcp__github__*";
|
const args = "--allowedTools --allowedTools mcp__github__*";
|
||||||
// Should not match the first one since the value is another flag
|
// The second --allowedTools is consumed as a value of the first, then skipped.
|
||||||
|
// This is an edge case with malformed input - returns empty.
|
||||||
expect(parseAllowedTools(args)).toEqual([]);
|
expect(parseAllowedTools(args)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses multiple separate --allowed-tools flags", () => {
|
||||||
|
const args =
|
||||||
|
"--allowed-tools 'mcp__context7__*' --allowed-tools 'Read,Glob' --allowed-tools 'mcp__github_inline_comment__*'";
|
||||||
|
expect(parseAllowedTools(args)).toEqual([
|
||||||
|
"mcp__context7__*",
|
||||||
|
"Read",
|
||||||
|
"Glob",
|
||||||
|
"mcp__github_inline_comment__*",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses multiple --allowed-tools flags on separate lines", () => {
|
||||||
|
const args = `--model 'claude-haiku'
|
||||||
|
--allowed-tools 'mcp__context7__*'
|
||||||
|
--allowed-tools 'Read,Glob,Grep'
|
||||||
|
--allowed-tools 'mcp__github_inline_comment__create_inline_comment'`;
|
||||||
|
expect(parseAllowedTools(args)).toEqual([
|
||||||
|
"mcp__context7__*",
|
||||||
|
"Read",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"mcp__github_inline_comment__create_inline_comment",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deduplicates tools from multiple flags", () => {
|
||||||
|
const args =
|
||||||
|
"--allowed-tools 'Read,Glob' --allowed-tools 'Glob,Grep' --allowed-tools 'Read'";
|
||||||
|
expect(parseAllowedTools(args)).toEqual(["Read", "Glob", "Grep"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("handles typo --alloedTools", () => {
|
test("handles typo --alloedTools", () => {
|
||||||
const args = "--alloedTools mcp__github__*";
|
const args = "--alloedTools mcp__github__*";
|
||||||
expect(parseAllowedTools(args)).toEqual([]);
|
expect(parseAllowedTools(args)).toEqual([]);
|
||||||
|
|||||||
@@ -55,6 +55,47 @@ describe("SSH Signing", () => {
|
|||||||
expect(permissions).toBe(0o600);
|
expect(permissions).toBe(0o600);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should normalize key to have trailing newline", async () => {
|
||||||
|
// ssh-keygen requires a trailing newline to parse the key
|
||||||
|
const keyWithoutNewline =
|
||||||
|
"-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-content\n-----END OPENSSH PRIVATE KEY-----";
|
||||||
|
const keyWithNewline = keyWithoutNewline + "\n";
|
||||||
|
|
||||||
|
// Create directory
|
||||||
|
await mkdir(testSshDir, { recursive: true, mode: 0o700 });
|
||||||
|
|
||||||
|
// Normalize the key (same logic as setupSshSigning)
|
||||||
|
const normalizedKey = keyWithoutNewline.endsWith("\n")
|
||||||
|
? keyWithoutNewline
|
||||||
|
: keyWithoutNewline + "\n";
|
||||||
|
|
||||||
|
await writeFile(testKeyPath, normalizedKey, { mode: 0o600 });
|
||||||
|
|
||||||
|
// Verify the written key ends with newline
|
||||||
|
const keyContent = await readFile(testKeyPath, "utf-8");
|
||||||
|
expect(keyContent).toBe(keyWithNewline);
|
||||||
|
expect(keyContent.endsWith("\n")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not add extra newline if key already has one", async () => {
|
||||||
|
const keyWithNewline =
|
||||||
|
"-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-content\n-----END OPENSSH PRIVATE KEY-----\n";
|
||||||
|
|
||||||
|
await mkdir(testSshDir, { recursive: true, mode: 0o700 });
|
||||||
|
|
||||||
|
// Normalize the key (same logic as setupSshSigning)
|
||||||
|
const normalizedKey = keyWithNewline.endsWith("\n")
|
||||||
|
? keyWithNewline
|
||||||
|
: keyWithNewline + "\n";
|
||||||
|
|
||||||
|
await writeFile(testKeyPath, normalizedKey, { mode: 0o600 });
|
||||||
|
|
||||||
|
// Verify no double newline
|
||||||
|
const keyContent = await readFile(testKeyPath, "utf-8");
|
||||||
|
expect(keyContent).toBe(keyWithNewline);
|
||||||
|
expect(keyContent.endsWith("\n\n")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
test("should create .ssh directory with secure permissions", async () => {
|
test("should create .ssh directory with secure permissions", async () => {
|
||||||
// Clean up first
|
// Clean up first
|
||||||
await rm(testSshDir, { recursive: true, force: true });
|
await rm(testSshDir, { recursive: true, force: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user