2018-02-02 11:37:16 +00:00
|
|
|
const ghGot = require('../../platform/github/gh-got-wrapper');
|
2018-04-01 19:41:26 +00:00
|
|
|
const changelogFilenameRegex = require('changelog-filename-regex');
|
2018-04-01 13:51:20 +00:00
|
|
|
const MarkdownIt = require('markdown-it');
|
|
|
|
|
|
|
|
const markdown = new MarkdownIt('zero');
|
|
|
|
markdown.enable(['heading', 'lheading']);
|
2018-02-02 11:37:16 +00:00
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
getReleaseList,
|
|
|
|
massageBody,
|
2018-02-06 17:53:36 +00:00
|
|
|
getReleaseNotesMd,
|
2018-02-02 11:37:16 +00:00
|
|
|
getReleaseNotes,
|
|
|
|
addReleaseNotes,
|
|
|
|
};
|
|
|
|
|
|
|
|
async function getReleaseList(repository) {
|
|
|
|
logger.debug('getReleaseList()');
|
|
|
|
try {
|
2018-03-20 08:24:22 +00:00
|
|
|
const res = await ghGot(
|
|
|
|
`https://api.github.com/repos/${repository}/releases?per_page=100`
|
|
|
|
);
|
2018-02-02 11:37:16 +00:00
|
|
|
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 */ {
|
2018-02-09 07:22:51 +00:00
|
|
|
logger.info({ repository, err }, 'getReleaseList error');
|
2018-02-02 11:37:16 +00:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-02 12:08:00 +00:00
|
|
|
function massageBody(input) {
|
|
|
|
let body = input || '';
|
2018-02-02 11:37:16 +00:00
|
|
|
// Convert line returns
|
2018-02-02 12:08:00 +00:00
|
|
|
body = body.replace(/\r\n/g, '\n');
|
2018-02-02 11:37:16 +00:00
|
|
|
// semantic-release cleanup
|
|
|
|
body = body.replace(/^<a name="[^"]*"><\/a>\n/, '');
|
|
|
|
body = body.replace(
|
|
|
|
/^##? \[[^\]]*\]\(https:\/\/github.com\/[^/]*\/[^/]*\/compare\/.*?\n/,
|
|
|
|
''
|
|
|
|
);
|
|
|
|
// Clean-up unnecessary commits link
|
|
|
|
body = `\n${body}\n`.replace(
|
|
|
|
/\nhttps:\/\/github.com\/[^/]+\/[^/]+\/compare\/[^\n]+(\n|$)/,
|
|
|
|
'\n'
|
|
|
|
);
|
|
|
|
// Reduce headings size
|
|
|
|
body = body
|
2018-02-07 09:59:37 +00:00
|
|
|
.replace(/\n\s*####? /g, '\n##### ')
|
|
|
|
.replace(/\n\s*## /g, '\n#### ')
|
|
|
|
.replace(/\n\s*# /g, '\n### ');
|
2018-02-02 11:37:16 +00:00
|
|
|
// Trim whitespace
|
|
|
|
return body.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getReleaseNotes(repository, version) {
|
|
|
|
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;
|
2018-02-07 10:00:47 +00:00
|
|
|
releaseNotes.url = `https://github.com/${repository}/releases/${
|
|
|
|
release.tag
|
|
|
|
}`;
|
2018-02-02 11:37:16 +00:00
|
|
|
releaseNotes.body = massageBody(releaseNotes.body);
|
|
|
|
if (!releaseNotes.body.length) {
|
|
|
|
releaseNotes = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2018-03-27 19:57:02 +00:00
|
|
|
logger.trace({ releaseNotes });
|
2018-02-02 11:37:16 +00:00
|
|
|
return releaseNotes;
|
|
|
|
}
|
|
|
|
|
2018-04-01 13:51:20 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-02-06 17:53:36 +00:00
|
|
|
async function getReleaseNotesMd(repository, version) {
|
|
|
|
logger.debug(`getReleaseNotes(${repository}, ${version})`);
|
2018-04-01 19:41:26 +00:00
|
|
|
let changelogMd = '';
|
2018-02-06 17:53:36 +00:00
|
|
|
try {
|
2018-04-01 19:41:26 +00:00
|
|
|
const apiPrefix = `https://api.github.com/repos/${repository}/contents/`;
|
|
|
|
const filesRes = await ghGot(apiPrefix);
|
|
|
|
const files = filesRes.body
|
|
|
|
.map(f => f.name)
|
|
|
|
.filter(f => changelogFilenameRegex.test(f));
|
|
|
|
if (!files.length) {
|
2018-04-18 19:19:00 +00:00
|
|
|
logger.trace('no changelog file found');
|
2018-04-01 19:41:26 +00:00
|
|
|
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}`);
|
2018-02-06 17:53:36 +00:00
|
|
|
changelogMd =
|
2018-04-01 19:41:26 +00:00
|
|
|
Buffer.from(fileRes.body.content, 'base64').toString() + '\n#\n##';
|
2018-02-06 17:53:36 +00:00
|
|
|
} catch (err) {
|
2018-04-01 19:41:26 +00:00
|
|
|
// Probably a 404?
|
2018-02-06 17:53:36 +00:00
|
|
|
return null;
|
|
|
|
}
|
2018-04-01 19:41:26 +00:00
|
|
|
|
2018-02-06 17:53:36 +00:00
|
|
|
changelogMd = changelogMd.replace(/\n\s*<a name="[^"]*">.*?<\/a>\n/g, '\n');
|
2018-04-01 13:51:20 +00:00
|
|
|
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 = `https://github.com/${repository}/blob/master/CHANGELOG.md#`;
|
|
|
|
url += title.join('-').replace(/[^A-Za-z0-9-]/g, '');
|
|
|
|
return {
|
|
|
|
body: massageBody(body),
|
|
|
|
url,
|
|
|
|
};
|
|
|
|
}
|
2018-02-06 17:53:36 +00:00
|
|
|
}
|
2018-04-01 13:51:20 +00:00
|
|
|
} catch (err) /* istanbul ignore next */ {
|
|
|
|
logger.warn({ err }, 'Error parsing CHANGELOG.md');
|
2018-02-06 17:53:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-01 13:51:20 +00:00
|
|
|
logger.debug({ repository }, `No level ${level} changelogs headings found`);
|
2018-02-06 17:53:36 +00:00
|
|
|
}
|
2018-02-07 08:44:44 +00:00
|
|
|
logger.debug({ repository, version }, 'No entry found in CHANGELOG.md');
|
2018-02-06 17:53:36 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-02-02 11:37:16 +00:00
|
|
|
async function addReleaseNotes(input) {
|
2018-02-08 11:10:36 +00:00
|
|
|
if (!(input && input.project && input.project.github && input.versions)) {
|
2018-02-02 11:37:16 +00:00
|
|
|
logger.debug('Missing project or versions');
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
const output = { ...input, versions: [] };
|
|
|
|
for (const v of input.versions) {
|
2018-02-06 17:53:36 +00:00
|
|
|
let releaseNotes = await getReleaseNotesMd(input.project.github, v.version);
|
|
|
|
if (!releaseNotes) {
|
2018-04-18 19:19:00 +00:00
|
|
|
logger.trace('No markdown release notes found for v' + v.version);
|
2018-02-06 17:53:36 +00:00
|
|
|
releaseNotes = await getReleaseNotes(input.project.github, v.version);
|
|
|
|
}
|
2018-05-01 09:55:40 +00:00
|
|
|
// Small hack to force display of release notes when there is a compare url
|
|
|
|
if (!releaseNotes && v.compare.url) {
|
|
|
|
releaseNotes = { url: v.compare.url };
|
|
|
|
}
|
2018-02-06 17:53:36 +00:00
|
|
|
output.versions.push({
|
|
|
|
...v,
|
|
|
|
releaseNotes,
|
|
|
|
});
|
|
|
|
output.hasReleaseNotes = output.hasReleaseNotes || !!releaseNotes;
|
2018-02-02 11:37:16 +00:00
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|