mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
* feat: use GitHub display name in Co-authored-by trailers - Add name field to GitHubAuthor type - Update GraphQL queries to fetch user display names - Add triggerDisplayName to CommonFields type - Extract display name from fetched GitHub data in prepareContext - Update Co-authored-by trailer generation to use display name when available This ensures consistency with GitHub's web interface behavior where Co-authored-by trailers use the user's display name rather than username. Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com> * fix: update GraphQL queries to handle Actor type correctly The name field is only available on the User subtype of Actor in GitHub's GraphQL API. This commit updates the queries to use inline fragments (... on User) to conditionally access the name field when the actor is a User type. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: clarify Co-authored-by instructions in prompt Replace interpolated values with clear references to XML tags and add explicit formatting instructions. This makes it clearer how to use the GitHub display name when available while maintaining the username for the email portion. Changes: - Use explicit references to <trigger_display_name> and <trigger_username> tags - Add clear formatting instructions and example - Explain fallback behavior when display name is not available 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: fetch trigger user display name via dedicated GraphQL query Instead of trying to extract the display name from existing data (which was incomplete due to Actor type limitations), we now: - Add a dedicated USER_QUERY to fetch user display names - Pass the trigger username to fetchGitHubData - Fetch the display name during data collection phase - Simplify prepareContext to use the pre-fetched display name This ensures we always get the correct display name for Co-authored-by trailers, regardless of where the trigger came from. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
234 lines
5.9 KiB
TypeScript
234 lines
5.9 KiB
TypeScript
import { execSync } from "child_process";
|
|
import type { Octokits } from "../api/client";
|
|
import { ISSUE_QUERY, PR_QUERY, USER_QUERY } from "../api/queries/github";
|
|
import type {
|
|
GitHubComment,
|
|
GitHubFile,
|
|
GitHubIssue,
|
|
GitHubPullRequest,
|
|
GitHubReview,
|
|
IssueQueryResponse,
|
|
PullRequestQueryResponse,
|
|
} from "../types";
|
|
import type { CommentWithImages } from "../utils/image-downloader";
|
|
import { downloadCommentImages } from "../utils/image-downloader";
|
|
|
|
type FetchDataParams = {
|
|
octokits: Octokits;
|
|
repository: string;
|
|
prNumber: string;
|
|
isPR: boolean;
|
|
triggerUsername?: string;
|
|
};
|
|
|
|
export type GitHubFileWithSHA = GitHubFile & {
|
|
sha: string;
|
|
};
|
|
|
|
export type FetchDataResult = {
|
|
contextData: GitHubPullRequest | GitHubIssue;
|
|
comments: GitHubComment[];
|
|
changedFiles: GitHubFile[];
|
|
changedFilesWithSHA: GitHubFileWithSHA[];
|
|
reviewData: { nodes: GitHubReview[] } | null;
|
|
imageUrlMap: Map<string, string>;
|
|
triggerDisplayName?: string | null;
|
|
};
|
|
|
|
export async function fetchGitHubData({
|
|
octokits,
|
|
repository,
|
|
prNumber,
|
|
isPR,
|
|
triggerUsername,
|
|
}: FetchDataParams): Promise<FetchDataResult> {
|
|
const [owner, repo] = repository.split("/");
|
|
if (!owner || !repo) {
|
|
throw new Error("Invalid repository format. Expected 'owner/repo'.");
|
|
}
|
|
|
|
let contextData: GitHubPullRequest | GitHubIssue | null = null;
|
|
let comments: GitHubComment[] = [];
|
|
let changedFiles: GitHubFile[] = [];
|
|
let reviewData: { nodes: GitHubReview[] } | null = null;
|
|
|
|
try {
|
|
if (isPR) {
|
|
// Fetch PR data with all comments and file information
|
|
const prResult = await octokits.graphql<PullRequestQueryResponse>(
|
|
PR_QUERY,
|
|
{
|
|
owner,
|
|
repo,
|
|
number: parseInt(prNumber),
|
|
},
|
|
);
|
|
|
|
if (prResult.repository.pullRequest) {
|
|
const pullRequest = prResult.repository.pullRequest;
|
|
contextData = pullRequest;
|
|
changedFiles = pullRequest.files.nodes || [];
|
|
comments = pullRequest.comments?.nodes || [];
|
|
reviewData = pullRequest.reviews || [];
|
|
|
|
console.log(`Successfully fetched PR #${prNumber} data`);
|
|
} else {
|
|
throw new Error(`PR #${prNumber} not found`);
|
|
}
|
|
} else {
|
|
// Fetch issue data
|
|
const issueResult = await octokits.graphql<IssueQueryResponse>(
|
|
ISSUE_QUERY,
|
|
{
|
|
owner,
|
|
repo,
|
|
number: parseInt(prNumber),
|
|
},
|
|
);
|
|
|
|
if (issueResult.repository.issue) {
|
|
contextData = issueResult.repository.issue;
|
|
comments = contextData?.comments?.nodes || [];
|
|
|
|
console.log(`Successfully fetched issue #${prNumber} data`);
|
|
} else {
|
|
throw new Error(`Issue #${prNumber} not found`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to fetch ${isPR ? "PR" : "issue"} data:`, error);
|
|
throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`);
|
|
}
|
|
|
|
// Compute SHAs for changed files
|
|
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
|
if (isPR && changedFiles.length > 0) {
|
|
changedFilesWithSHA = changedFiles.map((file) => {
|
|
// Don't compute SHA for deleted files
|
|
if (file.changeType === "DELETED") {
|
|
return {
|
|
...file,
|
|
sha: "deleted",
|
|
};
|
|
}
|
|
|
|
try {
|
|
// Use git hash-object to compute the SHA for the current file content
|
|
const sha = execSync(`git hash-object "${file.path}"`, {
|
|
encoding: "utf-8",
|
|
}).trim();
|
|
return {
|
|
...file,
|
|
sha,
|
|
};
|
|
} catch (error) {
|
|
console.warn(`Failed to compute SHA for ${file.path}:`, error);
|
|
// Return original file without SHA if computation fails
|
|
return {
|
|
...file,
|
|
sha: "unknown",
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Prepare all comments for image processing
|
|
const issueComments: CommentWithImages[] = comments
|
|
.filter((c) => c.body)
|
|
.map((c) => ({
|
|
type: "issue_comment" as const,
|
|
id: c.databaseId,
|
|
body: c.body,
|
|
}));
|
|
|
|
const reviewBodies: CommentWithImages[] =
|
|
reviewData?.nodes
|
|
?.filter((r) => r.body)
|
|
.map((r) => ({
|
|
type: "review_body" as const,
|
|
id: r.databaseId,
|
|
pullNumber: prNumber,
|
|
body: r.body,
|
|
})) ?? [];
|
|
|
|
const reviewComments: CommentWithImages[] =
|
|
reviewData?.nodes
|
|
?.flatMap((r) => r.comments?.nodes ?? [])
|
|
.filter((c) => c.body)
|
|
.map((c) => ({
|
|
type: "review_comment" as const,
|
|
id: c.databaseId,
|
|
body: c.body,
|
|
})) ?? [];
|
|
|
|
// Add the main issue/PR body if it has content
|
|
const mainBody: CommentWithImages[] = contextData.body
|
|
? [
|
|
{
|
|
...(isPR
|
|
? {
|
|
type: "pr_body" as const,
|
|
pullNumber: prNumber,
|
|
body: contextData.body,
|
|
}
|
|
: {
|
|
type: "issue_body" as const,
|
|
issueNumber: prNumber,
|
|
body: contextData.body,
|
|
}),
|
|
},
|
|
]
|
|
: [];
|
|
|
|
const allComments = [
|
|
...mainBody,
|
|
...issueComments,
|
|
...reviewBodies,
|
|
...reviewComments,
|
|
];
|
|
|
|
const imageUrlMap = await downloadCommentImages(
|
|
octokits,
|
|
owner,
|
|
repo,
|
|
allComments,
|
|
);
|
|
|
|
// Fetch trigger user display name if username is provided
|
|
let triggerDisplayName: string | null | undefined;
|
|
if (triggerUsername) {
|
|
triggerDisplayName = await fetchUserDisplayName(octokits, triggerUsername);
|
|
}
|
|
|
|
return {
|
|
contextData,
|
|
comments,
|
|
changedFiles,
|
|
changedFilesWithSHA,
|
|
reviewData,
|
|
imageUrlMap,
|
|
triggerDisplayName,
|
|
};
|
|
}
|
|
|
|
export type UserQueryResponse = {
|
|
user: {
|
|
name: string | null;
|
|
};
|
|
};
|
|
|
|
export async function fetchUserDisplayName(
|
|
octokits: Octokits,
|
|
login: string,
|
|
): Promise<string | null> {
|
|
try {
|
|
const result = await octokits.graphql<UserQueryResponse>(USER_QUERY, {
|
|
login,
|
|
});
|
|
return result.user.name;
|
|
} catch (error) {
|
|
console.warn(`Failed to fetch user display name for ${login}:`, error);
|
|
return null;
|
|
}
|
|
}
|