mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-03 17:58:30 +08:00
The formatted_context block sent to the agent omitted labels for both issues and pull requests, even though the GraphQL queries already fetched them. This caused agents to incorrectly report "no labels" when labels existed, breaking any workflow that routes on label state (e.g., drift-fix routing on drift:* labels in a Drift Watcher pattern). Changes: - Add 'PR Labels:' line to PR context output - Add 'Issue Labels:' line to issue context output - Both emit 'none' when no labels are present (explicit > omitted) - Bump labels(first: 1) → labels(first: 100) in both queries; the previous cap meant only one label would appear even after the formatter fix - Update existing tests + add 'with labels' tests for both PR and issue branches Discovered while building a GitHub Actions workflow that uses this action for drift-watcher routing in an internal seed framework (joshpayne-joby/slim-routines#6). The agent self-diagnosed the gap by inspecting this action's source — a satisfying full-loop. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
154 lines
4.0 KiB
TypeScript
154 lines
4.0 KiB
TypeScript
import type {
|
|
GitHubPullRequest,
|
|
GitHubIssue,
|
|
GitHubComment,
|
|
GitHubFile,
|
|
GitHubReview,
|
|
} from "../types";
|
|
import type { GitHubFileWithSHA } from "./fetcher";
|
|
import { sanitizeContent } from "../utils/sanitizer";
|
|
|
|
function formatLabels(labelNodes: Array<{ name: string }>): string {
|
|
if (labelNodes.length === 0) return "none";
|
|
return labelNodes.map((l) => l.name).join(", ");
|
|
}
|
|
|
|
export function formatContext(
|
|
contextData: GitHubPullRequest | GitHubIssue,
|
|
isPR: boolean,
|
|
): string {
|
|
if (isPR) {
|
|
const prData = contextData as GitHubPullRequest;
|
|
const sanitizedTitle = sanitizeContent(prData.title);
|
|
return `PR Title: ${sanitizedTitle}
|
|
PR Author: ${prData.author.login}
|
|
PR Branch: ${prData.headRefName} -> ${prData.baseRefName}
|
|
PR State: ${prData.state}
|
|
PR Labels: ${formatLabels(prData.labels.nodes)}
|
|
PR Additions: ${prData.additions}
|
|
PR Deletions: ${prData.deletions}
|
|
Total Commits: ${prData.commits.totalCount}
|
|
Changed Files: ${prData.files.nodes.length} files`;
|
|
} else {
|
|
const issueData = contextData as GitHubIssue;
|
|
const sanitizedTitle = sanitizeContent(issueData.title);
|
|
return `Issue Title: ${sanitizedTitle}
|
|
Issue Author: ${issueData.author.login}
|
|
Issue State: ${issueData.state}
|
|
Issue Labels: ${formatLabels(issueData.labels.nodes)}`;
|
|
}
|
|
}
|
|
|
|
export function formatBody(
|
|
body: string,
|
|
imageUrlMap: Map<string, string>,
|
|
): string {
|
|
let processedBody = body;
|
|
|
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
|
processedBody = processedBody.replaceAll(originalUrl, localPath);
|
|
}
|
|
|
|
processedBody = sanitizeContent(processedBody);
|
|
|
|
return processedBody;
|
|
}
|
|
|
|
export function formatComments(
|
|
comments: GitHubComment[],
|
|
imageUrlMap?: Map<string, string>,
|
|
): string {
|
|
return comments
|
|
.filter((comment) => !comment.isMinimized)
|
|
.map((comment) => {
|
|
let body = comment.body;
|
|
|
|
if (imageUrlMap && body) {
|
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
|
body = body.replaceAll(originalUrl, localPath);
|
|
}
|
|
}
|
|
|
|
body = sanitizeContent(body);
|
|
|
|
return `[${comment.author.login} at ${comment.createdAt}]: ${body}`;
|
|
})
|
|
.join("\n\n");
|
|
}
|
|
|
|
export function formatReviewComments(
|
|
reviewData: { nodes: GitHubReview[] } | null,
|
|
imageUrlMap?: Map<string, string>,
|
|
): string {
|
|
if (!reviewData || !reviewData.nodes) {
|
|
return "";
|
|
}
|
|
|
|
const formattedReviews = reviewData.nodes.map((review) => {
|
|
let reviewOutput = `[Review by ${review.author.login} at ${review.submittedAt}]: ${review.state}`;
|
|
|
|
if (review.body && review.body.trim()) {
|
|
let body = review.body;
|
|
|
|
if (imageUrlMap) {
|
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
|
body = body.replaceAll(originalUrl, localPath);
|
|
}
|
|
}
|
|
|
|
const sanitizedBody = sanitizeContent(body);
|
|
reviewOutput += `\n${sanitizedBody}`;
|
|
}
|
|
|
|
if (
|
|
review.comments &&
|
|
review.comments.nodes &&
|
|
review.comments.nodes.length > 0
|
|
) {
|
|
const comments = review.comments.nodes
|
|
.filter((comment) => !comment.isMinimized)
|
|
.map((comment) => {
|
|
let body = comment.body;
|
|
|
|
if (imageUrlMap) {
|
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
|
body = body.replaceAll(originalUrl, localPath);
|
|
}
|
|
}
|
|
|
|
body = sanitizeContent(body);
|
|
|
|
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
|
})
|
|
.join("\n");
|
|
if (comments) {
|
|
reviewOutput += `\n${comments}`;
|
|
}
|
|
}
|
|
|
|
return reviewOutput;
|
|
});
|
|
|
|
return formattedReviews.join("\n\n");
|
|
}
|
|
|
|
export function formatChangedFiles(changedFiles: GitHubFile[]): string {
|
|
return changedFiles
|
|
.map(
|
|
(file) =>
|
|
`- ${file.path} (${file.changeType}) +${file.additions}/-${file.deletions}`,
|
|
)
|
|
.join("\n");
|
|
}
|
|
|
|
export function formatChangedFilesWithSHA(
|
|
changedFiles: GitHubFileWithSHA[],
|
|
): string {
|
|
return changedFiles
|
|
.map(
|
|
(file) =>
|
|
`- ${file.path} (${file.changeType}) +${file.additions}/-${file.deletions} SHA: ${file.sha}`,
|
|
)
|
|
.join("\n");
|
|
}
|