Compare commits
4 Commits
main
...
tosinaf/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc19ae6bba | ||
|
|
3128a645ab | ||
|
|
600162b09b | ||
|
|
263b3b725a |
@ -199,6 +199,7 @@ runs:
|
||||
INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
|
||||
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GITHUB_JOB: ${{ github.job }}
|
||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||
|
||||
@ -90,6 +90,7 @@ type BaseContext = {
|
||||
branchPrefix: string;
|
||||
branchNameTemplate?: string;
|
||||
useStickyComment: boolean;
|
||||
jobId: string;
|
||||
useCommitSigning: boolean;
|
||||
sshSigningKey: string;
|
||||
botId: string;
|
||||
@ -150,6 +151,7 @@ export function parseGitHubContext(): GitHubContext {
|
||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||
jobId: process.env.GITHUB_JOB || "",
|
||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
||||
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
import { extractBotHeader } from "./comments/common";
|
||||
|
||||
export type ExecutionDetails = {
|
||||
total_cost_usd?: number;
|
||||
@ -79,11 +80,19 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
errorDetails,
|
||||
} = input;
|
||||
|
||||
// Extract and preserve bot header for sticky comment identification
|
||||
const botHeader = extractBotHeader(originalBody);
|
||||
|
||||
// Extract content from the original comment body
|
||||
// First, remove the "Claude Code is working…" or "Claude Code is working..." message
|
||||
const workingPattern = /Claude Code is working[…\.]{1,3}(?:\s*<img[^>]*>)?/i;
|
||||
let bodyContent = originalBody.replace(workingPattern, "").trim();
|
||||
|
||||
// Remove bot header from body content since we'll prepend it at the end
|
||||
if (botHeader) {
|
||||
bodyContent = bodyContent.replace(/^<!--\s*bot:\s*\S+\s*-->\n?/, "").trim();
|
||||
}
|
||||
|
||||
// Check if there's a PR link in the content
|
||||
let prLinkFromContent = "";
|
||||
|
||||
@ -199,5 +208,10 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
// Add the cleaned body content
|
||||
newBody += bodyContent;
|
||||
|
||||
// Prepend bot header if it existed in the original comment
|
||||
if (botHeader) {
|
||||
return (botHeader + newBody).trim();
|
||||
}
|
||||
|
||||
return newBody.trim();
|
||||
}
|
||||
|
||||
@ -24,10 +24,17 @@ export function createBranchLink(
|
||||
export function createCommentBody(
|
||||
jobRunLink: string,
|
||||
branchLink: string = "",
|
||||
botName: string = "",
|
||||
): string {
|
||||
return `Claude Code is working… ${SPINNER_HTML}
|
||||
const header = botName ? `<!-- bot: ${botName} -->\n` : "";
|
||||
return `${header}Claude Code is working… ${SPINNER_HTML}
|
||||
|
||||
I'll analyze this and get back to you.
|
||||
|
||||
${jobRunLink}${branchLink}`;
|
||||
}
|
||||
|
||||
export function extractBotHeader(body: string): string {
|
||||
const match = body.match(/^(<!--\s*bot:\s*\S+\s*-->\n?)/);
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
@ -14,16 +14,21 @@ import {
|
||||
} from "../../context";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
|
||||
const CLAUDE_APP_BOT_ID = 209825114;
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export async function createInitialComment(
|
||||
octokit: Octokit,
|
||||
context: ParsedGitHubContext,
|
||||
) {
|
||||
const { owner, repo } = context.repository;
|
||||
const botIdentifier = context.inputs.useStickyComment
|
||||
? context.inputs.jobId
|
||||
: "";
|
||||
|
||||
const jobRunLink = createJobRunLink(owner, repo, context.runId);
|
||||
const initialBody = createCommentBody(jobRunLink);
|
||||
const initialBody = createCommentBody(jobRunLink, "", botIdentifier);
|
||||
|
||||
try {
|
||||
let response;
|
||||
@ -33,20 +38,27 @@ export async function createInitialComment(
|
||||
context.isPR &&
|
||||
isPullRequestEvent(context)
|
||||
) {
|
||||
const comments = await octokit.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.entityNumber,
|
||||
});
|
||||
const existingComment = comments.data.find((comment) => {
|
||||
const idMatch = comment.user?.id === CLAUDE_APP_BOT_ID;
|
||||
const botNameMatch =
|
||||
comment.user?.type === "Bot" &&
|
||||
comment.user?.login.toLowerCase().includes("claude");
|
||||
const bodyMatch = comment.body === initialBody;
|
||||
// Use pagination to fetch all comments (handles PRs with 30+ comments)
|
||||
const comments = await octokit.paginate(
|
||||
octokit.rest.issues.listComments,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.entityNumber,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
|
||||
return idMatch || botNameMatch || bodyMatch;
|
||||
const existingComment = comments.find((comment) => {
|
||||
if (!botIdentifier) return false;
|
||||
|
||||
const headerPattern = new RegExp(
|
||||
`<!--\\s*bot:\\s*${escapeRegex(botIdentifier)}\\s*-->`,
|
||||
"i",
|
||||
);
|
||||
return headerPattern.test(comment.body || "");
|
||||
});
|
||||
|
||||
if (existingComment) {
|
||||
response = await octokit.rest.issues.updateComment({
|
||||
owner,
|
||||
|
||||
@ -33,7 +33,10 @@ export async function updateTrackingComment(
|
||||
branchLink = createBranchLink(owner, repo, branch);
|
||||
}
|
||||
|
||||
const updatedBody = createCommentBody(jobRunLink, branchLink);
|
||||
const botIdentifier = context.inputs.useStickyComment
|
||||
? context.inputs.jobId
|
||||
: "";
|
||||
const updatedBody = createCommentBody(jobRunLink, branchLink, botIdentifier);
|
||||
|
||||
// Update the existing comment with the branch link
|
||||
try {
|
||||
|
||||
@ -5,6 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { z } from "zod";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { extractBotHeader } from "../github/operations/comments/common";
|
||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||
|
||||
@ -55,13 +56,31 @@ server.tool(
|
||||
const isPullRequestReviewComment =
|
||||
eventName === "pull_request_review_comment";
|
||||
|
||||
// Fetch current comment to preserve the bot header for sticky comment functionality
|
||||
const currentComment = isPullRequestReviewComment
|
||||
? await octokit.rest.pulls.getReviewComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
})
|
||||
: await octokit.rest.issues.getComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
});
|
||||
|
||||
// Extract bot header if present (e.g., "<!-- bot: claude-code-review -->")
|
||||
const botHeader = extractBotHeader(currentComment.data.body || "");
|
||||
|
||||
// Sanitize then prepend header to preserve sticky comment identification
|
||||
const sanitizedBody = sanitizeContent(body);
|
||||
const bodyWithHeader = botHeader + sanitizedBody;
|
||||
|
||||
const result = await updateClaudeComment(octokit, {
|
||||
owner,
|
||||
repo,
|
||||
commentId,
|
||||
body: sanitizedBody,
|
||||
body: bodyWithHeader,
|
||||
isPullRequestReviewComment,
|
||||
});
|
||||
|
||||
|
||||
@ -443,4 +443,79 @@ describe("updateCommentBody", () => {
|
||||
expect(result).not.toContain("tree/claude/issue-123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bot header preservation", () => {
|
||||
it("should preserve bot header through comment update", () => {
|
||||
const input: CommentUpdateInput = {
|
||||
currentBody:
|
||||
"<!-- bot: claude-review -->\nClaude Code is working…\n\nI'll analyze this and get back to you.\n\n[View job run](https://github.com/owner/repo/actions/runs/123)",
|
||||
actionFailed: false,
|
||||
executionDetails: { duration_ms: 30000 },
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
triggerUsername: "test-user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toStartWith("<!-- bot: claude-review -->\n");
|
||||
expect(result).toContain("Claude finished @test-user's task");
|
||||
});
|
||||
|
||||
it("should preserve bot header with different bot names", () => {
|
||||
const input: CommentUpdateInput = {
|
||||
currentBody:
|
||||
"<!-- bot: claude-docs-review -->\nClaude Code is working...",
|
||||
actionFailed: false,
|
||||
executionDetails: { duration_ms: 30000 },
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
triggerUsername: "user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toStartWith("<!-- bot: claude-docs-review -->");
|
||||
});
|
||||
|
||||
it("should work without bot header", () => {
|
||||
const input: CommentUpdateInput = {
|
||||
currentBody:
|
||||
"Claude Code is working…\n\nI'll analyze this and get back to you.",
|
||||
actionFailed: false,
|
||||
executionDetails: { duration_ms: 30000 },
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
triggerUsername: "test-user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).not.toContain("<!-- bot:");
|
||||
expect(result).toContain("Claude finished @test-user's task");
|
||||
});
|
||||
|
||||
it("should preserve bot header when action fails", () => {
|
||||
const input: CommentUpdateInput = {
|
||||
currentBody: "<!-- bot: claude-review -->\nClaude Code is working…",
|
||||
actionFailed: true,
|
||||
executionDetails: { duration_ms: 10000 },
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
errorDetails: "Something went wrong",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toStartWith("<!-- bot: claude-review -->");
|
||||
expect(result).toContain("**Claude encountered an error after 10s**");
|
||||
});
|
||||
|
||||
it("should not preserve bot header if not at start of comment", () => {
|
||||
const input: CommentUpdateInput = {
|
||||
currentBody:
|
||||
"Some text before\n<!-- bot: claude-review -->\nClaude Code is working…",
|
||||
actionFailed: false,
|
||||
executionDetails: { duration_ms: 5000 },
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
triggerUsername: "testuser",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result.startsWith("<!-- bot:")).toBe(false);
|
||||
expect(result).toContain("**Claude finished");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,6 +32,7 @@ describe("prepareMcpConfig", () => {
|
||||
labelTrigger: "",
|
||||
branchPrefix: "",
|
||||
useStickyComment: false,
|
||||
jobId: "",
|
||||
useCommitSigning: false,
|
||||
sshSigningKey: "",
|
||||
botId: String(CLAUDE_APP_BOT_ID),
|
||||
|
||||
@ -19,6 +19,7 @@ const defaultInputs = {
|
||||
labelTrigger: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
jobId: "",
|
||||
useCommitSigning: false,
|
||||
sshSigningKey: "",
|
||||
botId: String(CLAUDE_APP_BOT_ID),
|
||||
|
||||
@ -19,6 +19,7 @@ describe("detectMode with enhanced routing", () => {
|
||||
labelTrigger: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
jobId: "",
|
||||
useCommitSigning: false,
|
||||
sshSigningKey: "",
|
||||
botId: "123456",
|
||||
|
||||
@ -67,6 +67,7 @@ describe("checkWritePermissions", () => {
|
||||
labelTrigger: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
jobId: "",
|
||||
useCommitSigning: false,
|
||||
sshSigningKey: "",
|
||||
botId: String(CLAUDE_APP_BOT_ID),
|
||||
|
||||
@ -360,4 +360,8 @@ describe("stripHtmlComments (legacy)", () => {
|
||||
"Hello World",
|
||||
);
|
||||
});
|
||||
|
||||
it("should strip all HTML comments including bot headers", () => {
|
||||
expect(stripHtmlComments("<!-- bot: claude-review -->Text")).toBe("Text");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user