Files
2026-07-13 13:39:12 +08:00

101 lines
3.2 KiB
JavaScript

/**
* Delivery detection: PR merged + CHANGELOG + git log, with confidence grading.
*/
const VERSION_HEADER_RE = /^##\s+\[?(\d+\.\d+\.\d+)\]?/;
export function parseChangelog(text, issueNumber) {
if (typeof text !== "string") return null;
if (!Number.isInteger(issueNumber) || issueNumber <= 0) return null;
const needle = `#${issueNumber}`;
const lines = text.split("\n");
let currentSection = null;
let currentVersion = null;
for (const line of lines) {
const headerMatch = line.match(VERSION_HEADER_RE);
if (headerMatch) {
currentSection = line.trim();
currentVersion = headerMatch[1];
continue;
}
if (!currentSection) continue;
// Match #N with word boundary: look for needle followed by non-word char or end
const idx = line.indexOf(needle);
if (idx !== -1) {
const nextIdx = idx + needle.length;
const nextChar = line[nextIdx];
const isWordBoundary = nextIdx >= line.length || /\W/.test(nextChar);
if (isWordBoundary) {
return {
section: currentSection,
version: currentVersion,
line: line.trim(),
};
}
}
}
return null;
}
function isExplicitClose(pr, issueNumber) {
const text = `${pr.title ?? ""}\n${pr.body ?? ""}`;
const re = new RegExp(`\\b(closes?|fixes?|fixed|resolves?|resolved)\\s+#${issueNumber}\\b`, "i");
return re.test(text);
}
function justMentions(pr, issueNumber) {
const text = `${pr.title ?? ""}\n${pr.body ?? ""}`;
return new RegExp(`#${issueNumber}\\b`).test(text);
}
export function detectDelivered(issueNumber, signals) {
const { mergedPrs = [], changelog = "", gitCommits = [] } = signals;
const closesPr = mergedPrs.find((p) => isExplicitClose(p, issueNumber));
const mentionPr = closesPr || mergedPrs.find((p) => justMentions(p, issueNumber));
const changelogHit = parseChangelog(changelog, issueNumber);
const gitHit = gitCommits.length > 0 ? gitCommits[0] : null;
const A = !!closesPr;
const B = !!(mentionPr && !closesPr);
const C = !!changelogHit;
const D = !!gitHit;
let confidence = "none";
if (A) confidence = "high";
else if ((C && D) || (B && C) || (B && D)) confidence = "medium";
else if (B || C || D) confidence = "low";
const evidence = {};
if (closesPr) {
evidence.pr_merged = {
number: closesPr.number,
merged_at: closesPr.mergedAt,
ref: `closes #${issueNumber}`,
};
} else if (mentionPr) {
evidence.pr_merged = {
number: mentionPr.number,
merged_at: mentionPr.mergedAt,
ref: `mentions #${issueNumber}`,
};
}
if (changelogHit) evidence.changelog_section = changelogHit.section;
if (gitHit) evidence.git_commits = gitCommits.slice(0, 5).map((c) => c.hash);
return { confidence, evidence };
}
export function resolveVersion(mergedAt, tagsByDate, currentReleaseBranch) {
const mergedTime = mergedAt instanceof Date ? mergedAt.getTime() : new Date(mergedAt).getTime();
const after = tagsByDate.find((t) => t.date.getTime() >= mergedTime);
if (after) {
return { version: after.name, version_source: "tag_after_merge" };
}
return {
version: currentReleaseBranch || "unreleased",
version_source: "branch_unreleased",
};
}