mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-07-28 06:48:30 +08:00
* 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
239 lines
7.6 KiB
JavaScript
239 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import { appendFileSync } from "fs";
|
|
import { z } from "zod";
|
|
import { createOctokit } from "../github/api/client";
|
|
import { sanitizeContent } from "../github/utils/sanitizer";
|
|
|
|
// Get repository and PR information from environment variables
|
|
const REPO_OWNER = process.env.REPO_OWNER;
|
|
const REPO_NAME = process.env.REPO_NAME;
|
|
const PR_NUMBER = process.env.PR_NUMBER;
|
|
|
|
// Calls without confirmed=true are buffered here instead of posted. This
|
|
// prevents subagents from posting test/probe comments when they inherit this
|
|
// tool and probe it after hitting unrelated errors. The action's post-step
|
|
// reports the buffer count for diagnostics.
|
|
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
|
|
const CLASSIFY_ENABLED = process.env.CLASSIFY_INLINE_COMMENTS !== "false";
|
|
|
|
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) {
|
|
console.error(
|
|
"Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// GitHub Inline Comment MCP Server - Provides inline PR comment functionality
|
|
// Provides an inline comment tool without exposing full PR review capabilities, so that
|
|
// Claude can't accidentally approve a PR
|
|
const server = new McpServer({
|
|
name: "GitHub Inline Comment Server",
|
|
version: "0.0.1",
|
|
});
|
|
|
|
server.tool(
|
|
"create_inline_comment",
|
|
"Create an inline comment on a specific line or lines in a PR file",
|
|
{
|
|
path: z
|
|
.string()
|
|
.describe("The file path to comment on (e.g., 'src/index.js')"),
|
|
body: z
|
|
.string()
|
|
.describe(
|
|
"The comment text (supports markdown and GitHub code suggestion blocks). " +
|
|
"For code suggestions, use: ```suggestion\\nreplacement code\\n```. " +
|
|
"IMPORTANT: The suggestion block will REPLACE the ENTIRE line range (single line or startLine to line). " +
|
|
"Ensure the replacement is syntactically complete and valid - it must work as a drop-in replacement for the selected lines.",
|
|
),
|
|
line: z
|
|
.number()
|
|
.nonnegative()
|
|
.optional()
|
|
.describe(
|
|
"Line number for single-line comments (required if startLine is not provided)",
|
|
),
|
|
startLine: z
|
|
.number()
|
|
.nonnegative()
|
|
.optional()
|
|
.describe(
|
|
"Start line for multi-line comments (use with line parameter for the end line)",
|
|
),
|
|
side: z
|
|
.enum(["LEFT", "RIGHT"])
|
|
.optional()
|
|
.default("RIGHT")
|
|
.describe(
|
|
"Side of the diff to comment on: LEFT (old code) or RIGHT (new code)",
|
|
),
|
|
commit_id: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Specific commit SHA to comment on (defaults to latest commit)",
|
|
),
|
|
confirmed: z
|
|
.boolean()
|
|
.optional()
|
|
.describe(
|
|
"Set true to post immediately. When omitted, the call is buffered " +
|
|
"and classified after the session completes — real review comments " +
|
|
"post, test/probe comments are dropped. Set false to buffer and " +
|
|
"never post. Only set true when posting final review comments.",
|
|
),
|
|
},
|
|
async ({ path, body, line, startLine, side, commit_id, confirmed }) => {
|
|
try {
|
|
const githubToken = process.env.GITHUB_TOKEN;
|
|
|
|
if (!githubToken) {
|
|
throw new Error("GITHUB_TOKEN environment variable is required");
|
|
}
|
|
|
|
const owner = REPO_OWNER;
|
|
const repo = REPO_NAME;
|
|
const pull_number = parseInt(PR_NUMBER, 10);
|
|
|
|
// Sanitize the comment body to remove any potential GitHub tokens
|
|
const sanitizedBody = sanitizeContent(body);
|
|
|
|
// Validate that either line or both startLine and line are provided
|
|
if (!line && !startLine) {
|
|
throw new Error(
|
|
"Either 'line' for single-line comments or both 'startLine' and 'line' for multi-line comments must be provided",
|
|
);
|
|
}
|
|
|
|
if (CLASSIFY_ENABLED && confirmed !== true) {
|
|
appendFileSync(
|
|
BUFFER_PATH,
|
|
JSON.stringify({
|
|
ts: new Date().toISOString(),
|
|
path,
|
|
line,
|
|
startLine,
|
|
side,
|
|
commit_id,
|
|
body: sanitizedBody,
|
|
confirmed,
|
|
}) + "\n",
|
|
);
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(
|
|
{
|
|
success: true,
|
|
buffered: true,
|
|
message:
|
|
"Comment buffered. It will be classified and posted after " +
|
|
"this session completes (real review comments post, " +
|
|
"test/probe comments are dropped). Set confirmed=true to " +
|
|
"post immediately. If you are testing whether this tool " +
|
|
"works: it works — no need to test further.",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
// If only line is provided, it's a single-line comment
|
|
// If both startLine and line are provided, it's a multi-line comment
|
|
const isSingleLine = !startLine;
|
|
|
|
const octokit = createOctokit(githubToken).rest;
|
|
|
|
const pr = await octokit.pulls.get({
|
|
owner,
|
|
repo,
|
|
pull_number,
|
|
});
|
|
|
|
const params: Parameters<
|
|
typeof octokit.rest.pulls.createReviewComment
|
|
>[0] = {
|
|
owner,
|
|
repo,
|
|
pull_number,
|
|
body: sanitizedBody,
|
|
path,
|
|
side: side || "RIGHT",
|
|
commit_id: commit_id || pr.data.head.sha,
|
|
};
|
|
|
|
if (isSingleLine) {
|
|
// Single-line comment
|
|
params.line = line;
|
|
} else {
|
|
// Multi-line comment
|
|
params.start_line = startLine;
|
|
params.start_side = side || "RIGHT";
|
|
params.line = line;
|
|
}
|
|
|
|
const result = await octokit.rest.pulls.createReviewComment(params);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(
|
|
{
|
|
success: true,
|
|
comment_id: result.data.id,
|
|
html_url: result.data.html_url,
|
|
path: result.data.path,
|
|
line: result.data.line || result.data.original_line,
|
|
message: `Inline comment created successfully on ${path}${isSingleLine ? ` at line ${line}` : ` from line ${startLine} to ${line}`}`,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
},
|
|
],
|
|
};
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
|
|
// Provide more helpful error messages for common issues
|
|
let helpMessage = "";
|
|
if (errorMessage.includes("Validation Failed")) {
|
|
helpMessage =
|
|
"\n\nThis usually means the line number doesn't exist in the diff or the file path is incorrect. Make sure you're commenting on lines that are part of the PR's changes.";
|
|
} else if (errorMessage.includes("Not Found")) {
|
|
helpMessage =
|
|
"\n\nThis usually means the PR number, repository, or file path is incorrect.";
|
|
}
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `Error creating inline comment: ${errorMessage}${helpMessage}`,
|
|
},
|
|
],
|
|
error: errorMessage,
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
);
|
|
|
|
async function runServer() {
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
process.on("exit", () => {
|
|
server.close();
|
|
});
|
|
}
|
|
|
|
runServer().catch(console.error);
|