Files
2026-07-13 13:22:34 +08:00

294 lines
7.8 KiB
JavaScript

function extractStackedPrBaseSha(prBody, headRef) {
if (!prBody || !prBody.includes("Stacked PR")) {
return null;
}
const marker = `[**${headRef}**]`;
const lines = prBody.split("\n");
for (const line of lines) {
if (line.includes(marker)) {
const match = line.match(/\/files\/(?<base>[a-f0-9]{7,40})\.\.(?<head>[a-f0-9]{7,40})/);
if (match) {
return match.groups.base;
}
}
}
return null;
}
async function getPrInfo(github, owner, repo, prNumber) {
console.error("Fetching PR information...");
const { data } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
});
return {
title: data.title,
body: data.body || "",
headSha: data.head.sha,
headRef: data.head.ref,
};
}
async function getPrDiff(github, owner, repo, prNumber, body, headSha, headRef) {
console.error("Fetching PR diff...");
let diff;
const baseSha = extractStackedPrBaseSha(body, headRef);
if (baseSha) {
console.error(
`Detected stacked PR, fetching incremental diff: ${baseSha.substring(
0,
7
)}..${headSha.substring(0, 7)}`
);
const response = await github.request("GET /repos/{owner}/{repo}/compare/{basehead}", {
owner,
repo,
basehead: `${baseSha}...${headSha}`,
headers: {
accept: "application/vnd.github.v3.diff",
},
});
diff = response.data;
} else {
const response = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
mediaType: {
format: "diff",
},
});
diff = response.data;
}
const maxLength = 50000;
if (diff.length > maxLength) {
diff = diff.substring(0, maxLength) + "\n\n... [diff truncated due to length] ...";
}
return diff;
}
async function getClosingIssues(github, owner, repo, prNumber) {
console.error("Fetching closing issues...");
const query = `
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
title
}
}
}
}
}
`;
try {
const result = await github.graphql(query, {
owner,
repo,
prNumber,
});
const nodes = result?.repository?.pullRequest?.closingIssuesReferences?.nodes;
if (nodes && Array.isArray(nodes)) {
return nodes.map((node) => ({
number: node.number,
title: node.title,
}));
}
return [];
} catch (error) {
console.error(`Warning: Failed to fetch closing issues: ${error.message}`);
return [];
}
}
function extractDescription(body) {
const pattern = /### What changes are proposed in this pull request\?\s*(.+?)(?=###|$)/is;
const match = body.match(pattern);
if (match) {
return match[1].trim();
}
return body;
}
function buildPrompt(title, body, diff, linkedIssues = null) {
const description = extractDescription(body).trim() || "(No description provided)";
let linkedIssuesSection = "";
if (linkedIssues && linkedIssues.length > 0) {
linkedIssuesSection = "\n## Linked Issues\n";
for (const { number, title } of linkedIssues) {
linkedIssuesSection += `- #${number}: ${title}\n`;
}
linkedIssuesSection += "\n";
}
return `Rewrite the PR title to be more descriptive and follow the guidelines below.
## Current PR Title
${title}
## PR Description
${description}
${linkedIssuesSection}
## Code Changes (Diff)
\`\`\`diff
${diff}
\`\`\`
## Guidelines for a good PR title:
1. Start with a verb in imperative mood (e.g., "Add", "Fix", "Update", "Remove", "Refactor")
2. Be specific about what changed and where
3. Keep it concise (aim for 72 characters or less, 100 characters maximum)
4. Do not include issue numbers in the title (they belong in the PR body)
5. Focus on the "what" and "why", not the "how"
6. Use proper capitalization (capitalize first letter, no period at end)
7. Use backticks for code/file references (e.g., \`ClassName\`, \`function_name\`, \`module.path\`)
Rewrite the PR title following these guidelines.`;
}
function buildIssuePrompt(title, body) {
const description = body.trim() || "(No description provided)";
return `Rewrite the issue title to be more descriptive and follow the guidelines below.
## Current Issue Title
${title}
## Issue Description
${description}
## Guidelines for a good issue title:
1. Clearly describe the problem or feature request (e.g., "\`load_model\` fails with \`KeyError\`
when model has nested flavors", "Support custom metric types in autologging")
2. Be specific about what the issue is about
3. Keep it concise (aim for 72 characters or less, 100 characters maximum)
4. Do not include issue numbers in the title
5. Focus on the problem or feature request
6. Use proper capitalization (capitalize first letter, no period at end)
7. Use backticks for code/file references (e.g., \`ClassName\`, \`function_name\`, \`module.path\`)
Rewrite the issue title following these guidelines.`;
}
async function callAnthropicApi(prompt, apiKey) {
console.error("Calling Claude API...");
const requestBody = {
model: "claude-haiku-4-5-20251001",
max_tokens: 256,
messages: [{ role: "user", content: prompt }],
output_config: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
title: {
type: "string",
description: "The rewritten PR title.",
},
},
required: ["title"],
additionalProperties: false,
},
},
},
};
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.error("API Response:");
console.error(JSON.stringify(data, null, 2));
const content = JSON.parse(data.content[0].text);
return content.title;
}
async function getIssueInfo(github, owner, repo, issueNumber) {
console.error("Fetching issue information...");
const { data } = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber,
});
return {
title: data.title,
body: data.body || "",
};
}
module.exports = async ({ github, context, core }) => {
// Parse inputs from environment
const repo = process.env.REPO;
const number = parseInt(process.env.NUMBER, 10);
const isPr = process.env.IS_PR === "true";
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!repo || !number || !apiKey) {
throw new Error("Missing required environment variables: REPO, NUMBER, ANTHROPIC_API_KEY");
}
const [owner, repoName] = repo.split("/");
let newTitle;
if (isPr) {
// Generate PR title
const { title, body, headSha, headRef } = await getPrInfo(github, owner, repoName, number);
console.error(`Original title: ${title}`);
const diff = await getPrDiff(github, owner, repoName, number, body, headSha, headRef);
const linkedIssues = await getClosingIssues(github, owner, repoName, number);
if (linkedIssues.length > 0) {
console.error(`Found ${linkedIssues.length} linked issue(s)`);
}
const prompt = buildPrompt(title, body, diff, linkedIssues);
newTitle = await callAnthropicApi(prompt, apiKey);
} else {
// Generate issue title
const { title, body } = await getIssueInfo(github, owner, repoName, number);
console.error(`Original title: ${title}`);
const prompt = buildIssuePrompt(title, body);
newTitle = await callAnthropicApi(prompt, apiKey);
}
console.log(newTitle);
core.setOutput("new_title", newTitle);
};