* feat(inline-comment): add confirmed param + probe-pattern safety net
Subagents that inherit this tool sometimes probe it with test comments
('Test comment to see if I can create inline comments') after hitting
unrelated errors elsewhere. Recurring issue across customer PRs.
Adds two defenses:
1. confirmed param: set true to post (final review comments should pass
this). When false, buffers to a JSONL file instead of posting.
2. Probe-pattern safety net: when confirmed is omitted (backward compat
for existing prompts), the body is checked against obvious probe
patterns ('test comment', 'can i', 'does this work', etc.). Matching
calls are buffered instead of posted.
A post-run step in action.yml reports the buffered call count and bodies
as a workflow warning for diagnostics.
Backward compatibility:
- Existing single-agent prompts (no confirmed param) post normally unless
the body happens to start with a probe phrase (unlikely for real
review comments)
- The code-review skill is being updated to pass confirmed: true in its
final posting step
- Subagent probes that would previously post now harmlessly buffer
* refactor: replace probe-regex with Haiku classification in post-step
The regex approach was narrow and could miss creative probe phrasings.
Replaced with a batch Haiku classification that runs after the session
completes.
Flow:
- MCP server: confirmed !== true -> buffer to JSONL (no classification
in-band, no latency in the tool path)
- Post-step (src/entrypoints/post-buffered-inline-comments.ts): reads
buffer, sends all bodies to a single Haiku call, posts only those
classified as real review comments
- confirmed=false entries are never posted regardless of classification
Fail-open: if ANTHROPIC_API_KEY is unavailable (Bedrock/Vertex users)
or the classification call fails, posts all unconfirmed comments. This
matches pre-PR behavior where all calls posted immediately.
The post-step emits :⚠️: for each filtered comment so users can
see what was dropped and why.
* feat: add classify_inline_comments opt-out input
New action input classify_inline_comments (default 'true'). Setting to
'false' restores pre-buffering behavior: all inline comment calls post
immediately regardless of the confirmed param.
Threads through: action input -> CLASSIFY_INLINE_COMMENTS env ->
context.inputs.classifyInlineComments -> MCP server env ->
CLASSIFY_ENABLED module const.
Post-step is also gated on the input so it skips entirely when
classification is disabled.
* docs: document classify_inline_comments input and confirmed param
- usage.md: add classify_inline_comments to inputs table
- solutions.md: mention confirmed=true in the prompt example and explain
buffering/classification in the tool permissions section
304 lines
8.3 KiB
TypeScript
304 lines
8.3 KiB
TypeScript
import * as github from "@actions/github";
|
|
import type {
|
|
IssuesEvent,
|
|
IssuesAssignedEvent,
|
|
IssueCommentEvent,
|
|
PullRequestEvent,
|
|
PullRequestReviewEvent,
|
|
PullRequestReviewCommentEvent,
|
|
WorkflowRunEvent,
|
|
} from "@octokit/webhooks-types";
|
|
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "./constants";
|
|
// Custom types for GitHub Actions events that aren't webhooks
|
|
export type WorkflowDispatchEvent = {
|
|
action?: never;
|
|
inputs?: Record<string, any>;
|
|
ref?: string;
|
|
repository: {
|
|
name: string;
|
|
owner: {
|
|
login: string;
|
|
};
|
|
};
|
|
sender: {
|
|
login: string;
|
|
};
|
|
workflow: string;
|
|
};
|
|
|
|
export type RepositoryDispatchEvent = {
|
|
action: string;
|
|
client_payload?: Record<string, any>;
|
|
repository: {
|
|
name: string;
|
|
owner: {
|
|
login: string;
|
|
};
|
|
};
|
|
sender: {
|
|
login: string;
|
|
};
|
|
};
|
|
|
|
export type ScheduleEvent = {
|
|
action?: never;
|
|
schedule?: string;
|
|
repository: {
|
|
name: string;
|
|
owner: {
|
|
login: string;
|
|
};
|
|
};
|
|
};
|
|
|
|
// Event name constants for better maintainability
|
|
const ENTITY_EVENT_NAMES = [
|
|
"issues",
|
|
"issue_comment",
|
|
"pull_request",
|
|
"pull_request_review",
|
|
"pull_request_review_comment",
|
|
] as const;
|
|
|
|
const AUTOMATION_EVENT_NAMES = [
|
|
"workflow_dispatch",
|
|
"repository_dispatch",
|
|
"schedule",
|
|
"workflow_run",
|
|
] as const;
|
|
|
|
// Derive types from constants for better maintainability
|
|
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
|
|
type AutomationEventName = (typeof AUTOMATION_EVENT_NAMES)[number];
|
|
|
|
// Common fields shared by all context types
|
|
type BaseContext = {
|
|
runId: string;
|
|
eventAction?: string;
|
|
repository: {
|
|
owner: string;
|
|
repo: string;
|
|
full_name: string;
|
|
};
|
|
actor: string;
|
|
inputs: {
|
|
prompt: string;
|
|
triggerPhrase: string;
|
|
assigneeTrigger: string;
|
|
labelTrigger: string;
|
|
baseBranch?: string;
|
|
branchPrefix: string;
|
|
branchNameTemplate?: string;
|
|
useStickyComment: boolean;
|
|
classifyInlineComments: boolean;
|
|
useCommitSigning: boolean;
|
|
sshSigningKey: string;
|
|
botId: string;
|
|
botName: string;
|
|
allowedBots: string;
|
|
allowedNonWriteUsers: string;
|
|
trackProgress: boolean;
|
|
includeFixLinks: boolean;
|
|
includeCommentsByActor: string;
|
|
excludeCommentsByActor: string;
|
|
};
|
|
};
|
|
|
|
// Context for entity-based events (issues, PRs, comments)
|
|
export type ParsedGitHubContext = BaseContext & {
|
|
eventName: EntityEventName;
|
|
payload:
|
|
| IssuesEvent
|
|
| IssueCommentEvent
|
|
| PullRequestEvent
|
|
| PullRequestReviewEvent
|
|
| PullRequestReviewCommentEvent;
|
|
entityNumber: number;
|
|
isPR: boolean;
|
|
};
|
|
|
|
// Context for automation events (workflow_dispatch, repository_dispatch, schedule, workflow_run)
|
|
export type AutomationContext = BaseContext & {
|
|
eventName: AutomationEventName;
|
|
payload:
|
|
| WorkflowDispatchEvent
|
|
| RepositoryDispatchEvent
|
|
| ScheduleEvent
|
|
| WorkflowRunEvent;
|
|
};
|
|
|
|
// Union type for all contexts
|
|
export type GitHubContext = ParsedGitHubContext | AutomationContext;
|
|
|
|
export function parseGitHubContext(): GitHubContext {
|
|
const context = github.context;
|
|
|
|
const commonFields = {
|
|
runId: process.env.GITHUB_RUN_ID!,
|
|
eventAction: context.payload.action,
|
|
repository: {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
full_name: `${context.repo.owner}/${context.repo.repo}`,
|
|
},
|
|
actor: context.actor,
|
|
inputs: {
|
|
prompt: process.env.PROMPT || "",
|
|
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
|
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
|
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
|
baseBranch: process.env.BASE_BRANCH,
|
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
|
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
|
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
|
classifyInlineComments: process.env.CLASSIFY_INLINE_COMMENTS !== "false",
|
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
|
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
|
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
|
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
|
|
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
|
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
|
|
trackProgress: process.env.TRACK_PROGRESS === "true",
|
|
includeFixLinks: process.env.INCLUDE_FIX_LINKS === "true",
|
|
includeCommentsByActor: process.env.INCLUDE_COMMENTS_BY_ACTOR ?? "",
|
|
excludeCommentsByActor: process.env.EXCLUDE_COMMENTS_BY_ACTOR ?? "",
|
|
},
|
|
};
|
|
|
|
switch (context.eventName) {
|
|
case "issues": {
|
|
const payload = context.payload as IssuesEvent;
|
|
return {
|
|
...commonFields,
|
|
eventName: "issues",
|
|
payload,
|
|
entityNumber: payload.issue.number,
|
|
isPR: false,
|
|
};
|
|
}
|
|
case "issue_comment": {
|
|
const payload = context.payload as IssueCommentEvent;
|
|
return {
|
|
...commonFields,
|
|
eventName: "issue_comment",
|
|
payload,
|
|
entityNumber: payload.issue.number,
|
|
isPR: Boolean(payload.issue.pull_request),
|
|
};
|
|
}
|
|
case "pull_request":
|
|
case "pull_request_target": {
|
|
const payload = context.payload as PullRequestEvent;
|
|
return {
|
|
...commonFields,
|
|
eventName: "pull_request",
|
|
payload,
|
|
entityNumber: payload.pull_request.number,
|
|
isPR: true,
|
|
};
|
|
}
|
|
case "pull_request_review": {
|
|
const payload = context.payload as PullRequestReviewEvent;
|
|
return {
|
|
...commonFields,
|
|
eventName: "pull_request_review",
|
|
payload,
|
|
entityNumber: payload.pull_request.number,
|
|
isPR: true,
|
|
};
|
|
}
|
|
case "pull_request_review_comment": {
|
|
const payload = context.payload as PullRequestReviewCommentEvent;
|
|
return {
|
|
...commonFields,
|
|
eventName: "pull_request_review_comment",
|
|
payload,
|
|
entityNumber: payload.pull_request.number,
|
|
isPR: true,
|
|
};
|
|
}
|
|
case "workflow_dispatch": {
|
|
return {
|
|
...commonFields,
|
|
eventName: "workflow_dispatch",
|
|
payload: context.payload as unknown as WorkflowDispatchEvent,
|
|
};
|
|
}
|
|
case "repository_dispatch": {
|
|
return {
|
|
...commonFields,
|
|
eventName: "repository_dispatch",
|
|
payload: context.payload as unknown as RepositoryDispatchEvent,
|
|
};
|
|
}
|
|
case "schedule": {
|
|
return {
|
|
...commonFields,
|
|
eventName: "schedule",
|
|
payload: context.payload as unknown as ScheduleEvent,
|
|
};
|
|
}
|
|
case "workflow_run": {
|
|
return {
|
|
...commonFields,
|
|
eventName: "workflow_run",
|
|
payload: context.payload as unknown as WorkflowRunEvent,
|
|
};
|
|
}
|
|
default:
|
|
throw new Error(`Unsupported event type: ${context.eventName}`);
|
|
}
|
|
}
|
|
|
|
export function isIssuesEvent(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
|
return context.eventName === "issues";
|
|
}
|
|
|
|
export function isIssueCommentEvent(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
|
return context.eventName === "issue_comment";
|
|
}
|
|
|
|
export function isPullRequestEvent(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
|
return context.eventName === "pull_request";
|
|
}
|
|
|
|
export function isPullRequestReviewEvent(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
|
return context.eventName === "pull_request_review";
|
|
}
|
|
|
|
export function isPullRequestReviewCommentEvent(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
|
return context.eventName === "pull_request_review_comment";
|
|
}
|
|
|
|
export function isIssuesAssignedEvent(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
|
return isIssuesEvent(context) && context.eventAction === "assigned";
|
|
}
|
|
|
|
// Type guard to check if context is an entity context (has entityNumber and isPR)
|
|
export function isEntityContext(
|
|
context: GitHubContext,
|
|
): context is ParsedGitHubContext {
|
|
return ENTITY_EVENT_NAMES.includes(context.eventName as EntityEventName);
|
|
}
|
|
|
|
// Type guard to check if context is an automation context
|
|
export function isAutomationContext(
|
|
context: GitHubContext,
|
|
): context is AutomationContext {
|
|
return AUTOMATION_EVENT_NAMES.includes(
|
|
context.eventName as AutomationEventName,
|
|
);
|
|
}
|