UNPKG

renovate

Version:

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

383 lines • 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getReleaseList = getReleaseList; exports.getCachedReleaseList = getCachedReleaseList; exports.massageBody = massageBody; exports.massageName = massageName; exports.getReleaseNotes = getReleaseNotes; exports.getReleaseNotesMdFileInner = getReleaseNotesMdFileInner; exports.getReleaseNotesMdFile = getReleaseNotesMdFile; exports.getReleaseNotesMd = getReleaseNotesMd; exports.releaseNotesCacheMinutes = releaseNotesCacheMinutes; exports.addReleaseNotes = addReleaseNotes; exports.shouldSkipChangelogMd = shouldSkipChangelogMd; const tslib_1 = require("tslib"); const is_1 = tslib_1.__importDefault(require("@sindresorhus/is")); const luxon_1 = require("luxon"); const markdown_it_1 = tslib_1.__importDefault(require("markdown-it")); const logger_1 = require("../../../../../logger"); const memCache = tslib_1.__importStar(require("../../../../../util/cache/memory")); const packageCache = tslib_1.__importStar(require("../../../../../util/cache/package")); const common_1 = require("../../../../../util/common"); const markdown_1 = require("../../../../../util/markdown"); const regex_1 = require("../../../../../util/regex"); const string_1 = require("../../../../../util/string"); const url_1 = require("../../../../../util/url"); const bitbucket = tslib_1.__importStar(require("./bitbucket")); const bitbucketServer = tslib_1.__importStar(require("./bitbucket-server")); const gitea = tslib_1.__importStar(require("./gitea")); const github = tslib_1.__importStar(require("./github")); const gitlab = tslib_1.__importStar(require("./gitlab")); const markdown = new markdown_it_1.default('zero'); markdown.enable(['heading', 'lheading']); const repositoriesToSkipMdFetching = ['facebook/react-native']; async function getReleaseList(project, release) { logger_1.logger.trace('getReleaseList()'); const { apiBaseUrl, repository, type } = project; try { switch (type) { case 'gitea': return await gitea.getReleaseList(project, release); case 'gitlab': return await gitlab.getReleaseList(project, release); case 'github': return await github.getReleaseList(project, release); case 'bitbucket': return bitbucket.getReleaseList(project, release); case 'bitbucket-server': logger_1.logger.trace('Unsupported Bitbucket Server feature. Skipping release fetching.'); return []; default: logger_1.logger.warn({ apiBaseUrl, repository, type }, 'Invalid project type'); return []; } } catch (err) /* istanbul ignore next */ { if (err.statusCode === 404) { logger_1.logger.debug({ repository, type, apiBaseUrl }, 'getReleaseList 404'); } else { logger_1.logger.debug({ repository, type, apiBaseUrl, err }, 'getReleaseList error'); } } return []; } function getCachedReleaseList(project, release) { const { repository, apiBaseUrl } = project; // TODO: types (#22198) const cacheKey = `getReleaseList-${apiBaseUrl}-${repository}`; const cachedResult = memCache.get(cacheKey); // istanbul ignore if if (cachedResult !== undefined) { return cachedResult; } const promisedRes = getReleaseList(project, release); memCache.set(cacheKey, promisedRes); return promisedRes; } function massageBody(input, baseUrl) { let body = (0, string_1.coerceString)(input); // Convert line returns body = body.replace((0, regex_1.regEx)(/\r\n/g), '\n'); // semantic-release cleanup body = body.replace((0, regex_1.regEx)(/^<a name="[^"]*"><\/a>\n/), ''); body = body.replace((0, regex_1.regEx)(`^##? \\[[^\\]]*\\]\\(${baseUrl}[^/]*/[^/]*/compare/.*?\\n`, undefined, false), ''); // Clean-up unnecessary commits link body = `\n${body}\n`.replace((0, regex_1.regEx)(`\\n${baseUrl}[^/]+/[^/]+/compare/[^\\n]+(\\n|$)`), '\n'); // Reduce headings size body = body .replace((0, regex_1.regEx)(/\n\s*####? /g), '\n##### ') .replace((0, regex_1.regEx)(/\n\s*## /g), '\n#### ') .replace((0, regex_1.regEx)(/\n\s*# /g), '\n### '); // Trim whitespace 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 undefined; } return name; } async function getReleaseNotes(project, release, config) { const { packageName, depName, repository } = project; const { version, gitRef } = release; // TODO: types (#22198) logger_1.logger.trace(`getReleaseNotes(${repository}, ${version}, ${packageName}, ${depName})`); const releases = await getCachedReleaseList(project, release); logger_1.logger.trace({ releases }, 'Release list from getReleaseList'); let releaseNotes = null; let matchedRelease = getExactReleaseMatch(packageName, depName, version, releases); if (is_1.default.undefined(matchedRelease)) { // no exact match of a release then check other cases matchedRelease = releases.find((r) => r.tag === version || r.tag === `v${version}` || r.tag === gitRef || r.tag === `v${gitRef}`); } if (is_1.default.undefined(matchedRelease) && config.extractVersion) { const extractVersionRegEx = (0, regex_1.regEx)(config.extractVersion); matchedRelease = releases.find((r) => { const extractedVersion = extractVersionRegEx.exec(r.tag)?.groups ?.version; return version === extractedVersion; }); } releaseNotes = await releaseNotesResult(matchedRelease, project); logger_1.logger.trace({ releaseNotes }); return releaseNotes; } function getExactReleaseMatch(packageName, depName, version, releases) { const exactReleaseReg = (0, regex_1.regEx)(`(?:^|/)(?:${packageName}|${depName})[@_-]v?${version}`); const candidateReleases = releases.filter((r) => r.tag?.endsWith(version)); const matchedRelease = candidateReleases.find((r) => exactReleaseReg.test(r.tag)); return matchedRelease; } async function releaseNotesResult(releaseMatch, project) { if (!releaseMatch) { return null; } const { baseUrl, repository } = project; const releaseNotes = releaseMatch; if ((0, common_1.detectPlatform)(baseUrl) === 'gitlab') { releaseNotes.url = `${baseUrl}${repository}/tags/${releaseMatch.tag}`; } else { releaseNotes.url = releaseMatch.url ? releaseMatch.url : /* istanbul ignore next */ `${baseUrl}${repository}/releases/${releaseMatch.tag}`; } // set body for release notes 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 (0, markdown_1.linkify)(releaseNotes.body, { repository: `${baseUrl}${repository}`, }); } } catch (err) /* istanbul ignore next */ { logger_1.logger.warn({ err, baseUrl, repository }, 'Error linkifying'); } } else { return null; } return releaseNotes; } function sectionize(text, level) { const sections = []; const lines = text.split(regex_1.newlineRegex); const tokens = markdown.parse(text, undefined); tokens.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 'gitea': return await gitea.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory); case 'gitlab': return await gitlab.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory); case 'github': return await github.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory); case 'bitbucket': return await bitbucket.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory); case 'bitbucket-server': return await bitbucketServer.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory); default: logger_1.logger.warn({ apiBaseUrl, repository, type }, 'Invalid project type'); return null; } } catch (err) /* istanbul ignore next */ { if (err.statusCode === 404) { logger_1.logger.debug({ repository, type, apiBaseUrl }, 'Error 404 getting changelog md'); } else { logger_1.logger.debug({ err, repository, type, apiBaseUrl }, 'Error getting changelog md'); } } return null; } function getReleaseNotesMdFile(project) { const { sourceDirectory, repository, apiBaseUrl } = project; // TODO: types (#22198) const cacheKey = sourceDirectory ? `getReleaseNotesMdFile@v2-${repository}-${sourceDirectory}-${apiBaseUrl}` : `getReleaseNotesMdFile@v2-${repository}-${apiBaseUrl}`; const cachedResult = memCache.get(cacheKey); // istanbul ignore if if (cachedResult !== undefined) { return cachedResult; } const promisedRes = getReleaseNotesMdFileInner(project); memCache.set(cacheKey, promisedRes); return promisedRes; } async function getReleaseNotesMd(project, release) { const { baseUrl, repository } = project; const version = release.version; logger_1.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((0, regex_1.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 { // replace brackets and parenthesis with space const deParenthesizedSection = section.replace((0, regex_1.regEx)(/[[\]()]/g), ' '); const [heading] = deParenthesizedSection.split(regex_1.newlineRegex); const title = heading .replace((0, regex_1.regEx)(/^\s*#*\s*/), '') .split(' ') .filter(Boolean); const body = section.replace((0, regex_1.regEx)(/.*?\n(-{3,}\n)?/), '').trim(); const notesSourceUrl = getNotesSourceUrl(baseUrl, repository, project, changelogFile); const mdHeadingLink = title .filter((word) => !(0, url_1.isHttpUrl)(word)) .join('-') .replace((0, regex_1.regEx)(/[^A-Za-z0-9-]/g), ''); const url = `${notesSourceUrl}#${mdHeadingLink}`; // Look for version in title for (const word of title) { if (word.includes(version) && !(0, url_1.isHttpUrl)(word)) { logger_1.logger.trace({ body }, 'Found release notes for v' + version); return { body: await linkifyBody(project, body), url, notesSourceUrl, }; } } } catch (err) /* istanbul ignore next */ { logger_1.logger.warn({ file: changelogFile, err }, `Error parsing changelog file`); } } } logger_1.logger.trace({ repository }, `No level ${level} changelogs headings found`); } logger_1.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 = is_1.default.date(releaseDate) ? luxon_1.DateTime.fromJSDate(releaseDate) : luxon_1.DateTime.fromISO(releaseDate); const now = luxon_1.DateTime.local(); if (!dt.isValid || now.diff(dt, 'days').days < 7) { return 55; } if (now.diff(dt, 'months').months < 6) { return 1435; // 5 minutes shy of one day } return 14495; // 5 minutes shy of 10 days } async function addReleaseNotes(input, config) { if (!input?.versions || !input.project?.type) { logger_1.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 packageCache.get(cacheNamespace, cacheKey); releaseNotes ??= await getReleaseNotesMd(input.project, v); releaseNotes ??= await getReleaseNotes(input.project, v, config); // If there is no release notes, at least try to show the compare URL if (!releaseNotes && v.compare.url) { releaseNotes = { url: v.compare.url, notesSourceUrl: '' }; } const cacheMinutes = releaseNotesCacheMinutes(v.date); await packageCache.set(cacheNamespace, cacheKey, releaseNotes, cacheMinutes); output.versions.push({ ...v, releaseNotes: 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 (0, url_1.joinUrlParts)(baseUrl, 'projects', projectKey, 'repos', repositorySlug, 'browse', changelogFile, '?at=HEAD'); } return (0, url_1.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 (0, markdown_1.linkify)(body, { repository: `${baseUrl}${repository}`, }); } catch (err) /* istanbul ignore next */ { logger_1.logger.warn({ body, err }, 'linkify error'); } } return body; } //# sourceMappingURL=release-notes.js.map