react-native-ui-lib
Version:
[](https://stand-with-ukraine.pp.ua)
265 lines (222 loc) • 8.21 kB
JavaScript
const fs = require('fs');
const _ = require('lodash');
const chalk = require('chalk');
const childProcess = require('child_process');
const readline = require('readline');
const BRANCH_CATEGORIES = [
{name: 'features', branch: 'feat/', title: ':gift: Features'},
{name: 'web', branch: 'web/', title: ':spider_web: Web support'},
{name: 'fixes', branch: 'fix/', title: ':wrench: Fixes'},
{name: 'infra', branch: 'infra/', title: ':gear: Maintenance & Infra'}
];
const SILENT_PRS = ['none', 'n/a', 'na'];
function getBranchPrefixes() {
return BRANCH_CATEGORIES.map(category => category.branch);
}
function fetchLatestReleaseDate(tagPrefix, version) {
const release = childProcess.execSync(`gh release view ${tagPrefix}${version}`).toString();
const releaseMetaData = release.split('--')[0];
const createDate = _.flow(data => _.split(data, '\n'),
linesData => _.find(linesData, l => l.startsWith('created')),
createdData => _.split(createdData, '\t'),
_.last)(releaseMetaData);
return new Date(createDate);
}
function parsePR(prContent) {
const sections = prContent.split('##');
const PRInfo = {};
sections.forEach(section => {
const lines = section.trim().split('\r\n');
const title = lines.splice(0, 1)[0].toLowerCase();
const body = lines.join('\r\n');
if (title && body) {
PRInfo[title] = body;
}
});
return PRInfo;
}
async function fetchMergedPRs(postMergedDate, _repo, isPatchRelease) {
console.log('Find all merged PRs since - ', postMergedDate);
// process.stderr.write(`Loading page ${page}..`);
const str = childProcess.execSync('gh pr list --json headRefName,body,title,number,mergedAt,url,labels --limit 100 --state merged --search "base:master"',
{
encoding: 'utf8'
});
const PRs = JSON.parse(str);
if (PRs.message) {
console.log('\x1b[31m', 'Something went wrong', PRs.message);
return;
}
const filteringMessageStyle = chalk.bgYellow.black;
console.log(filteringMessageStyle(isPatchRelease
? 'Patch release - only hotfix PRs will be included.'
: 'Non-patch release - hotfix PRs will be excluded.'));
const relevantPRs = _.flow(prs => _.filter(prs, pr => !!pr.mergedAt && new Date(pr.mergedAt) > postMergedDate),
prs => _.filter(prs, pr => {
const isHotfix = pr.labels.some(label => label.name === 'hotfix');
if (isHotfix && !isPatchRelease) {
console.log(filteringMessageStyle(`PR ${pr.number} is a hotfix and was excluded from the release notes.`));
}
return isPatchRelease ? isHotfix : !isHotfix;
}),
prs => _.sortBy(prs, 'mergedAt'),
prs =>
_.map(prs, pr => {
try {
return {
mergedAt: pr.mergedAt,
url: pr.url,
branch: pr.headRefName,
number: pr.number,
title: pr.title,
info: parsePR(pr.body)
};
} catch {
// eslint-disable-next-line no-restricted-syntax
console.error('Failed parsing PR: ', pr.url);
return null;
}
}),
prs => _.compact(prs))(PRs);
return relevantPRs;
}
function isSilent(pr) {
if (!pr.info.changelog) {
return true;
} else {
const changelog = pr.info.changelog.toLowerCase();
if (SILENT_PRS.includes(changelog)) {
return true;
}
}
return false;
}
function getPRsByType(PRs, categories) {
const categorizedPRs = [];
categories.forEach(category => {
categorizedPRs.push({name: category.name, PRs: [], title: category.title});
});
PRs.forEach(pr => {
const category = categories.find(category => {
return pr.branch.toLowerCase().startsWith(category.branch);
});
if (isSilent(pr)) {
const silentCategory = categorizedPRs.find(cat => cat.name === 'silent');
silentCategory.PRs.push(pr);
} else if (category) {
const foundCategory = categorizedPRs.find(cat => cat.name === category.name);
foundCategory.PRs.push(pr);
} else {
const otherCategory = categorizedPRs.find(cat => cat.name === 'others');
otherCategory.PRs.push(pr);
}
});
return categorizedPRs;
}
function getLine(log, requester, prNumber) {
return `• ${log}${requester}${prNumber} \n`;
}
function getAdditionalInfo(pr) {
// TODO: Remove `jira issue` once fully migrated (has to remain for backwards compatibility)
let additionalInfo = pr.info['jira issue'] || pr.info['Additional info'];
if (additionalInfo === undefined || additionalInfo.toLowerCase() === 'none' || additionalInfo.includes('???')) {
additionalInfo = '';
} else {
additionalInfo = ` (${additionalInfo})`;
}
return additionalInfo;
}
function getEntry(pr) {
let releaseNotes = '';
const log = pr.info.changelog || pr.title;
const additionalInfo = getAdditionalInfo(pr);
const prNumber = ` (#${pr.number})`;
if (log.includes('\r\n')) {
log.split('\r\n').forEach(l => {
releaseNotes += getLine(l, additionalInfo, prNumber);
});
} else {
releaseNotes = getLine(log, additionalInfo, prNumber);
}
return releaseNotes;
}
function getTitle(title) {
return `\n\n${title}\n\n`;
}
function getReleaseNotesForType(PRs, title) {
let releaseNotes = getTitle(title);
PRs.forEach(pr => {
releaseNotes += getEntry(pr);
});
return releaseNotes;
}
function generateReleaseNotesFromPRs(PRs, categories, header) {
if (!PRs) {
return;
}
const prCategories = [
...BRANCH_CATEGORIES,
...categories,
{name: 'others', branch: '', title: 'OTHERS'},
{
name: 'silent',
branch: '',
title: '// Silent - these PRs did not have a changelog or were left out for some other reason, is it on purpose?'
}
];
const categorizedPRs = getPRsByType(PRs, prCategories);
let releaseNotes = header;
releaseNotes += getTitle(':rocket: What’s New?');
categorizedPRs.forEach(({PRs, title}) => {
if (PRs.length > 0) {
releaseNotes += getReleaseNotesForType(PRs, title);
}
});
releaseNotes += getTitle(':bulb: Deprecations & Migrations');
return releaseNotes;
}
// eslint-disable-next-line max-len, max-params
async function _generateReleaseNotes(latestVersion, newVersion, fileNamePrefix, repo, header, tagPrefix, categories, isPatchRelease) {
const latestReleaseDate = fetchLatestReleaseDate(tagPrefix, latestVersion);
const PRs = await fetchMergedPRs(latestReleaseDate, repo, isPatchRelease);
const releaseNotes = generateReleaseNotesFromPRs(PRs, categories, header);
fs.writeFileSync(`${process.env.HOME}/Downloads/${fileNamePrefix}-release-notes_${newVersion}.txt`, releaseNotes, {
encoding: 'utf8'
});
// eslint-disable-next-line max-len
console.log(`\x1b[1m\x1b[32m✔\x1b[0m \x1b[32m${fileNamePrefix}-release-notes.txt was successfully written to ${process.env.HOME}/Downloads\x1b[0m \x1b[1m\x1b[32m✔\x1b[0m`);
}
function isPatchRelease(lastVersion, newVersion) {
const [lastMajor, lastMinor, lastPatch] = lastVersion.split('.').map(Number);
const [newMajor, newMinor, newPatch] = newVersion.split('.').map(Number);
return lastMajor === newMajor && lastMinor === newMinor && newPatch - lastPatch > 0;
}
// eslint-disable-next-line max-params
async function generateReleaseNotes(latestVersion,
newVersion,
fileNamePrefix,
repo,
getHeader = () => '',
tagPrefix = '',
categories = []) {
let latestVer, newVer;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(`What is the current version? `, currentV => {
rl.question('What is the next version for release? ', newV => {
latestVer = currentV || latestVersion;
newVer = newV || newVersion;
rl.close();
});
});
rl.on('close', () => {
const header = getHeader(newVer);
console.info(`Current latest version is v${latestVer}`);
console.info(`Generating release notes out or PRs for v${newVer}`);
// eslint-disable-next-line max-len
_generateReleaseNotes(latestVer, newVer, fileNamePrefix, repo, header, tagPrefix, categories, isPatchRelease(latestVer, newVer));
});
}
module.exports = {generateReleaseNotes, generateReleaseNotesFromPRs, parsePR, getBranchPrefixes};