Compare commits

..

6 Commits

Author SHA1 Message Date
Ashwin Bhat
a017b830c0 chore: comment out release-base-action job in release workflow (#833)
Temporarily disable the release-base-action job that syncs releases
to the claude-code-base-action repository. The job checkout step
and subsequent tag/release creation steps are now commented out.


Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 2
Claude-Permission-Prompts: 2
Claude-Escapes: 0

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-16 14:16:27 -08:00
GitHub Actions
75f52e56b2 chore: bump Claude Code to 2.1.9 and Agent SDK to 0.2.9 2026-01-16 02:18:38 +00:00
Ashwin Bhat
1bbc9e7ff7 fix: add checkHumanActor to agent mode (#826)
Fixes issue #641 where users were getting banned due to rapid successive
Claude runs triggered by the synchronize event.

Changes:
- Add checkHumanActor call to agent mode's prepare() method to reject
  bot-triggered workflows unless explicitly allowed via allowed_bots
- Update checkHumanActor to accept GitHubContext (union type) instead
  of just ParsedGitHubContext
- Add tests for bot rejection/allowance in agent mode

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 3
Claude-Escapes: 0
2026-01-15 10:28:46 -08:00
Ashwin Bhat
625ea1519c docs: clarify that Claude does not auto-create PRs by default (#824)
Add a new section to security.md explaining that in the default
configuration, Claude commits to a branch and provides a link for
the user to create the PR themselves, ensuring human oversight.


Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 2
Claude-Permission-Prompts: 2
Claude-Escapes: 0

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 15:22:40 -08: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
14 changed files with 137 additions and 673 deletions

View File

@@ -109,48 +109,48 @@ jobs:
echo "Updated $major_version tag to point to $next_version"
release-base-action:
needs: create-release
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout base-action repo
uses: actions/checkout@v5
with:
repository: anthropics/claude-code-base-action
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
fetch-depth: 0
# - name: Create and push tag
# run: |
# 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"
# # 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
# - name: Create GitHub release
# env:
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
# run: |
# next_version="${{ needs.create-release.outputs.next_version }}"
# # Create the release
# gh release create "$next_version" \
# --repo anthropics/claude-code-base-action \
# --title "$next_version" \
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
# --latest=false
# # Update beta release to be latest
# gh release edit beta \
# --repo anthropics/claude-code-base-action \
# --latest
# release-base-action:
# needs: create-release
# if: ${{ !inputs.dry_run }}
# runs-on: ubuntu-latest
# environment: production
# steps:
# - name: Checkout base-action repo
# uses: actions/checkout@v5
# with:
# repository: anthropics/claude-code-base-action
# token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
# fetch-depth: 0
#
# - name: Create and push tag
# run: |
# 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"
#
# # 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
#
# - name: Create GitHub release
# env:
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
# run: |
# next_version="${{ needs.create-release.outputs.next_version }}"
#
# # Create the release
# gh release create "$next_version" \
# --repo anthropics/claude-code-base-action \
# --title "$next_version" \
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
# --latest=false
#
# # Update beta release to be latest
# gh release edit beta \
# --repo anthropics/claude-code-base-action \
# --latest

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.9"
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."

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.9"
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.9",
"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.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=="],
"@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.9",
"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.9",
"@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.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=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],

View File

@@ -13,6 +13,16 @@
- **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
## 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
**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.

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.9",
"@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1",

View File

@@ -6,11 +6,11 @@
*/
import type { Octokit } from "@octokit/rest";
import type { ParsedGitHubContext } from "../context";
import type { GitHubContext } from "../context";
export async function checkHumanActor(
octokit: Octokit,
githubContext: ParsedGitHubContext,
githubContext: GitHubContext,
) {
// Fetch user information from GitHub API
const { data: userData } = await octokit.users.getByUsername({

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

@@ -8,6 +8,7 @@ import {
configureGitAuth,
setupSshSigning,
} from "../../github/operations/git-config";
import { checkHumanActor } from "../../github/validation/actor";
import type { GitHubContext } from "../../github/context";
import { isEntityContext } from "../../github/context";
@@ -80,7 +81,14 @@ export const agentMode: Mode = {
return false;
},
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
async prepare({
context,
octokit,
githubToken,
}: ModeOptions): Promise<ModeResult> {
// Check if actor is human (prevents bot-triggered loops)
await checkHumanActor(octokit.rest, context);
// Configure git authentication for agent mode (same as tag mode)
// SSH signing takes precedence if provided
const useSshSigning = !!context.inputs.sshSigningKey;

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

View File

@@ -145,12 +145,12 @@ describe("Agent Mode", () => {
users: {
getAuthenticated: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345 },
data: { login: "test-user", id: 12345, type: "User" },
}),
),
getByUsername: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345 },
data: { login: "test-user", id: 12345, type: "User" },
}),
),
},
@@ -187,6 +187,65 @@ describe("Agent Mode", () => {
process.env.GITHUB_REF_NAME = originalRefName;
});
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 () => {
const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch",
@@ -199,12 +258,12 @@ describe("Agent Mode", () => {
users: {
getAuthenticated: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345 },
data: { login: "test-user", id: 12345, type: "User" },
}),
),
getByUsername: mock(() =>
Promise.resolve({
data: { login: "test-user", id: 12345 },
data: { login: "test-user", id: 12345, type: "User" },
}),
),
},