UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

329 lines (328 loc) • 13.2 kB
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