renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
404 lines • 17.7 kB
JavaScript
;
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 forgejo = tslib_1.__importStar(require("./forgejo"));
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 'bitbucket':
return bitbucket.getReleaseList(project, release);
case 'bitbucket-server':
logger_1.logger.trace('Unsupported Bitbucket Server feature. Skipping release fetching.');
return [];
case 'forgejo':
return await forgejo.getReleaseList(project, release);
case 'gitea':
return await gitea.getReleaseList(project, release);
case 'github':
return await github.getReleaseList(project, release);
case 'gitlab':
return await gitlab.getReleaseList(project, release);
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 'bitbucket':
return await bitbucket.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory);
case 'bitbucket-server':
return await bitbucketServer.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory);
case 'forgejo':
return await forgejo.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory);
case 'gitea':
return await gitea.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory);
case 'github':
return await github.getReleaseNotesMd(repository, apiBaseUrl, sourceDirectory);
case 'gitlab':
return await gitlab.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, packageName } = 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,
};
}
}
// Look for version in body - useful for monorepos. First check for heading with "(yyyy-mm-dd)"
const releasesRegex = (0, regex_1.regEx)(/([0-9]{4}-[0-9]{2}-[0-9]{2})/);
if (packageName && heading.search(releasesRegex) !== -1) {
// Now check if any line contains both the package name and the version
const bodyLines = body.split('\n');
if (bodyLines.some((line) => line.includes(packageName) &&
line.includes(version) &&
!(0, url_1.isHttpUrl)(line))) {
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