Compare commits

..

6 Commits

Author SHA1 Message Date
JB
2565b54417 Fix build 2026-01-14 10:44:43 -05:00
claude[bot]
a9bd2f53bf docs: update base-action README examples from @beta to @v1
Co-authored-by: JB <hackyon-anthropic@users.noreply.github.com>
2026-01-14 15:09:59 +00:00
JB
5875a49630 Add error handling in create and push tag
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-01-14 10:07:55 -05:00
JB
1cb675955b fix: re-enable tag creation and push step in release workflow
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 3
Claude-Escapes: 0
2026-01-14 09:47:51 -05:00
GitHub Actions
a9171f0ced chore: bump Claude Code to 2.1.7 and Agent SDK to 0.2.7 2026-01-14 00:03:29 +00:00
GitHub Actions
4778aeae4c chore: bump Claude Code to 2.1.6 and Agent SDK to 0.2.6 2026-01-13 02:25:17 +00:00
11 changed files with 41 additions and 653 deletions

View File

@@ -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:

View File

@@ -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..."

View File

@@ -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"

View File

@@ -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..."

View File

@@ -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=="],

View File

@@ -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": {

View File

@@ -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=="],

View File

@@ -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",

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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);
});
});