mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-25 08:14:12 +08:00
Compare commits
2 Commits
v1.0.30
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d544d7303b | ||
|
|
5c0f1e2273 |
@@ -17,7 +17,6 @@ TASK OVERVIEW:
|
|||||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||||
|
|
||||||
2. Next, use gh commands to get context about the issue:
|
2. Next, use gh commands to get context about the issue:
|
||||||
|
|
||||||
- Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
|
- Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
|
||||||
- Use `gh search issues` to find similar issues that might provide context for proper categorization
|
- Use `gh search issues` to find similar issues that might provide context for proper categorization
|
||||||
- You have access to these Bash commands:
|
- You have access to these Bash commands:
|
||||||
@@ -27,7 +26,6 @@ TASK OVERVIEW:
|
|||||||
- Bash(gh search:\*) - to search for similar issues
|
- Bash(gh search:\*) - to search for similar issues
|
||||||
|
|
||||||
3. Analyze the issue content, considering:
|
3. Analyze the issue content, considering:
|
||||||
|
|
||||||
- The issue title and description
|
- The issue title and description
|
||||||
- The type of issue (bug report, feature request, question, etc.)
|
- The type of issue (bug report, feature request, question, etc.)
|
||||||
- Technical areas mentioned
|
- Technical areas mentioned
|
||||||
@@ -36,7 +34,6 @@ TASK OVERVIEW:
|
|||||||
- Components affected
|
- Components affected
|
||||||
|
|
||||||
4. Select appropriate labels from the available labels list provided above:
|
4. Select appropriate labels from the available labels list provided above:
|
||||||
|
|
||||||
- Choose labels that accurately reflect the issue's nature
|
- Choose labels that accurately reflect the issue's nature
|
||||||
- Be specific but comprehensive
|
- Be specific but comprehensive
|
||||||
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
|
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
|
||||||
|
|||||||
90
.github/workflows/release.yml
vendored
90
.github/workflows/release.yml
vendored
@@ -109,48 +109,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
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ inputs:
|
|||||||
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||||
required: false
|
required: false
|
||||||
default: "claude/"
|
default: "claude/"
|
||||||
branch_name_template:
|
|
||||||
description: "Template for branch naming. Available variables: {{prefix}}, {{entityType}}, {{entityNumber}}, {{timestamp}}, {{sha}}, {{label}}, {{description}}. {{label}} will be first label from the issue/PR, or {{entityType}} as a fallback. {{description}} will be the first 5 words of the issue/PR title in kebab-case. Default: '{{prefix}}{{entityType}}-{{entityNumber}}-{{timestamp}}'"
|
|
||||||
required: false
|
|
||||||
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."
|
||||||
required: false
|
required: false
|
||||||
@@ -182,7 +178,6 @@ runs:
|
|||||||
LABEL_TRIGGER: ${{ inputs.label_trigger }}
|
LABEL_TRIGGER: ${{ inputs.label_trigger }}
|
||||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||||
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
||||||
BRANCH_NAME_TEMPLATE: ${{ inputs.branch_name_template }}
|
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
||||||
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
||||||
@@ -213,7 +208,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.9"
|
CLAUDE_CODE_VERSION="2.0.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..."
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ Thank you for your interest in contributing to Claude Code Base Action! This doc
|
|||||||
```
|
```
|
||||||
|
|
||||||
This script:
|
This script:
|
||||||
|
|
||||||
- Installs `act` if not present (requires Homebrew on macOS)
|
- Installs `act` if not present (requires Homebrew on macOS)
|
||||||
- Runs the GitHub Action workflow locally using Docker
|
- Runs the GitHub Action workflow locally using Docker
|
||||||
- Requires your `ANTHROPIC_API_KEY` to be set
|
- Requires your `ANTHROPIC_API_KEY` to be set
|
||||||
|
|||||||
@@ -85,26 +85,26 @@ Add the following to your workflow file:
|
|||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||||
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
||||||
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
|
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
|
||||||
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
|
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
|
||||||
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
|
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
|
||||||
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
|
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
|
||||||
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
|
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
|
||||||
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
|
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
|
||||||
| `system_prompt` | Override system prompt | No | '' |
|
| `system_prompt` | Override system prompt | No | '' |
|
||||||
| `append_system_prompt` | Append to system prompt | No | '' |
|
| `append_system_prompt` | Append to system prompt | No | '' |
|
||||||
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
|
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
|
||||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
||||||
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
||||||
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
|
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | 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 | '' |
|
||||||
| `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' |
|
||||||
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
|
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
|
||||||
| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* |
|
| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* |
|
||||||
|
|
||||||
\*Either `prompt` or `prompt_file` must be provided, but not both.
|
\*Either `prompt` or `prompt_file` must be provided, but not both.
|
||||||
@@ -490,7 +490,6 @@ This example shows how to use OIDC authentication with GCP Vertex AI:
|
|||||||
To securely use your Anthropic API key:
|
To securely use your Anthropic API key:
|
||||||
|
|
||||||
1. Add your API key as a repository secret:
|
1. Add your API key as a repository secret:
|
||||||
|
|
||||||
- Go to your repository's Settings
|
- Go to your repository's Settings
|
||||||
- Navigate to "Secrets and variables" → "Actions"
|
- Navigate to "Secrets and variables" → "Actions"
|
||||||
- Click "New repository secret"
|
- Click "New repository secret"
|
||||||
|
|||||||
@@ -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.9"
|
CLAUDE_CODE_VERSION="2.0.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.9",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.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.9", "", { "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-b4JD6ZKCZeVDqpWBnb+zJISWi3HzlweNlV7Oy/uo5G2XAfUV2M5AJ/tomKZCvZsvmr1fYbmmfyde3GL2h0pksA=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.76", "", { "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": "^3.24.1 || ^4.0.0" } }, "sha512-s7RvpXoFaLXLG7A1cJBAPD8ilwOhhc/12fb5mJXRuD561o4FmPtQ+WRfuy9akMmrFRfLsKv8Ornw3ClGAPL2fw=="],
|
||||||
|
|
||||||
"@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.9",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
||||||
"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.9",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.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.9", "", { "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-b4JD6ZKCZeVDqpWBnb+zJISWi3HzlweNlV7Oy/uo5G2XAfUV2M5AJ/tomKZCvZsvmr1fYbmmfyde3GL2h0pksA=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.76", "", { "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": "^3.24.1 || ^4.0.0" } }, "sha512-s7RvpXoFaLXLG7A1cJBAPD8ilwOhhc/12fb5mJXRuD561o4FmPtQ+WRfuy9akMmrFRfLsKv8Ornw3ClGAPL2fw=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ The `additional_permissions` input allows Claude to access GitHub Actions workfl
|
|||||||
To allow Claude to view workflow run results, job logs, and CI status:
|
To allow Claude to view workflow run results, job logs, and CI status:
|
||||||
|
|
||||||
1. **Grant the necessary permission to your GitHub token**:
|
1. **Grant the necessary permission to your GitHub token**:
|
||||||
|
|
||||||
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
|
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -228,12 +228,10 @@ jobs:
|
|||||||
The action now automatically detects the appropriate mode:
|
The action now automatically detects the appropriate mode:
|
||||||
|
|
||||||
1. **If `prompt` is provided** → Runs in **automation mode**
|
1. **If `prompt` is provided** → Runs in **automation mode**
|
||||||
|
|
||||||
- Executes immediately without waiting for @claude mentions
|
- Executes immediately without waiting for @claude mentions
|
||||||
- Perfect for scheduled tasks, PR automation, etc.
|
- Perfect for scheduled tasks, PR automation, etc.
|
||||||
|
|
||||||
2. **If no `prompt` but @claude is mentioned** → Runs in **interactive mode**
|
2. **If no `prompt` but @claude is mentioned** → Runs in **interactive mode**
|
||||||
|
|
||||||
- Waits for and responds to @claude mentions
|
- Waits for and responds to @claude mentions
|
||||||
- Creates tracking comments with progress
|
- Creates tracking comments with progress
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,6 @@
|
|||||||
- **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.
|
||||||
@@ -85,14 +75,12 @@ Commits will show as verified and attributed to the GitHub account that owns the
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Add the **public key** to your GitHub account:
|
2. Add the **public key** to your GitHub account:
|
||||||
|
|
||||||
- Go to GitHub → Settings → SSH and GPG keys
|
- Go to GitHub → Settings → SSH and GPG keys
|
||||||
- Click "New SSH key"
|
- Click "New SSH key"
|
||||||
- Select **Key type: Signing Key** (important)
|
- Select **Key type: Signing Key** (important)
|
||||||
- Paste the contents of `~/.ssh/signing_key.pub`
|
- Paste the contents of `~/.ssh/signing_key.pub`
|
||||||
|
|
||||||
3. Add the **private key** to your repository secrets:
|
3. Add the **private key** to your repository secrets:
|
||||||
|
|
||||||
- Go to your repo → Settings → Secrets and variables → Actions
|
- Go to your repo → Settings → Secrets and variables → Actions
|
||||||
- Create a new secret named `SSH_SIGNING_KEY`
|
- Create a new secret named `SSH_SIGNING_KEY`
|
||||||
- Paste the contents of `~/.ssh/signing_key`
|
- Paste the contents of `~/.ssh/signing_key`
|
||||||
|
|||||||
@@ -31,27 +31,23 @@ The fastest way to create a custom GitHub App is using our pre-configured manife
|
|||||||
**🚀 [Download the Quick Setup Tool](./create-app.html)** (Right-click → "Save Link As" or "Download Linked File")
|
**🚀 [Download the Quick Setup Tool](./create-app.html)** (Right-click → "Save Link As" or "Download Linked File")
|
||||||
|
|
||||||
After downloading, open `create-app.html` in your web browser:
|
After downloading, open `create-app.html` in your web browser:
|
||||||
|
|
||||||
- **For Personal Accounts:** Click the "Create App for Personal Account" button
|
- **For Personal Accounts:** Click the "Create App for Personal Account" button
|
||||||
- **For Organizations:** Enter your organization name and click "Create App for Organization"
|
- **For Organizations:** Enter your organization name and click "Create App for Organization"
|
||||||
|
|
||||||
The tool will automatically configure all required permissions and submit the manifest.
|
The tool will automatically configure all required permissions and submit the manifest.
|
||||||
|
|
||||||
Alternatively, you can use the manifest file directly:
|
Alternatively, you can use the manifest file directly:
|
||||||
|
|
||||||
- Use the [`github-app-manifest.json`](../github-app-manifest.json) file from this repository
|
- Use the [`github-app-manifest.json`](../github-app-manifest.json) file from this repository
|
||||||
- Visit https://github.com/settings/apps/new (for personal) or your organization's app settings
|
- Visit https://github.com/settings/apps/new (for personal) or your organization's app settings
|
||||||
- Look for the "Create from manifest" option and paste the JSON content
|
- Look for the "Create from manifest" option and paste the JSON content
|
||||||
|
|
||||||
2. **Complete the creation flow:**
|
2. **Complete the creation flow:**
|
||||||
|
|
||||||
- GitHub will show you a preview of the app configuration
|
- GitHub will show you a preview of the app configuration
|
||||||
- Confirm the app name (you can customize it)
|
- Confirm the app name (you can customize it)
|
||||||
- Click "Create GitHub App"
|
- Click "Create GitHub App"
|
||||||
- The app will be created with all required permissions automatically configured
|
- The app will be created with all required permissions automatically configured
|
||||||
|
|
||||||
3. **Generate and download a private key:**
|
3. **Generate and download a private key:**
|
||||||
|
|
||||||
- After creating the app, you'll be redirected to the app settings
|
- After creating the app, you'll be redirected to the app settings
|
||||||
- Scroll down to "Private keys"
|
- Scroll down to "Private keys"
|
||||||
- Click "Generate a private key"
|
- Click "Generate a private key"
|
||||||
@@ -64,7 +60,6 @@ The fastest way to create a custom GitHub App is using our pre-configured manife
|
|||||||
If you prefer to configure the app manually or need custom permissions:
|
If you prefer to configure the app manually or need custom permissions:
|
||||||
|
|
||||||
1. **Create a new GitHub App:**
|
1. **Create a new GitHub App:**
|
||||||
|
|
||||||
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
|
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
|
||||||
- Click "New GitHub App"
|
- Click "New GitHub App"
|
||||||
- Configure the app with these minimum permissions:
|
- Configure the app with these minimum permissions:
|
||||||
@@ -77,19 +72,16 @@ If you prefer to configure the app manually or need custom permissions:
|
|||||||
- Create the app
|
- Create the app
|
||||||
|
|
||||||
2. **Generate and download a private key:**
|
2. **Generate and download a private key:**
|
||||||
|
|
||||||
- After creating the app, scroll down to "Private keys"
|
- After creating the app, scroll down to "Private keys"
|
||||||
- Click "Generate a private key"
|
- Click "Generate a private key"
|
||||||
- Download the `.pem` file (keep this secure!)
|
- Download the `.pem` file (keep this secure!)
|
||||||
|
|
||||||
3. **Install the app on your repository:**
|
3. **Install the app on your repository:**
|
||||||
|
|
||||||
- Go to the app's settings page
|
- Go to the app's settings page
|
||||||
- Click "Install App"
|
- Click "Install App"
|
||||||
- Select the repositories where you want to use Claude
|
- Select the repositories where you want to use Claude
|
||||||
|
|
||||||
4. **Add the app credentials to your repository secrets:**
|
4. **Add the app credentials to your repository secrets:**
|
||||||
|
|
||||||
- Go to your repository's Settings → Secrets and variables → Actions
|
- Go to your repository's Settings → Secrets and variables → Actions
|
||||||
- Add these secrets:
|
- Add these secrets:
|
||||||
- `APP_ID`: Your GitHub App's ID (found in the app settings)
|
- `APP_ID`: Your GitHub App's ID (found in the app settings)
|
||||||
@@ -138,7 +130,6 @@ For more information on creating GitHub Apps, see the [GitHub documentation](htt
|
|||||||
To securely use your Anthropic API key:
|
To securely use your Anthropic API key:
|
||||||
|
|
||||||
1. Add your API key as a repository secret:
|
1. Add your API key as a repository secret:
|
||||||
|
|
||||||
- Go to your repository's Settings
|
- Go to your repository's Settings
|
||||||
- Navigate to "Secrets and variables" → "Actions"
|
- Navigate to "Secrets and variables" → "Actions"
|
||||||
- Click "New repository secret"
|
- Click "New repository secret"
|
||||||
|
|||||||
@@ -21,7 +21,26 @@ jobs:
|
|||||||
!startsWith(github.event.workflow_run.head_branch, 'claude-auto-fix-ci-')
|
!startsWith(github.event.workflow_run.head_branch, 'claude-auto-fix-ci-')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check for clawd-stop label
|
||||||
|
id: check_label
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }};
|
||||||
|
const { data: pr } = await github.rest.pulls.get({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: prNumber
|
||||||
|
});
|
||||||
|
const hasClawdStop = pr.labels.some(label => label.name === 'clawd-stop');
|
||||||
|
if (hasClawdStop) {
|
||||||
|
console.log('PR has clawd-stop label, skipping auto-fix');
|
||||||
|
}
|
||||||
|
return hasClawdStop;
|
||||||
|
result-encoding: string
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
if: steps.check_label.outputs.result != 'true'
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.workflow_run.head_branch }}
|
ref: ${{ github.event.workflow_run.head_branch }}
|
||||||
@@ -29,11 +48,13 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Setup git identity
|
- name: Setup git identity
|
||||||
|
if: steps.check_label.outputs.result != 'true'
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "claude[bot]@users.noreply.github.com"
|
git config --global user.email "claude[bot]@users.noreply.github.com"
|
||||||
git config --global user.name "claude[bot]"
|
git config --global user.name "claude[bot]"
|
||||||
|
|
||||||
- name: Create fix branch
|
- name: Create fix branch
|
||||||
|
if: steps.check_label.outputs.result != 'true'
|
||||||
id: branch
|
id: branch
|
||||||
run: |
|
run: |
|
||||||
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
||||||
@@ -41,6 +62,7 @@ jobs:
|
|||||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Get CI failure details
|
- name: Get CI failure details
|
||||||
|
if: steps.check_label.outputs.result != 'true'
|
||||||
id: failure_details
|
id: failure_details
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
@@ -79,6 +101,7 @@ jobs:
|
|||||||
};
|
};
|
||||||
|
|
||||||
- name: Fix CI failures with Claude
|
- name: Fix CI failures with Claude
|
||||||
|
if: steps.check_label.outputs.result != 'true'
|
||||||
id: claude
|
id: claude
|
||||||
uses: anthropics/claude-code-action@v1
|
uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -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.9",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.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",
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ export const PR_QUERY = `
|
|||||||
additions
|
additions
|
||||||
deletions
|
deletions
|
||||||
state
|
state
|
||||||
labels(first: 1) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commits(first: 100) {
|
commits(first: 100) {
|
||||||
totalCount
|
totalCount
|
||||||
nodes {
|
nodes {
|
||||||
@@ -106,11 +101,6 @@ export const ISSUE_QUERY = `
|
|||||||
updatedAt
|
updatedAt
|
||||||
lastEditedAt
|
lastEditedAt
|
||||||
state
|
state
|
||||||
labels(first: 1) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
comments(first: 100) {
|
comments(first: 100) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ type BaseContext = {
|
|||||||
labelTrigger: string;
|
labelTrigger: string;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
branchNameTemplate?: string;
|
|
||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
sshSigningKey: string;
|
sshSigningKey: string;
|
||||||
@@ -146,7 +145,6 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
||||||
baseBranch: process.env.BASE_BRANCH,
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||||
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
|
||||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||||
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
||||||
|
|||||||
@@ -6,22 +6,12 @@
|
|||||||
* - For Issues: Create a new branch
|
* - For Issues: Create a new branch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { $ } from "bun";
|
|
||||||
import { execFileSync } from "child_process";
|
import { execFileSync } from "child_process";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import type { ParsedGitHubContext } from "../context";
|
import type { ParsedGitHubContext } from "../context";
|
||||||
import type { GitHubPullRequest } from "../types";
|
import type { GitHubPullRequest } from "../types";
|
||||||
import type { Octokits } from "../api/client";
|
import type { Octokits } from "../api/client";
|
||||||
import type { FetchDataResult } from "../data/fetcher";
|
import type { FetchDataResult } from "../data/fetcher";
|
||||||
import { generateBranchName } from "../../utils/branch-template";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the first label from GitHub data, or returns undefined if no labels exist
|
|
||||||
*/
|
|
||||||
function extractFirstLabel(githubData: FetchDataResult): string | undefined {
|
|
||||||
const labels = githubData.contextData.labels?.nodes;
|
|
||||||
return labels && labels.length > 0 ? labels[0]?.name : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a git branch name against a strict whitelist pattern.
|
* Validates a git branch name against a strict whitelist pattern.
|
||||||
@@ -135,7 +125,7 @@ export async function setupBranch(
|
|||||||
): Promise<BranchInfo> {
|
): Promise<BranchInfo> {
|
||||||
const { owner, repo } = context.repository;
|
const { owner, repo } = context.repository;
|
||||||
const entityNumber = context.entityNumber;
|
const entityNumber = context.entityNumber;
|
||||||
const { baseBranch, branchPrefix, branchNameTemplate } = context.inputs;
|
const { baseBranch, branchPrefix } = context.inputs;
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
@@ -201,8 +191,17 @@ export async function setupBranch(
|
|||||||
// Generate branch name for either an issue or closed/merged PR
|
// Generate branch name for either an issue or closed/merged PR
|
||||||
const entityType = isPR ? "pr" : "issue";
|
const entityType = isPR ? "pr" : "issue";
|
||||||
|
|
||||||
// Get the SHA of the source branch to use in template
|
// Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format
|
||||||
let sourceSHA: string | undefined;
|
const now = new Date();
|
||||||
|
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
// Ensure branch name is Kubernetes-compatible:
|
||||||
|
// - Lowercase only
|
||||||
|
// - Alphanumeric with hyphens
|
||||||
|
// - No underscores
|
||||||
|
// - Max 50 chars (to allow for prefixes)
|
||||||
|
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
const newBranch = branchName.toLowerCase().substring(0, 50);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the source branch to verify it exists
|
// Get the SHA of the source branch to verify it exists
|
||||||
@@ -212,46 +211,8 @@ export async function setupBranch(
|
|||||||
ref: `heads/${sourceBranch}`,
|
ref: `heads/${sourceBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
sourceSHA = sourceBranchRef.data.object.sha;
|
const currentSHA = sourceBranchRef.data.object.sha;
|
||||||
console.log(`Source branch SHA: ${sourceSHA}`);
|
console.log(`Source branch SHA: ${currentSHA}`);
|
||||||
|
|
||||||
// Extract first label from GitHub data
|
|
||||||
const firstLabel = extractFirstLabel(githubData);
|
|
||||||
|
|
||||||
// Extract title from GitHub data
|
|
||||||
const title = githubData.contextData.title;
|
|
||||||
|
|
||||||
// Generate branch name using template or default format
|
|
||||||
let newBranch = generateBranchName(
|
|
||||||
branchNameTemplate,
|
|
||||||
branchPrefix,
|
|
||||||
entityType,
|
|
||||||
entityNumber,
|
|
||||||
sourceSHA,
|
|
||||||
firstLabel,
|
|
||||||
title,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if generated branch already exists on remote
|
|
||||||
try {
|
|
||||||
await $`git ls-remote --exit-code origin refs/heads/${newBranch}`.quiet();
|
|
||||||
|
|
||||||
// If we get here, branch exists (exit code 0)
|
|
||||||
console.log(
|
|
||||||
`Branch '${newBranch}' already exists, falling back to default format`,
|
|
||||||
);
|
|
||||||
newBranch = generateBranchName(
|
|
||||||
undefined, // Force default template
|
|
||||||
branchPrefix,
|
|
||||||
entityType,
|
|
||||||
entityNumber,
|
|
||||||
sourceSHA,
|
|
||||||
firstLabel,
|
|
||||||
title,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Branch doesn't exist (non-zero exit code), continue with generated name
|
|
||||||
}
|
|
||||||
|
|
||||||
// For commit signing, defer branch creation to the file ops server
|
// For commit signing, defer branch creation to the file ops server
|
||||||
if (context.inputs.useCommitSigning) {
|
if (context.inputs.useCommitSigning) {
|
||||||
|
|||||||
@@ -63,11 +63,6 @@ export type GitHubPullRequest = {
|
|||||||
additions: number;
|
additions: number;
|
||||||
deletions: number;
|
deletions: number;
|
||||||
state: string;
|
state: string;
|
||||||
labels: {
|
|
||||||
nodes: Array<{
|
|
||||||
name: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
commits: {
|
commits: {
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
nodes: Array<{
|
nodes: Array<{
|
||||||
@@ -93,11 +88,6 @@ export type GitHubIssue = {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
lastEditedAt?: string;
|
lastEditedAt?: string;
|
||||||
state: string;
|
state: string;
|
||||||
labels: {
|
|
||||||
nodes: Array<{
|
|
||||||
name: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
comments: {
|
comments: {
|
||||||
nodes: GitHubComment[];
|
nodes: GitHubComment[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
import type { GitHubContext } from "../context";
|
import type { ParsedGitHubContext } from "../context";
|
||||||
|
|
||||||
export async function checkHumanActor(
|
export async function checkHumanActor(
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
githubContext: GitHubContext,
|
githubContext: ParsedGitHubContext,
|
||||||
) {
|
) {
|
||||||
// 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,7 +8,6 @@ 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";
|
||||||
|
|
||||||
@@ -81,14 +80,7 @@ export const agentMode: Mode = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepare({
|
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||||
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,33 +1,22 @@
|
|||||||
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+"([^"]+)"/g, // Double quoted
|
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/, // Double quoted
|
||||||
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/g, // Single quoted
|
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/, // Single quoted
|
||||||
/--(?:allowedTools|allowed-tools)\s+([^'"\s][^\s]*)/g, // Unquoted (must not start with quote)
|
/--(?:allowedTools|allowed-tools)\s+([^\s]+)/, // Unquoted
|
||||||
];
|
];
|
||||||
|
|
||||||
const tools: string[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
for (const match of claudeArgs.matchAll(pattern)) {
|
const match = claudeArgs.match(pattern);
|
||||||
if (match[1]) {
|
if (match && match[1]) {
|
||||||
// Don't add if the value starts with -- (another flag)
|
// Don't return if the value starts with -- (another flag)
|
||||||
if (match[1].startsWith("--")) {
|
if (match[1].startsWith("--")) {
|
||||||
continue;
|
return [];
|
||||||
}
|
|
||||||
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 tools;
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Branch name template parsing and variable substitution utilities
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NUM_DESCRIPTION_WORDS = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the first 5 words from a title and converts them to kebab-case
|
|
||||||
*/
|
|
||||||
function extractDescription(
|
|
||||||
title: string,
|
|
||||||
numWords: number = NUM_DESCRIPTION_WORDS,
|
|
||||||
): string {
|
|
||||||
if (!title || title.trim() === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return title
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.slice(0, numWords) // Only first `numWords` words
|
|
||||||
.join("-")
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9-]/g, "") // Remove non-alphanumeric except hyphens
|
|
||||||
.replace(/-+/g, "-") // Replace multiple hyphens with single
|
|
||||||
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BranchTemplateVariables {
|
|
||||||
prefix: string;
|
|
||||||
entityType: string;
|
|
||||||
entityNumber: number;
|
|
||||||
timestamp: string;
|
|
||||||
sha?: string;
|
|
||||||
label?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces template variables in a branch name template
|
|
||||||
* Template format: {{variableName}}
|
|
||||||
*/
|
|
||||||
export function applyBranchTemplate(
|
|
||||||
template: string,
|
|
||||||
variables: BranchTemplateVariables,
|
|
||||||
): string {
|
|
||||||
let result = template;
|
|
||||||
|
|
||||||
// Replace each variable
|
|
||||||
Object.entries(variables).forEach(([key, value]) => {
|
|
||||||
const placeholder = `{{${key}}}`;
|
|
||||||
const replacement = value ? String(value) : "";
|
|
||||||
result = result.replaceAll(placeholder, replacement);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a branch name from the provided `template` and set of `variables`. Uses a default format if the template is empty or produces an empty result.
|
|
||||||
*/
|
|
||||||
export function generateBranchName(
|
|
||||||
template: string | undefined,
|
|
||||||
branchPrefix: string,
|
|
||||||
entityType: string,
|
|
||||||
entityNumber: number,
|
|
||||||
sha?: string,
|
|
||||||
label?: string,
|
|
||||||
title?: string,
|
|
||||||
): string {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const variables: BranchTemplateVariables = {
|
|
||||||
prefix: branchPrefix,
|
|
||||||
entityType,
|
|
||||||
entityNumber,
|
|
||||||
timestamp: `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`,
|
|
||||||
sha: sha?.substring(0, 8), // First 8 characters of SHA
|
|
||||||
label: label || entityType, // Fall back to entityType if no label
|
|
||||||
description: title ? extractDescription(title) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (template?.trim()) {
|
|
||||||
const branchName = applyBranchTemplate(template, variables);
|
|
||||||
|
|
||||||
// Some templates could produce empty results- validate
|
|
||||||
if (branchName.trim().length > 0) return branchName;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Branch template '${template}' generated empty result, falling back to default format`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${variables.timestamp}`;
|
|
||||||
// Kubernetes compatible: lowercase, max 50 chars, alphanumeric and hyphens only
|
|
||||||
return branchName.toLowerCase().substring(0, 50);
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import {
|
|
||||||
applyBranchTemplate,
|
|
||||||
generateBranchName,
|
|
||||||
} from "../src/utils/branch-template";
|
|
||||||
|
|
||||||
describe("branch template utilities", () => {
|
|
||||||
describe("applyBranchTemplate", () => {
|
|
||||||
it("should replace all template variables", () => {
|
|
||||||
const template =
|
|
||||||
"{{prefix}}{{entityType}}-{{entityNumber}}-{{timestamp}}";
|
|
||||||
const variables = {
|
|
||||||
prefix: "feat/",
|
|
||||||
entityType: "issue",
|
|
||||||
entityNumber: 123,
|
|
||||||
timestamp: "20240301-1430",
|
|
||||||
sha: "abcd1234",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyBranchTemplate(template, variables);
|
|
||||||
expect(result).toBe("feat/issue-123-20240301-1430");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle custom templates with multiple variables", () => {
|
|
||||||
const template =
|
|
||||||
"{{prefix}}fix/{{entityType}}_{{entityNumber}}_{{timestamp}}_{{sha}}";
|
|
||||||
const variables = {
|
|
||||||
prefix: "claude-",
|
|
||||||
entityType: "pr",
|
|
||||||
entityNumber: 456,
|
|
||||||
timestamp: "20240301-1430",
|
|
||||||
sha: "abcd1234",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyBranchTemplate(template, variables);
|
|
||||||
expect(result).toBe("claude-fix/pr_456_20240301-1430_abcd1234");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle templates with missing variables gracefully", () => {
|
|
||||||
const template = "{{prefix}}{{entityType}}-{{missing}}-{{entityNumber}}";
|
|
||||||
const variables = {
|
|
||||||
prefix: "feat/",
|
|
||||||
entityType: "issue",
|
|
||||||
entityNumber: 123,
|
|
||||||
timestamp: "20240301-1430",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = applyBranchTemplate(template, variables);
|
|
||||||
expect(result).toBe("feat/issue-{{missing}}-123");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("generateBranchName", () => {
|
|
||||||
it("should use custom template when provided", () => {
|
|
||||||
const template = "{{prefix}}custom-{{entityType}}_{{entityNumber}}";
|
|
||||||
const result = generateBranchName(template, "feature/", "issue", 123);
|
|
||||||
|
|
||||||
expect(result).toBe("feature/custom-issue_123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use default format when template is empty", () => {
|
|
||||||
const result = generateBranchName("", "claude/", "issue", 123);
|
|
||||||
|
|
||||||
expect(result).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use default format when template is undefined", () => {
|
|
||||||
const result = generateBranchName(undefined, "claude/", "pr", 456);
|
|
||||||
|
|
||||||
expect(result).toMatch(/^claude\/pr-456-\d{8}-\d{4}$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve custom template formatting (no automatic lowercase/truncation)", () => {
|
|
||||||
const template = "{{prefix}}UPPERCASE_Branch-Name_{{entityNumber}}";
|
|
||||||
const result = generateBranchName(template, "Feature/", "issue", 123);
|
|
||||||
|
|
||||||
expect(result).toBe("Feature/UPPERCASE_Branch-Name_123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not truncate custom template results", () => {
|
|
||||||
const template =
|
|
||||||
"{{prefix}}very-long-branch-name-that-exceeds-the-maximum-allowed-length-{{entityNumber}}";
|
|
||||||
const result = generateBranchName(template, "feature/", "issue", 123);
|
|
||||||
|
|
||||||
expect(result).toBe(
|
|
||||||
"feature/very-long-branch-name-that-exceeds-the-maximum-allowed-length-123",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should apply Kubernetes-compatible transformations to default template only", () => {
|
|
||||||
const result = generateBranchName(undefined, "Feature/", "issue", 123);
|
|
||||||
|
|
||||||
expect(result).toMatch(/^feature\/issue-123-\d{8}-\d{4}$/);
|
|
||||||
expect(result.length).toBeLessThanOrEqual(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle SHA in template", () => {
|
|
||||||
const template = "{{prefix}}{{entityType}}-{{entityNumber}}-{{sha}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"fix/",
|
|
||||||
"pr",
|
|
||||||
789,
|
|
||||||
"abcdef123456",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe("fix/pr-789-abcdef12");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use label in template when provided", () => {
|
|
||||||
const template = "{{prefix}}{{label}}/{{entityNumber}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"feature/",
|
|
||||||
"issue",
|
|
||||||
123,
|
|
||||||
undefined,
|
|
||||||
"bug",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe("feature/bug/123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fallback to entityType when label template is used but no label provided", () => {
|
|
||||||
const template = "{{prefix}}{{label}}-{{entityNumber}}";
|
|
||||||
const result = generateBranchName(template, "fix/", "pr", 456);
|
|
||||||
|
|
||||||
expect(result).toBe("fix/pr-456");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle template with both label and entityType", () => {
|
|
||||||
const template = "{{prefix}}{{label}}-{{entityType}}_{{entityNumber}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"dev/",
|
|
||||||
"issue",
|
|
||||||
789,
|
|
||||||
undefined,
|
|
||||||
"enhancement",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe("dev/enhancement-issue_789");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use description in template when provided", () => {
|
|
||||||
const template = "{{prefix}}{{description}}/{{entityNumber}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"feature/",
|
|
||||||
"issue",
|
|
||||||
123,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"Fix login bug with OAuth",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe("feature/fix-login-bug-with-oauth/123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle template with multiple variables including description", () => {
|
|
||||||
const template =
|
|
||||||
"{{prefix}}{{label}}/{{description}}-{{entityType}}_{{entityNumber}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"dev/",
|
|
||||||
"issue",
|
|
||||||
456,
|
|
||||||
undefined,
|
|
||||||
"bug",
|
|
||||||
"User authentication fails completely",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe(
|
|
||||||
"dev/bug/user-authentication-fails-completely-issue_456",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle description with special characters in template", () => {
|
|
||||||
const template = "{{prefix}}{{description}}-{{entityNumber}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"fix/",
|
|
||||||
"pr",
|
|
||||||
789,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"Add: User Registration & Email Validation",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe("fix/add-user-registration-email-789");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should truncate descriptions to exactly 5 words", () => {
|
|
||||||
const result = generateBranchName(
|
|
||||||
"{{prefix}}{{description}}/{{entityNumber}}",
|
|
||||||
"feature/",
|
|
||||||
"issue",
|
|
||||||
999,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"This is a very long title with many more than five words in it",
|
|
||||||
);
|
|
||||||
expect(result).toBe("feature/this-is-a-very-long/999");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty description in template", () => {
|
|
||||||
const template = "{{prefix}}{{description}}-{{entityNumber}}";
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"test/",
|
|
||||||
"issue",
|
|
||||||
101,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe("test/-101");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fallback to default format when template produces empty result", () => {
|
|
||||||
const template = "{{description}}"; // Will be empty if no title provided
|
|
||||||
const result = generateBranchName(template, "claude/", "issue", 123);
|
|
||||||
|
|
||||||
expect(result).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
|
||||||
expect(result.length).toBeLessThanOrEqual(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fallback to default format when template produces only whitespace", () => {
|
|
||||||
const template = " {{description}} "; // Will be " " if description is empty
|
|
||||||
const result = generateBranchName(
|
|
||||||
template,
|
|
||||||
"fix/",
|
|
||||||
"pr",
|
|
||||||
456,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toMatch(/^fix\/pr-456-\d{8}-\d{4}$/);
|
|
||||||
expect(result.length).toBeLessThanOrEqual(50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -61,7 +61,6 @@ describe("generatePrompt", () => {
|
|||||||
body: "This is a test PR",
|
body: "This is a test PR",
|
||||||
author: { login: "testuser" },
|
author: { login: "testuser" },
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
labels: { nodes: [] },
|
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
additions: 15,
|
additions: 15,
|
||||||
deletions: 5,
|
deletions: 5,
|
||||||
@@ -476,7 +475,6 @@ describe("generatePrompt", () => {
|
|||||||
body: "The login form is not working",
|
body: "The login form is not working",
|
||||||
author: { login: "testuser" },
|
author: { login: "testuser" },
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
labels: { nodes: [] },
|
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
comments: {
|
comments: {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ describe("formatContext", () => {
|
|||||||
additions: 50,
|
additions: 50,
|
||||||
deletions: 30,
|
deletions: 30,
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
labels: {
|
|
||||||
nodes: [],
|
|
||||||
},
|
|
||||||
commits: {
|
commits: {
|
||||||
totalCount: 3,
|
totalCount: 3,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -66,9 +63,6 @@ Changed Files: 2 files`,
|
|||||||
author: { login: "test-user" },
|
author: { login: "test-user" },
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
labels: {
|
|
||||||
nodes: [],
|
|
||||||
},
|
|
||||||
comments: {
|
comments: {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -145,12 +145,12 @@ describe("Agent Mode", () => {
|
|||||||
users: {
|
users: {
|
||||||
getAuthenticated: mock(() =>
|
getAuthenticated: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345, type: "User" },
|
data: { login: "test-user", id: 12345 },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
getByUsername: mock(() =>
|
getByUsername: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345, type: "User" },
|
data: { login: "test-user", id: 12345 },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -187,65 +187,6 @@ 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",
|
||||||
@@ -258,12 +199,12 @@ describe("Agent Mode", () => {
|
|||||||
users: {
|
users: {
|
||||||
getAuthenticated: mock(() =>
|
getAuthenticated: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345, type: "User" },
|
data: { login: "test-user", id: 12345 },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
getByUsername: mock(() =>
|
getByUsername: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345, type: "User" },
|
data: { login: "test-user", id: 12345 },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,44 +35,12 @@ describe("parseAllowedTools", () => {
|
|||||||
expect(parseAllowedTools("")).toEqual([]);
|
expect(parseAllowedTools("")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles --allowedTools followed by another --allowedTools flag", () => {
|
test("handles duplicate --allowedTools flags", () => {
|
||||||
const args = "--allowedTools --allowedTools mcp__github__*";
|
const args = "--allowedTools --allowedTools mcp__github__*";
|
||||||
// The second --allowedTools is consumed as a value of the first, then skipped.
|
// Should not match the first one since the value is another flag
|
||||||
// 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([]);
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ describe("pull_request_target event support", () => {
|
|||||||
},
|
},
|
||||||
comments: { nodes: [] },
|
comments: { nodes: [] },
|
||||||
reviews: { nodes: [] },
|
reviews: { nodes: [] },
|
||||||
labels: { nodes: [] },
|
|
||||||
},
|
},
|
||||||
comments: [],
|
comments: [],
|
||||||
changedFiles: [],
|
changedFiles: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user