Files
claude-code-action/src/github/validation/trigger.ts
Claude b868c24c84 feat: improve autofix suggestion detection for PR comments
Add detection logic to recognize actionable suggestions in PR comments, even when
they come from bot accounts like claude[bot]. This addresses the issue where the
autofix workflow incorrectly classified clear bug fix suggestions as not actionable.

New features:
- detectActionableSuggestion(): Analyzes comment body for actionable patterns
- Detects GitHub inline committable suggestions (```suggestion blocks)
- Detects clear bug fix language patterns (e.g., "should be", "change to", "replace with")
- Detects code alternatives and fix recommendations
- Returns confidence level (high/medium/low) and reason for the determination
- checkContainsActionableSuggestion(): Context-aware wrapper for GitHub events
- checkContainsTriggerOrActionableSuggestion(): Combined trigger and suggestion check
- checkIsActionableForAutofix(): Convenience function for autofix workflows
- extractSuggestionCode(): Extracts code from GitHub suggestion blocks

This enables workflows to automatically apply suggestions from code review comments,
improving the developer experience for PR reviews.
2026-01-12 03:04:17 +00:00

240 lines
7.4 KiB
TypeScript

#!/usr/bin/env bun
import * as core from "@actions/core";
import {
isIssuesEvent,
isIssuesAssignedEvent,
isIssueCommentEvent,
isPullRequestEvent,
isPullRequestReviewEvent,
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 {
inputs: { assigneeTrigger, labelTrigger, triggerPhrase, prompt },
} = context;
// If prompt is provided, always trigger
if (prompt) {
console.log(`Prompt provided, triggering action`);
return true;
}
// Check for assignee trigger
if (isIssuesAssignedEvent(context)) {
// Remove @ symbol from assignee_trigger if present
let triggerUser = assigneeTrigger.replace(/^@/, "");
const assigneeUsername = context.payload.assignee?.login || "";
if (triggerUser && assigneeUsername === triggerUser) {
console.log(`Issue assigned to trigger user '${triggerUser}'`);
return true;
}
}
// Check for label trigger
if (isIssuesEvent(context) && context.eventAction === "labeled") {
const labelName = (context.payload as any).label?.name || "";
if (labelTrigger && labelName === labelTrigger) {
console.log(`Issue labeled with trigger label '${labelTrigger}'`);
return true;
}
}
// Check for issue body and title trigger on issue creation
if (isIssuesEvent(context) && context.eventAction === "opened") {
const issueBody = context.payload.issue.body || "";
const issueTitle = context.payload.issue.title || "";
// Check for exact match with word boundaries or punctuation
const regex = new RegExp(
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
);
// Check in body
if (regex.test(issueBody)) {
console.log(
`Issue body contains exact trigger phrase '${triggerPhrase}'`,
);
return true;
}
// Check in title
if (regex.test(issueTitle)) {
console.log(
`Issue title contains exact trigger phrase '${triggerPhrase}'`,
);
return true;
}
}
// Check for pull request body and title trigger
if (isPullRequestEvent(context)) {
const prBody = context.payload.pull_request.body || "";
const prTitle = context.payload.pull_request.title || "";
// Check for exact match with word boundaries or punctuation
const regex = new RegExp(
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
);
// Check in body
if (regex.test(prBody)) {
console.log(
`Pull request body contains exact trigger phrase '${triggerPhrase}'`,
);
return true;
}
// Check in title
if (regex.test(prTitle)) {
console.log(
`Pull request title contains exact trigger phrase '${triggerPhrase}'`,
);
return true;
}
}
// Check for pull request review body trigger
if (
isPullRequestReviewEvent(context) &&
(context.eventAction === "submitted" || context.eventAction === "edited")
) {
const reviewBody = context.payload.review.body || "";
// Check for exact match with word boundaries or punctuation
const regex = new RegExp(
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
);
if (regex.test(reviewBody)) {
console.log(
`Pull request review contains exact trigger phrase '${triggerPhrase}'`,
);
return true;
}
}
// Check for comment trigger
if (
isIssueCommentEvent(context) ||
isPullRequestReviewCommentEvent(context)
) {
const commentBody = isIssueCommentEvent(context)
? context.payload.comment.body
: context.payload.comment.body;
// Check for exact match with word boundaries or punctuation
const regex = new RegExp(
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
);
if (regex.test(commentBody)) {
console.log(`Comment contains exact trigger phrase '${triggerPhrase}'`);
return true;
}
}
console.log(`No trigger was met for ${triggerPhrase}`);
return false;
}
export function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export async function checkTriggerAction(context: ParsedGitHubContext) {
const containsTrigger = checkContainsTrigger(context);
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";