renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
329 lines (328 loc) • 13.2 kB
JavaScript
import { get, set } from "../../../../../util/cache/memory/index.js";
import { newlineRegex, regEx } from "../../../../../util/regex.js";
import { coerceString } from "../../../../../util/string.js";
import { logger } from "../../../../../logger/index.js";
import { isHttpUrl, joinUrlParts } from "../../../../../util/url.js";
import { detectPlatform } from "../../../../../util/common.js";
import { get as get$1, set as set$1 } from "../../../../../util/cache/package/index.js";
import { linkify } from "../../../../../util/markdown.js";
import { getReleaseList as getReleaseList$1, getReleaseNotesMd as getReleaseNotesMd$1 } from "./bitbucket/index.js";
import { getReleaseNotesMd as getReleaseNotesMd$2 } from "./bitbucket-server/index.js";
import { getReleaseList as getReleaseList$2, getReleaseNotesMd as getReleaseNotesMd$3 } from "./forgejo/index.js";
import { getReleaseList as getReleaseList$3, getReleaseNotesMd as getReleaseNotesMd$4 } from "./gitea/index.js";
import { getReleaseList as getReleaseList$4, getReleaseNotesMd as getReleaseNotesMd$5 } from "./github/index.js";
import { getReleaseList as getReleaseList$5, getReleaseNotesMd as getReleaseNotesMd$6 } from "./gitlab/index.js";
import { isDate, isUndefined } from "@sindresorhus/is";
import { DateTime } from "luxon";
import MarkdownIt from "markdown-it";
//#region lib/workers/repository/update/pr/changelog/release-notes.ts
const markdown = new MarkdownIt("zero");
markdown.enable([
"heading",
"lheading",
"fence"
]);
const repositoriesToSkipMdFetching = ["facebook/react-native"];
async function getReleaseList(project, release) {
logger.trace("getReleaseList()");
const { apiBaseUrl, repository, type } = project;
try {
switch (type) {
case "bitbucket": return getReleaseList$1(project, release);
case "bitbucket-server":
logger.trace("Unsupported Bitbucket Server feature. Skipping release fetching.");
return [];
case "forgejo": return await getReleaseList$2(project, release);
case "gitea": return await getReleaseList$3(project, release);
case "github": return await getReleaseList$4(project, release);
case "gitlab": return await getReleaseList$5(project, release);
default:
logger.warn({
apiBaseUrl,
repository,
type
}, "Invalid project type");
return [];
}
} catch (err) /* istanbul ignore next */ {
if (err.statusCode === 404) logger.debug({
repository,
type,
apiBaseUrl
}, "getReleaseList 404");
else logger.debug({
repository,
type,
apiBaseUrl,
err
}, "getReleaseList error");
}
return [];
}
function getCachedReleaseList(project, release) {
const { repository, apiBaseUrl } = project;
const cacheKey = `getReleaseList-${apiBaseUrl}-${repository}`;
const cachedResult = get(cacheKey);
// istanbul ignore if
if (cachedResult !== void 0) return cachedResult;
const promisedRes = getReleaseList(project, release);
set(cacheKey, promisedRes);
return promisedRes;
}
function massageBody(input, baseUrl) {
let body = coerceString(input);
body = body.replace(regEx(/\r\n/g), "\n");
body = body.replace(regEx(/^<a name="[^"]*"><\/a>\n/), "");
body = body.replace(regEx(`^##? \\[[^\\]]*\\]\\(${baseUrl}[^/]*/[^/]*/compare/.*?\\n`, void 0, false), "");
body = `\n${body}\n`.replace(regEx(`\\n${baseUrl}[^/]+/[^/]+/compare/[^\\n]+(\\n|$)`), "\n");
body = body.split(regEx(/(```[\s\S]*?```)/g)).map((part) => part.startsWith("```") ? part : part.replace(regEx(/\n\s*####? /g), "\n##### ").replace(regEx(/\n\s*## /g), "\n#### ").replace(regEx(/\n\s*# /g), "\n### ")).join("");
return body.trim();
}
function massageName(input, version) {
let name = input ?? "";
if (version) name = name.replace(RegExp(`^(Release )?v?${version}`, "i"), "").trim();
name = name.trim();
if (!name.length) return;
return name;
}
async function getReleaseNotes(project, release, config) {
const { packageName, depName, repository } = project;
const { version, gitRef } = release;
logger.trace(`getReleaseNotes(${repository}, ${version}, ${packageName}, ${depName})`);
const releases = await getCachedReleaseList(project, release);
logger.trace({ releases }, "Release list from getReleaseList");
let releaseNotes = null;
let matchedRelease = getExactReleaseMatch(packageName, depName, version, releases);
if (isUndefined(matchedRelease)) matchedRelease = releases.find((r) => r.tag === version || r.tag === `v${version}` || r.tag === gitRef || r.tag === `v${gitRef}`);
if (isUndefined(matchedRelease) && config.extractVersion) {
const extractVersionRegEx = regEx(config.extractVersion);
matchedRelease = releases.find((r) => {
return version === extractVersionRegEx.exec(r.tag)?.groups?.version;
});
}
releaseNotes = await releaseNotesResult(matchedRelease, project);
logger.trace({ releaseNotes });
return releaseNotes;
}
function getExactReleaseMatch(packageName, depName, version, releases) {
const exactReleaseReg = regEx(`(?:^|/)(?:${packageName}|${depName})[@_/-]v?${version}`);
return releases.filter((r) => r.tag?.endsWith(version)).find((r) => exactReleaseReg.test(r.tag));
}
async function releaseNotesResult(releaseMatch, project) {
if (!releaseMatch) return null;
const { baseUrl, repository } = project;
const releaseNotes = releaseMatch;
if (detectPlatform(baseUrl) === "gitlab") releaseNotes.url = `${baseUrl}${repository}/tags/${releaseMatch.tag}`;
else releaseNotes.url = releaseMatch.url ? releaseMatch.url : `${baseUrl}${repository}/releases/${releaseMatch.tag}`;
releaseNotes.body = massageBody(releaseNotes.body, baseUrl);
releaseNotes.name = massageName(releaseNotes.name, releaseNotes.tag);
if (releaseNotes.body.length || releaseNotes.name?.length) try {
if (baseUrl !== "https://gitlab.com/") releaseNotes.body = await linkify(releaseNotes.body, { repository: `${baseUrl}${repository}` });
} catch (err) /* istanbul ignore next */ {
logger.warn({
err,
baseUrl,
repository
}, "Error linkifying");
}
else return null;
return releaseNotes;
}
function sectionize(text, level) {
const sections = [];
const lines = text.split(newlineRegex);
markdown.parse(text, void 0).forEach((token) => {
if (token.type === "heading_open") {
const lev = +token.tag.substring(1);
if (lev <= level) sections.push([lev, token.map[0]]);
}
});
sections.push([-1, lines.length]);
const result = [];
for (let i = 1; i < sections.length; i += 1) {
const [lev, start] = sections[i - 1];
const [, end] = sections[i];
if (lev === level) result.push(lines.slice(start, end).join("\n"));
}
return result;
}
async function getReleaseNotesMdFileInner(project) {
const { repository, type } = project;
const apiBaseUrl = project.apiBaseUrl;
const sourceDirectory = project.sourceDirectory;
try {
switch (type) {
case "bitbucket": return await getReleaseNotesMd$1(repository, apiBaseUrl, sourceDirectory);
case "bitbucket-server": return await getReleaseNotesMd$2(repository, apiBaseUrl, sourceDirectory);
case "forgejo": return await getReleaseNotesMd$3(repository, apiBaseUrl, sourceDirectory);
case "gitea": return await getReleaseNotesMd$4(repository, apiBaseUrl, sourceDirectory);
case "github": return await getReleaseNotesMd$5(repository, apiBaseUrl, sourceDirectory);
case "gitlab": return await getReleaseNotesMd$6(repository, apiBaseUrl, sourceDirectory);
default:
logger.warn({
apiBaseUrl,
repository,
type
}, "Invalid project type");
return null;
}
} catch (err) /* istanbul ignore next */ {
if (err.statusCode === 404) logger.debug({
repository,
type,
apiBaseUrl
}, "Error 404 getting changelog md");
else logger.debug({
err,
repository,
type,
apiBaseUrl
}, "Error getting changelog md");
}
return null;
}
function getReleaseNotesMdFile(project) {
const { sourceDirectory, repository, apiBaseUrl } = project;
const cacheKey = sourceDirectory ? `getReleaseNotesMdFile@v2-${repository}-${sourceDirectory}-${apiBaseUrl}` : `getReleaseNotesMdFile@v2-${repository}-${apiBaseUrl}`;
const cachedResult = get(cacheKey);
// istanbul ignore if
if (cachedResult !== void 0) return cachedResult;
const promisedRes = getReleaseNotesMdFileInner(project);
set(cacheKey, promisedRes);
return promisedRes;
}
async function getReleaseNotesMd(project, release) {
const { baseUrl, repository, packageName } = project;
const version = release.version;
logger.trace(`getReleaseNotesMd(${repository}, ${version})`);
if (shouldSkipChangelogMd(repository)) return null;
const changelog = await getReleaseNotesMdFile(project);
if (!changelog) return null;
const { changelogFile } = changelog;
const changelogMd = changelog.changelogMd.replace(regEx(/\n\s*<a name="[^"]*">.*?<\/a>\n/g), "\n");
for (const level of [
1,
2,
3,
4,
5,
6,
7
]) {
const changelogParsed = sectionize(changelogMd, level);
if (changelogParsed.length >= 2) for (const section of changelogParsed) try {
const [heading] = section.replace(regEx(/[[\]()]/g), " ").split(newlineRegex);
const title = heading.replace(regEx(/^\s*#*\s*/), "").split(" ").filter(Boolean);
const body = section.replace(regEx(/.*?\n(-{3,}\n)?/), "").trim();
const notesSourceUrl = getNotesSourceUrl(baseUrl, repository, project, changelogFile);
const url = `${notesSourceUrl}#${title.filter((word) => !isHttpUrl(word)).join("-").replace(regEx(/[^A-Za-z0-9-]/g), "")}`;
for (const word of title) if (word.includes(version) && !isHttpUrl(word)) {
logger.trace({ body }, `Found release notes for v${version}`);
return {
body: await linkifyBody(project, body),
url,
notesSourceUrl
};
}
const releasesRegex = regEx(/([0-9]{4}-[0-9]{2}-[0-9]{2})/);
if (packageName && heading.search(releasesRegex) !== -1) {
const linkRefDefRegex = regEx(/^\s*\[[^\]]+\]:\s*\S+/);
if (body.split("\n").some((line) => line.includes(packageName) && line.includes(version) && !isHttpUrl(line) && !linkRefDefRegex.test(line))) {
logger.trace({ body }, `Found release notes for v${version}`);
return {
body: await linkifyBody(project, body),
url,
notesSourceUrl
};
}
}
} catch (err) /* istanbul ignore next */ {
logger.warn({
file: changelogFile,
err
}, `Error parsing changelog file`);
}
logger.trace({ repository }, `No level ${level} changelogs headings found`);
}
logger.trace({
repository,
version
}, `No entry found in ${changelogFile}`);
return null;
}
/**
* Determine how long to cache release notes based on when the version was released.
*
* It's not uncommon for release notes to be updated shortly after the release itself,
* so only cache for about an hour when the release is less than a week old. Otherwise,
* cache for days.
*/
function releaseNotesCacheMinutes(releaseDate) {
const dt = isDate(releaseDate) ? DateTime.fromJSDate(releaseDate) : DateTime.fromISO(releaseDate);
const now = DateTime.local();
if (!dt.isValid || now.diff(dt, "days").days < 7) return 55;
if (now.diff(dt, "months").months < 6) return 1435;
return 14495;
}
async function addReleaseNotes(input, config) {
if (!input?.versions || !input.project?.type) {
logger.debug("Missing project or versions");
return input ?? null;
}
const output = {
...input,
versions: [],
hasReleaseNotes: false
};
const { repository, sourceDirectory, type: projectType } = input.project;
const cacheNamespace = `changelog-${projectType}-notes@v2`;
const cacheKeyPrefix = sourceDirectory ? `${repository}:${sourceDirectory}` : `${repository}`;
for (const v of input.versions) {
let releaseNotes;
const cacheKey = `${cacheKeyPrefix}:${v.version}`;
releaseNotes = await get$1(cacheNamespace, cacheKey);
releaseNotes ??= await getReleaseNotesMd(input.project, v);
releaseNotes ??= await getReleaseNotes(input.project, v, config);
if (!releaseNotes && v.compare.url) releaseNotes = {
url: v.compare.url,
notesSourceUrl: ""
};
const cacheMinutes = releaseNotesCacheMinutes(v.date);
await set$1(cacheNamespace, cacheKey, releaseNotes, cacheMinutes);
output.versions.push({
...v,
releaseNotes
});
if (releaseNotes) output.hasReleaseNotes = true;
}
return output;
}
/**
* Skip fetching changelog/release-notes markdown files.
* Will force a fallback to using GitHub release notes
*/
function shouldSkipChangelogMd(repository) {
return repositoriesToSkipMdFetching.includes(repository);
}
function getNotesSourceUrl(baseUrl, repository, project, changelogFile) {
if (project.type === "bitbucket-server") {
const [projectKey, repositorySlug] = repository.split("/");
return joinUrlParts(baseUrl, "projects", projectKey, "repos", repositorySlug, "browse", changelogFile, "?at=HEAD");
}
return joinUrlParts(baseUrl, repository, project.type === "bitbucket" ? "src" : "blob", "HEAD", changelogFile);
}
async function linkifyBody({ baseUrl, repository }, bodyStr) {
const body = massageBody(bodyStr, baseUrl);
if (body?.length) try {
return await linkify(body, { repository: `${baseUrl}${repository}` });
} catch (err) /* istanbul ignore next */ {
logger.warn({
body,
err
}, "linkify error");
}
return body;
}
//#endregion
export { addReleaseNotes };
//# sourceMappingURL=release-notes.js.map