renovate/lib/workers/pr/release-notes.js
Marco Massarotto 929231b46c Feat/allow ghe changelog ()
This PR follows up and fixes 

The previous PR didn't behave correctly as it was expecting Github Enterprise to ALWAYS been configured and that the github.com token was available in the `GITHUB_COM_TOKEN` env variable.
But for non GHE project `GITHUB_COM_TOKEN` is not defined and github.com token is available at the `GITHUB_TOKEN` env variable instead.

This updated PR fix this issue and avoid further problems by prioritising `github.com` over GHE.
Now the code is NOOP if no `GITHUB_ENDPOINT` is configured.

If it's configured, instead, now the codes assumes that, by DEFAULT, a dependency is hosted on `github.com` so it removes `GITHUB_ENDPOINT` and use `GITHUB_COM_TOKEN` as Github token immediately.
They are restored only if needed, when a dependency is hosted on the provided GithubEnterprise.
2018-05-19 07:44:56 +02:00

195 lines
5.9 KiB
JavaScript

const ghGot = require('../../platform/github/gh-got-wrapper');
const changelogFilenameRegex = require('changelog-filename-regex');
const MarkdownIt = require('markdown-it');
const markdown = new MarkdownIt('zero');
markdown.enable(['heading', 'lheading']);
module.exports = {
getReleaseList,
massageBody,
getReleaseNotesMd,
getReleaseNotes,
addReleaseNotes,
};
async function getReleaseList(repository) {
logger.debug('getReleaseList()');
try {
const res = await ghGot(`repos/${repository}/releases?per_page=100`);
return res.body.map(release => ({
url: release.html_url,
id: release.id,
tag: release.tag_name,
name: release.name,
body: release.body,
}));
} catch (err) /* istanbul ignore next */ {
logger.info({ repository, err }, 'getReleaseList error');
return [];
}
}
function massageBody(input, githubBaseURL) {
let body = input || '';
// Convert line returns
body = body.replace(/\r\n/g, '\n');
// semantic-release cleanup
body = body.replace(/^<a name="[^"]*"><\/a>\n/, '');
body = body.replace(
new RegExp(
`^##? \\[[^\\]]*\\]\\(${githubBaseURL}[^/]*\\/[^/]*\\/compare\\/.*?\\n`
),
''
);
// Clean-up unnecessary commits link
body = `\n${body}\n`.replace(
new RegExp(`\\n${githubBaseURL}[^/]+\\/[^/]+\\/compare\\/[^\\n]+(\\n|$)`),
'\n'
);
// Reduce headings size
body = body
.replace(/\n\s*####? /g, '\n##### ')
.replace(/\n\s*## /g, '\n#### ')
.replace(/\n\s*# /g, '\n### ');
// Trim whitespace
return body.trim();
}
async function getReleaseNotes(repository, version, githubBaseURL) {
logger.debug(`getReleaseNotes(${repository}, ${version})`);
const releaseList = await getReleaseList(repository);
let releaseNotes;
releaseList.forEach(release => {
if (release.tag === version || release.tag === `v${version}`) {
releaseNotes = release;
releaseNotes.url = `${githubBaseURL}${repository}/releases/${
release.tag
}`;
releaseNotes.body = massageBody(releaseNotes.body, githubBaseURL);
if (!releaseNotes.body.length) {
releaseNotes = undefined;
}
}
});
logger.trace({ releaseNotes });
return releaseNotes;
}
function sectionize(text, level) {
const sections = [];
const lines = text.split('\n');
const tokens = markdown.parse(text);
tokens.forEach(token => {
if (token.type === 'heading_open') {
const lev = +token.tag.substr(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 getReleaseNotesMd(repository, version, githubBaseURL) {
logger.trace(`getReleaseNotes(${repository}, ${version})`);
let changelogMd = '';
try {
const apiPrefix = `repos/${repository}/contents/`;
const filesRes = await ghGot(apiPrefix);
const files = filesRes.body
.map(f => f.name)
.filter(f => changelogFilenameRegex.test(f));
if (!files.length) {
logger.trace('no changelog file found');
return null;
}
const file = files[0];
/* istanbul ignore if */
if (files.length > 1) {
logger.info(`Multiple candidates for changelog file, using ${file}`);
}
const fileRes = await ghGot(`${apiPrefix}/${file}`);
changelogMd =
Buffer.from(fileRes.body.content, 'base64').toString() + '\n#\n##';
} catch (err) {
// Probably a 404?
return null;
}
changelogMd = changelogMd.replace(/\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.split('\n');
const title = heading
.replace(/^\s*#*\s*/, '')
.split(' ')
.filter(Boolean);
const body = section.replace(/.*?\n(-{3,}\n)?/, '').trim();
for (const word of title) {
if (word.includes(version)) {
logger.trace({ body }, 'Found release notes for v' + version);
let url = `${githubBaseURL}${repository}/blob/master/CHANGELOG.md#`;
url += title.join('-').replace(/[^A-Za-z0-9-]/g, '');
return {
body: massageBody(body, githubBaseURL),
url,
};
}
}
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error parsing CHANGELOG.md');
}
}
}
logger.trace({ repository }, `No level ${level} changelogs headings found`);
}
logger.trace({ repository, version }, 'No entry found in CHANGELOG.md');
return null;
}
async function addReleaseNotes(input) {
if (!(input && input.project && input.project.github && input.versions)) {
logger.debug('Missing project or versions');
return input;
}
const output = { ...input, versions: [] };
const repository = input.project.github.replace(/\.git$/, '');
for (const v of input.versions) {
let releaseNotes = await getReleaseNotesMd(
repository,
v.version,
input.project.githubBaseURL
);
if (!releaseNotes) {
logger.trace('No markdown release notes found for v' + v.version);
releaseNotes = await getReleaseNotes(
repository,
v.version,
input.project.githubBaseURL
);
}
// Small hack to force display of release notes when there is a compare url
if (!releaseNotes && v.compare.url) {
releaseNotes = { url: v.compare.url };
}
output.versions.push({
...v,
releaseNotes,
});
output.hasReleaseNotes = output.hasReleaseNotes || !!releaseNotes;
}
return output;
}