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

216 lines
6.3 KiB
JavaScript

/**
* Extract release information from either release event or workflow_dispatch input
*/
function extractReleaseInfo(context) {
let releaseVersion;
let releaseTag;
if (context.eventName === "workflow_dispatch") {
// Manual trigger with version parameter
releaseVersion = context.payload.inputs?.release_version;
if (!releaseVersion) {
throw new Error("release_version input is required for workflow_dispatch");
}
releaseTag = releaseVersion.startsWith("v") ? releaseVersion : `v${releaseVersion}`;
releaseVersion = releaseVersion.replace(/^v/, ""); // Remove 'v' prefix if present
console.log(`Processing manual workflow for release: ${releaseTag} (${releaseVersion})`);
} else {
// Automatic trigger from release event
const release = context.payload.release;
if (!release) {
throw new Error("Release information not found in payload");
}
releaseTag = release.tag_name;
releaseVersion = releaseTag.replace(/^v/, ""); // Remove 'v' prefix if present
console.log(`Processing release event: ${releaseTag} (${releaseVersion})`);
}
const versionMatch = releaseVersion.match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!versionMatch) {
console.log(`Skipping invalid release: ${releaseVersion}`);
throw new Error(`Invalid version format: ${releaseVersion}`);
}
const [, major, minor, patch] = versionMatch;
const nextPatchVersion = `${major}.${minor}.${parseInt(patch) + 1}`;
const releaseLabel = `v${releaseVersion}`;
const nextPatchLabel = `v${nextPatchVersion}`;
// Get release branch name (e.g., branch-3.1 for v3.1.4)
const releaseBranch = `branch-${major}.${minor}`;
console.log(`Release label: ${releaseLabel}`);
console.log(`Next patch label: ${nextPatchLabel}`);
console.log(`Release branch: ${releaseBranch}`);
return {
releaseVersion,
releaseTag,
releaseLabel,
nextPatchLabel,
releaseBranch,
};
}
/**
* Helper function to extract PR number from commit message
*/
function extractPRNumberFromCommitMessage(commitMessage) {
const prRegex = /\(#(\d+)\)$/;
const lines = commitMessage.split("\n");
for (const line of lines) {
const match = line.trim().match(prRegex);
if (match) {
return parseInt(match[1], 10);
}
}
return null;
}
/**
* Extract PR numbers from release branch commits
*/
async function extractPRNumbersFromBranch(github, context, releaseBranch) {
const releasePRNumbers = new Set();
try {
const commits = await github.paginate(github.rest.repos.listCommits, {
owner: context.repo.owner,
repo: context.repo.repo,
sha: releaseBranch,
since: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), // Last 30 days
});
for (const commit of commits) {
const prNumber = extractPRNumberFromCommitMessage(commit.commit.message);
if (prNumber) {
releasePRNumbers.add(prNumber);
}
}
console.log(`Found ${releasePRNumbers.size} PR numbers from ${releaseBranch} commits`);
} catch (error) {
if (error.status === 404) {
console.log(
`Release branch '${releaseBranch}' not found. This may be expected for new releases.`
);
console.log("Skipping commit analysis - will update all PRs with the release label.");
} else {
throw error;
}
}
return releasePRNumbers;
}
/**
* Fetch all merged PRs with a specific label
*/
async function fetchPRsWithLabel(github, context, releaseLabel) {
const allIssues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
labels: releaseLabel,
state: "all",
});
const prsWithReleaseLabel = allIssues.filter((item) => {
if (!item.pull_request) return false;
if (item.state === "open") return true;
if (item.state === "closed" && item.pull_request.merged_at) return true;
return false;
});
console.log(`Found ${prsWithReleaseLabel.length} PRs with label ${releaseLabel}`);
return prsWithReleaseLabel;
}
/**
* Update PR labels for PRs not included in release
*/
async function updatePRLabels(
github,
context,
prsWithReleaseLabel,
releasePRNumbers,
releaseLabel,
nextPatchLabel
) {
const pullRequests = prsWithReleaseLabel.filter((item) => item.pull_request);
console.log(
`Processing ${pullRequests.length} PRs (filtered out ${
prsWithReleaseLabel.length - pullRequests.length
} issues)`
);
const prsToUpdate = [];
for (const pr of pullRequests) {
if (releasePRNumbers.has(pr.number)) continue;
prsToUpdate.push(pr.number);
}
console.log(`Found ${prsToUpdate.length} PRs that need label updates: ${prsToUpdate.join(", ")}`);
for (const prNumber of prsToUpdate) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: releaseLabel,
});
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: [nextPatchLabel],
});
console.log(`Updated PR #${prNumber}: ${releaseLabel}${nextPatchLabel}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.log(`Warning: Failed to update labels for PR #${prNumber}: ${errorMessage}`);
}
}
}
/**
* Main function to update release labels
*
* This script checks all PRs labeled with a release version and updates
* their labels to the next patch version if they weren't actually included
* in the release (handles cherry-picked commits properly).
*/
async function updateReleaseLabels({ github, context }) {
try {
const releaseInfo = extractReleaseInfo(context);
const releasePRNumbers = await extractPRNumbersFromBranch(
github,
context,
releaseInfo.releaseBranch
);
const prsWithReleaseLabel = await fetchPRsWithLabel(github, context, releaseInfo.releaseLabel);
await updatePRLabels(
github,
context,
prsWithReleaseLabel,
releasePRNumbers,
releaseInfo.releaseLabel,
releaseInfo.nextPatchLabel
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Error updating release labels: ${errorMessage}`);
throw error;
}
}
module.exports = { updateReleaseLabels };