mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
6 Commits
claude/sla
...
fix-tag-sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2565b54417 | ||
|
|
a9bd2f53bf | ||
|
|
5875a49630 | ||
|
|
1cb675955b | ||
|
|
a9171f0ced | ||
|
|
4778aeae4c |
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -122,20 +122,21 @@ jobs:
|
||||
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Create and push tag
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
set -e # Exit on any error
|
||||
next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# # Create the version tag
|
||||
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||
# git push origin "$next_version"
|
||||
# Create the version tag
|
||||
git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||
git push origin "$next_version"
|
||||
|
||||
# # Update the beta tag
|
||||
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
||||
# git push origin beta --force
|
||||
# Update the v1 tag
|
||||
git tag -fa v1 -m "Update v1 tag to ${next_version}"
|
||||
git push origin v1 --force
|
||||
|
||||
# - name: Create GitHub release
|
||||
# env:
|
||||
|
||||
@@ -213,7 +213,7 @@ runs:
|
||||
|
||||
# Install Claude Code if no custom executable is provided
|
||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||
CLAUDE_CODE_VERSION="2.1.4"
|
||||
CLAUDE_CODE_VERSION="2.1.7"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
|
||||
@@ -11,7 +11,7 @@ Add the following to your workflow file:
|
||||
```yaml
|
||||
# Using a direct prompt
|
||||
- name: Run Claude Code with direct prompt
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
|
||||
@@ -19,7 +19,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Or using a prompt from a file
|
||||
- name: Run Claude Code with prompt file
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt_file: "/path/to/prompt.txt"
|
||||
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
|
||||
@@ -27,7 +27,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Or limiting the conversation turns
|
||||
- name: Run Claude Code with limited turns
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
|
||||
@@ -36,7 +36,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Using custom system prompts
|
||||
- name: Run Claude Code with custom system prompt
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Build a REST API"
|
||||
system_prompt: "You are a senior backend engineer. Focus on security, performance, and maintainability."
|
||||
@@ -45,7 +45,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Or appending to the default system prompt
|
||||
- name: Run Claude Code with appended system prompt
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Create a database schema"
|
||||
append_system_prompt: "After writing code, be sure to code review yourself."
|
||||
@@ -54,7 +54,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Using custom environment variables
|
||||
- name: Run Claude Code with custom environment variables
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Deploy to staging environment"
|
||||
claude_env: |
|
||||
@@ -66,7 +66,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Using fallback model for handling API errors
|
||||
- name: Run Claude Code with fallback model
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Review and fix TypeScript errors"
|
||||
model: "claude-opus-4-1-20250805"
|
||||
@@ -76,7 +76,7 @@ Add the following to your workflow file:
|
||||
|
||||
# Using OAuth token instead of API key
|
||||
- name: Run Claude Code with OAuth token
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Update dependencies"
|
||||
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
|
||||
@@ -130,7 +130,7 @@ Example usage:
|
||||
|
||||
```yaml
|
||||
- name: Run Claude Code with Node.js 20
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
env:
|
||||
NODE_VERSION: "20.x"
|
||||
with:
|
||||
@@ -146,7 +146,7 @@ The `claude_env` input accepts YAML multiline format with key-value pairs:
|
||||
|
||||
```yaml
|
||||
- name: Deploy with custom environment
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Deploy the application to the staging environment"
|
||||
claude_env: |
|
||||
@@ -200,7 +200,7 @@ Provide a path to a JSON file containing Claude Code settings:
|
||||
|
||||
```yaml
|
||||
- name: Run Claude Code with settings file
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
settings: "path/to/settings.json"
|
||||
@@ -214,7 +214,7 @@ Provide the settings configuration directly as a JSON string:
|
||||
|
||||
```yaml
|
||||
- name: Run Claude Code with inline settings
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
settings: |
|
||||
@@ -263,7 +263,7 @@ Provide a path to a JSON file containing MCP configuration:
|
||||
|
||||
```yaml
|
||||
- name: Run Claude Code with MCP config file
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
mcp_config: "path/to/mcp-config.json"
|
||||
@@ -277,7 +277,7 @@ Provide the MCP configuration directly as a JSON string:
|
||||
|
||||
```yaml
|
||||
- name: Run Claude Code with inline MCP config
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
mcp_config: |
|
||||
@@ -317,7 +317,7 @@ You can combine MCP config with other inputs like allowed tools:
|
||||
```yaml
|
||||
# Using multiple inputs together
|
||||
- name: Run Claude Code with MCP and custom tools
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Access the custom MCP server and use its tools"
|
||||
mcp_config: "mcp-config.json"
|
||||
@@ -345,7 +345,7 @@ jobs:
|
||||
|
||||
- name: Run Code Review with Claude
|
||||
id: code-review
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Review the PR changes. Focus on code quality, potential bugs, and performance issues. Suggest improvements where appropriate. Write your review as markdown text."
|
||||
allowed_tools: "Bash(git diff --name-only HEAD~1),Bash(git diff HEAD~1),View,GlobTool,GrepTool,Write"
|
||||
@@ -408,7 +408,7 @@ Use provider-specific model names based on your chosen provider:
|
||||
```yaml
|
||||
# For direct Anthropic API (default)
|
||||
- name: Run Claude Code with Anthropic API
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
model: "claude-3-7-sonnet-20250219"
|
||||
@@ -422,7 +422,7 @@ Use provider-specific model names based on your chosen provider:
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Run Claude Code with Bedrock
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
model: "anthropic.claude-3-7-sonnet-20250219-v1:0"
|
||||
@@ -436,7 +436,7 @@ Use provider-specific model names based on your chosen provider:
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Run Claude Code with Vertex AI
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
model: "claude-3-7-sonnet@20250219"
|
||||
@@ -455,7 +455,7 @@ This example shows how to use OIDC authentication with AWS Bedrock:
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Run Claude Code with AWS OIDC
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
use_bedrock: "true"
|
||||
@@ -475,7 +475,7 @@ This example shows how to use OIDC authentication with GCP Vertex AI:
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Run Claude Code with GCP OIDC
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: "Your prompt here"
|
||||
use_vertex: "true"
|
||||
|
||||
@@ -124,7 +124,7 @@ runs:
|
||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
run: |
|
||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||
CLAUDE_CODE_VERSION="2.1.4"
|
||||
CLAUDE_CODE_VERSION="2.1.7"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"name": "@anthropic-ai/claude-code-base-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
||||
"shell-quote": "^1.8.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
"@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.4", "", { "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-5RpMO8aLEwuAd8h7/QHMCKzdVSihZCtHGnouPp+Isvc7zPzQXKb6GvUitkbs3wIBgIbXA/vXQmIi126uw9qo0A=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.7", "", { "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-I1/zcnLah74kZeRkj/1QnDaC6ItJ2m/Bftlm25uoaRkZx7i7SkcpqM9jGE/r2A8PMxnw5WpabP60Xgj99CrTuw=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
||||
"shell-quote": "^1.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
4
bun.lock
4
bun.lock
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
"@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.4", "", { "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-5RpMO8aLEwuAd8h7/QHMCKzdVSihZCtHGnouPp+Isvc7zPzQXKb6GvUitkbs3wIBgIbXA/vXQmIi126uw9qo0A=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.7", "", { "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-I1/zcnLah74kZeRkj/1QnDaC6ItJ2m/Bftlm25uoaRkZx7i7SkcpqM9jGE/r2A8PMxnw5WpabP60Xgj99CrTuw=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
|
||||
@@ -10,11 +10,6 @@ import {
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../context";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import {
|
||||
detectActionableSuggestion,
|
||||
isCommentActionableForAutofix,
|
||||
type ActionableSuggestionResult,
|
||||
} from "../../utils/detect-actionable-suggestion";
|
||||
|
||||
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||
const {
|
||||
@@ -151,89 +146,3 @@ export async function checkTriggerAction(context: ParsedGitHubContext) {
|
||||
core.setOutput("contains_trigger", containsTrigger.toString());
|
||||
return containsTrigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the context contains an actionable suggestion that can be automatically fixed.
|
||||
* This is useful for autofix workflows that want to respond to code review suggestions,
|
||||
* even when they come from bot accounts like claude[bot].
|
||||
*
|
||||
* @param context - The parsed GitHub context
|
||||
* @returns Detection result with confidence level and reason
|
||||
*/
|
||||
export function checkContainsActionableSuggestion(
|
||||
context: ParsedGitHubContext,
|
||||
): ActionableSuggestionResult {
|
||||
// Extract comment body based on event type
|
||||
let commentBody: string | undefined;
|
||||
|
||||
if (isPullRequestReviewCommentEvent(context)) {
|
||||
commentBody = context.payload.comment.body;
|
||||
} else if (isIssueCommentEvent(context)) {
|
||||
commentBody = context.payload.comment.body;
|
||||
} else if (isPullRequestReviewEvent(context)) {
|
||||
commentBody = context.payload.review.body ?? undefined;
|
||||
}
|
||||
|
||||
return detectActionableSuggestion(commentBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced trigger check that also considers actionable suggestions.
|
||||
* This function first checks for the standard trigger phrase, and if not found,
|
||||
* optionally checks for actionable suggestions when `checkSuggestions` is true.
|
||||
*
|
||||
* @param context - The parsed GitHub context
|
||||
* @param checkSuggestions - Whether to also check for actionable suggestions (default: false)
|
||||
* @returns Whether the action should be triggered
|
||||
*/
|
||||
export function checkContainsTriggerOrActionableSuggestion(
|
||||
context: ParsedGitHubContext,
|
||||
checkSuggestions: boolean = false,
|
||||
): boolean {
|
||||
// First, check for standard trigger
|
||||
if (checkContainsTrigger(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If checkSuggestions is enabled, also check for actionable suggestions
|
||||
if (checkSuggestions) {
|
||||
const suggestionResult = checkContainsActionableSuggestion(context);
|
||||
if (suggestionResult.isActionable) {
|
||||
console.log(
|
||||
`Comment contains actionable suggestion: ${suggestionResult.reason} (confidence: ${suggestionResult.confidence})`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a PR comment is actionable for autofix purposes.
|
||||
* This is a convenience function for workflows that want to automatically
|
||||
* apply suggestions from code review comments.
|
||||
*
|
||||
* @param context - The parsed GitHub context
|
||||
* @returns Whether the comment should be treated as actionable for autofix
|
||||
*/
|
||||
export function checkIsActionableForAutofix(
|
||||
context: ParsedGitHubContext,
|
||||
): boolean {
|
||||
// Only applicable to PR review comment events
|
||||
if (!isPullRequestReviewCommentEvent(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commentBody = context.payload.comment.body;
|
||||
const authorUsername = context.payload.comment.user?.login;
|
||||
|
||||
return isCommentActionableForAutofix(commentBody, authorUsername);
|
||||
}
|
||||
|
||||
// Re-export the types and functions from the utility module for convenience
|
||||
export {
|
||||
detectActionableSuggestion,
|
||||
isCommentActionableForAutofix,
|
||||
type ActionableSuggestionResult,
|
||||
} from "../../utils/detect-actionable-suggestion";
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Detects if a PR comment contains actionable suggestions that can be automatically fixed.
|
||||
*
|
||||
* This module identifies:
|
||||
* 1. GitHub inline committable suggestions (```suggestion blocks)
|
||||
* 2. Clear bug fix suggestions with specific patterns
|
||||
* 3. Code fix recommendations with explicit changes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Patterns that indicate a comment contains a GitHub inline committable suggestion.
|
||||
* These are code blocks that GitHub renders with a "Commit suggestion" button.
|
||||
*/
|
||||
const COMMITTABLE_SUGGESTION_PATTERN = /```suggestion\b[\s\S]*?```/i;
|
||||
|
||||
/**
|
||||
* Patterns that indicate a clear, actionable bug fix suggestion.
|
||||
* These phrases typically precede concrete fix recommendations.
|
||||
*/
|
||||
const BUG_FIX_PATTERNS = [
|
||||
// Direct fix suggestions
|
||||
/\bshould\s+(?:be|use|return|change\s+to)\b/i,
|
||||
/\bchange\s+(?:this\s+)?to\b/i,
|
||||
/\breplace\s+(?:this\s+)?with\b/i,
|
||||
/\buse\s+(?:this\s+)?instead\b/i,
|
||||
/\binstead\s+of\s+.*?,?\s*use\b/i,
|
||||
|
||||
// Bug identification with fix
|
||||
/\b(?:bug|issue|error|problem):\s*.*(?:fix|change|update|replace)/i,
|
||||
/\bfix(?:ed)?\s+by\s+(?:chang|replac|updat)/i,
|
||||
/\bto\s+fix\s+(?:this|the)\b/i,
|
||||
|
||||
// Explicit code changes
|
||||
/\bthe\s+(?:correct|proper|right)\s+(?:code|syntax|value|approach)\s+(?:is|would\s+be)\b/i,
|
||||
/\bshould\s+(?:read|look\s+like)\b/i,
|
||||
|
||||
// Missing/wrong patterns
|
||||
/\bmissing\s+(?:a\s+)?(?:semicolon|bracket|parenthesis|quote|import|return|await)\b/i,
|
||||
/\bextra\s+(?:semicolon|bracket|parenthesis|quote)\b/i,
|
||||
/\bwrong\s+(?:type|value|variable|import|parameter)\b/i,
|
||||
/\btypo\s+(?:in|here)\b/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* Patterns that suggest code alternatives (less strong than direct fixes but still actionable).
|
||||
*/
|
||||
const CODE_ALTERNATIVE_PATTERNS = [
|
||||
/```[\w]*\n[\s\S]+?\n```/, // Any code block (might contain the fix)
|
||||
/\b(?:try|consider)\s+(?:using|changing|replacing)\b/i,
|
||||
/\bhere'?s?\s+(?:the|a)\s+(?:fix|solution|correction)\b/i,
|
||||
/\b(?:correct|fixed|updated)\s+(?:version|code|implementation)\b/i,
|
||||
];
|
||||
|
||||
export interface ActionableSuggestionResult {
|
||||
/** Whether the comment contains an actionable suggestion */
|
||||
isActionable: boolean;
|
||||
/** Whether the comment contains a GitHub inline committable suggestion */
|
||||
hasCommittableSuggestion: boolean;
|
||||
/** Whether the comment contains clear bug fix language */
|
||||
hasBugFixSuggestion: boolean;
|
||||
/** Whether the comment contains code alternatives */
|
||||
hasCodeAlternative: boolean;
|
||||
/** Confidence level: 'high', 'medium', or 'low' */
|
||||
confidence: "high" | "medium" | "low";
|
||||
/** Reason for the determination */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a comment contains actionable suggestions that can be automatically fixed.
|
||||
*
|
||||
* @param commentBody - The body of the PR comment to analyze
|
||||
* @returns Object with detection results and confidence level
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = detectActionableSuggestion("```suggestion\nfixed code\n```");
|
||||
* // { isActionable: true, hasCommittableSuggestion: true, confidence: 'high', ... }
|
||||
*
|
||||
* const result2 = detectActionableSuggestion("You should use `const` instead of `let` here");
|
||||
* // { isActionable: true, hasBugFixSuggestion: true, confidence: 'medium', ... }
|
||||
* ```
|
||||
*/
|
||||
export function detectActionableSuggestion(
|
||||
commentBody: string | undefined | null,
|
||||
): ActionableSuggestionResult {
|
||||
if (!commentBody) {
|
||||
return {
|
||||
isActionable: false,
|
||||
hasCommittableSuggestion: false,
|
||||
hasBugFixSuggestion: false,
|
||||
hasCodeAlternative: false,
|
||||
confidence: "low",
|
||||
reason: "Empty or missing comment body",
|
||||
};
|
||||
}
|
||||
|
||||
// Check for GitHub inline committable suggestion (highest confidence)
|
||||
const hasCommittableSuggestion =
|
||||
COMMITTABLE_SUGGESTION_PATTERN.test(commentBody);
|
||||
if (hasCommittableSuggestion) {
|
||||
return {
|
||||
isActionable: true,
|
||||
hasCommittableSuggestion: true,
|
||||
hasBugFixSuggestion: false,
|
||||
hasCodeAlternative: false,
|
||||
confidence: "high",
|
||||
reason: "Contains GitHub inline committable suggestion (```suggestion)",
|
||||
};
|
||||
}
|
||||
|
||||
// Check for clear bug fix patterns (medium-high confidence)
|
||||
const matchedBugFixPattern = BUG_FIX_PATTERNS.find((pattern) =>
|
||||
pattern.test(commentBody),
|
||||
);
|
||||
if (matchedBugFixPattern) {
|
||||
// Higher confidence if also contains a code block
|
||||
const hasCodeBlock = CODE_ALTERNATIVE_PATTERNS[0].test(commentBody);
|
||||
return {
|
||||
isActionable: true,
|
||||
hasCommittableSuggestion: false,
|
||||
hasBugFixSuggestion: true,
|
||||
hasCodeAlternative: hasCodeBlock,
|
||||
confidence: hasCodeBlock ? "high" : "medium",
|
||||
reason: hasCodeBlock
|
||||
? "Contains clear bug fix suggestion with code example"
|
||||
: "Contains clear bug fix suggestion",
|
||||
};
|
||||
}
|
||||
|
||||
// Check for code alternatives (medium confidence)
|
||||
const matchedAlternativePattern = CODE_ALTERNATIVE_PATTERNS.find((pattern) =>
|
||||
pattern.test(commentBody),
|
||||
);
|
||||
if (matchedAlternativePattern) {
|
||||
return {
|
||||
isActionable: true,
|
||||
hasCommittableSuggestion: false,
|
||||
hasBugFixSuggestion: false,
|
||||
hasCodeAlternative: true,
|
||||
confidence: "medium",
|
||||
reason: "Contains code alternative or fix suggestion",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isActionable: false,
|
||||
hasCommittableSuggestion: false,
|
||||
hasBugFixSuggestion: false,
|
||||
hasCodeAlternative: false,
|
||||
confidence: "low",
|
||||
reason: "No actionable suggestion patterns detected",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a comment should be treated as actionable for autofix purposes,
|
||||
* even if it comes from a bot account like claude[bot].
|
||||
*
|
||||
* This is particularly useful for workflows that want to automatically apply
|
||||
* suggestions from code review comments.
|
||||
*
|
||||
* @param commentBody - The body of the PR comment
|
||||
* @param authorUsername - The username of the comment author
|
||||
* @returns Whether the comment should be treated as actionable
|
||||
*/
|
||||
export function isCommentActionableForAutofix(
|
||||
commentBody: string | undefined | null,
|
||||
authorUsername?: string,
|
||||
): boolean {
|
||||
const result = detectActionableSuggestion(commentBody);
|
||||
|
||||
// If it's already clearly actionable (high confidence), return true
|
||||
if (result.confidence === "high") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For medium confidence, be more lenient
|
||||
if (result.confidence === "medium" && result.isActionable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the suggested code from a GitHub inline committable suggestion block.
|
||||
*
|
||||
* @param commentBody - The body of the PR comment
|
||||
* @returns The suggested code content, or null if no suggestion block found
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const code = extractSuggestionCode("```suggestion\nconst x = 1;\n```");
|
||||
* // "const x = 1;"
|
||||
* ```
|
||||
*/
|
||||
export function extractSuggestionCode(
|
||||
commentBody: string | undefined | null,
|
||||
): string | null {
|
||||
if (!commentBody) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = commentBody.match(/```suggestion\b\n?([\s\S]*?)```/i);
|
||||
if (match && match[1] !== undefined) {
|
||||
return match[1].trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
detectActionableSuggestion,
|
||||
isCommentActionableForAutofix,
|
||||
extractSuggestionCode,
|
||||
} from "../src/utils/detect-actionable-suggestion";
|
||||
|
||||
describe("detectActionableSuggestion", () => {
|
||||
describe("GitHub inline committable suggestions", () => {
|
||||
it("should detect suggestion blocks with high confidence", () => {
|
||||
const comment = `Here's a fix:
|
||||
\`\`\`suggestion
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasCommittableSuggestion).toBe(true);
|
||||
expect(result.confidence).toBe("high");
|
||||
expect(result.reason).toContain("committable suggestion");
|
||||
});
|
||||
|
||||
it("should detect suggestion blocks with multiple lines", () => {
|
||||
const comment = `\`\`\`suggestion
|
||||
function foo() {
|
||||
return bar();
|
||||
}
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasCommittableSuggestion).toBe(true);
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
|
||||
it("should detect suggestion blocks case-insensitively", () => {
|
||||
const comment = `\`\`\`SUGGESTION
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasCommittableSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it("should not confuse regular code blocks with suggestion blocks", () => {
|
||||
const comment = `\`\`\`javascript
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.hasCommittableSuggestion).toBe(false);
|
||||
// But it should still detect the code alternative
|
||||
expect(result.hasCodeAlternative).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bug fix suggestions", () => {
|
||||
it('should detect "should be" patterns', () => {
|
||||
const comment = "This should be `const` instead of `let`";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
expect(result.confidence).toBe("medium");
|
||||
});
|
||||
|
||||
it('should detect "change to" patterns', () => {
|
||||
const comment = "Change this to use async/await";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "replace with" patterns', () => {
|
||||
const comment = "Replace this with Array.from()";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "use instead" patterns', () => {
|
||||
const comment = "Use this instead of the deprecated method";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "instead of X, use Y" patterns', () => {
|
||||
const comment = "Instead of forEach, use map";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "to fix this" patterns', () => {
|
||||
const comment = "To fix this, you need to add the await keyword";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "the correct code is" patterns', () => {
|
||||
const comment = "The correct code would be: return null;";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "missing semicolon" patterns', () => {
|
||||
const comment = "Missing a semicolon at the end";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "typo" patterns', () => {
|
||||
const comment = "Typo here: teh should be the";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "wrong type" patterns', () => {
|
||||
const comment = "Wrong type here, should be string not number";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it("should have high confidence when bug fix suggestion includes code block", () => {
|
||||
const comment = `You should use const here:
|
||||
\`\`\`javascript
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasBugFixSuggestion).toBe(true);
|
||||
expect(result.hasCodeAlternative).toBe(true);
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
});
|
||||
|
||||
describe("code alternatives", () => {
|
||||
it('should detect "try using" patterns', () => {
|
||||
const comment = "Try using Array.map() instead";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "here\'s the fix" patterns', () => {
|
||||
const comment = "Here's the fix for this issue";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect code blocks as potential alternatives", () => {
|
||||
const comment = `Try this approach:
|
||||
\`\`\`
|
||||
const result = [];
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasCodeAlternative).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-actionable comments", () => {
|
||||
it("should not flag general questions", () => {
|
||||
const comment = "Why is this returning undefined?";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(false);
|
||||
expect(result.confidence).toBe("low");
|
||||
});
|
||||
|
||||
it("should not flag simple observations", () => {
|
||||
const comment = "This looks interesting";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(false);
|
||||
});
|
||||
|
||||
it("should not flag approval comments", () => {
|
||||
const comment = "LGTM! :+1:";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle empty comments", () => {
|
||||
const result = detectActionableSuggestion("");
|
||||
expect(result.isActionable).toBe(false);
|
||||
expect(result.reason).toContain("Empty");
|
||||
});
|
||||
|
||||
it("should handle null comments", () => {
|
||||
const result = detectActionableSuggestion(null);
|
||||
expect(result.isActionable).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle undefined comments", () => {
|
||||
const result = detectActionableSuggestion(undefined);
|
||||
expect(result.isActionable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle comments with both suggestion block and bug fix language", () => {
|
||||
const comment = `This should be fixed. Here's the suggestion:
|
||||
\`\`\`suggestion
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
// Suggestion block takes precedence (high confidence)
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasCommittableSuggestion).toBe(true);
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
|
||||
it("should handle very long comments", () => {
|
||||
const longContent = "a".repeat(10000);
|
||||
const comment = `${longContent}
|
||||
\`\`\`suggestion
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
expect(result.hasCommittableSuggestion).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle comments with special characters", () => {
|
||||
const comment =
|
||||
"You should be using `const` here! @#$%^&* Change this to `let`";
|
||||
const result = detectActionableSuggestion(comment);
|
||||
expect(result.isActionable).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCommentActionableForAutofix", () => {
|
||||
it("should return true for high confidence suggestions", () => {
|
||||
const comment = `\`\`\`suggestion
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
expect(isCommentActionableForAutofix(comment)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for medium confidence suggestions", () => {
|
||||
const comment = "You should use const here instead of let";
|
||||
expect(isCommentActionableForAutofix(comment)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-actionable comments", () => {
|
||||
const comment = "This looks fine to me";
|
||||
expect(isCommentActionableForAutofix(comment)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle bot authors correctly", () => {
|
||||
const comment = `\`\`\`suggestion
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
// Should still return true even for bot authors
|
||||
expect(isCommentActionableForAutofix(comment, "claude[bot]")).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle empty comments", () => {
|
||||
expect(isCommentActionableForAutofix("")).toBe(false);
|
||||
expect(isCommentActionableForAutofix(null)).toBe(false);
|
||||
expect(isCommentActionableForAutofix(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractSuggestionCode", () => {
|
||||
it("should extract code from suggestion block", () => {
|
||||
const comment = `Here's a fix:
|
||||
\`\`\`suggestion
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
expect(extractSuggestionCode(comment)).toBe("const x = 1;");
|
||||
});
|
||||
|
||||
it("should extract multi-line code from suggestion block", () => {
|
||||
const comment = `\`\`\`suggestion
|
||||
function foo() {
|
||||
return bar();
|
||||
}
|
||||
\`\`\``;
|
||||
expect(extractSuggestionCode(comment)).toBe(
|
||||
"function foo() {\n return bar();\n}",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty suggestion blocks", () => {
|
||||
const comment = `\`\`\`suggestion
|
||||
\`\`\``;
|
||||
expect(extractSuggestionCode(comment)).toBe("");
|
||||
});
|
||||
|
||||
it("should return null for comments without suggestion blocks", () => {
|
||||
const comment = "Just a regular comment";
|
||||
expect(extractSuggestionCode(comment)).toBe(null);
|
||||
});
|
||||
|
||||
it("should return null for empty comments", () => {
|
||||
expect(extractSuggestionCode("")).toBe(null);
|
||||
expect(extractSuggestionCode(null)).toBe(null);
|
||||
expect(extractSuggestionCode(undefined)).toBe(null);
|
||||
});
|
||||
|
||||
it("should not extract from regular code blocks", () => {
|
||||
const comment = `\`\`\`javascript
|
||||
const x = 1;
|
||||
\`\`\``;
|
||||
expect(extractSuggestionCode(comment)).toBe(null);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user