Skip to content

Release notes script - fix and improve #2605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
## Description
*Enter description to help the reviewer understand what's the change about...*
<!--
Enter description to help the reviewer understand what's the change about...*
-->

## Changelog
*Add a quick message for our users about this change (include Component name, relevant props and general purpose of the PR)*
<!--
Add a quick message for our users about this change (include Component name, relevant props and general purpose of the PR)*
-->

## Additional info
<!--
If applicable, add additional info such as link to the bug being fixed by this PR etc
-->
3 changes: 2 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ markdowns/
.babelrc
# typings/
# eslint-rules/
scripts/
scripts/*
!scripts/prReleaseNotesCommon.ts
demo-app.component.js
index.android.js
index.ios.js
Expand Down
139 changes: 5 additions & 134 deletions scripts/prReleaseNotes.js
Original file line number Diff line number Diff line change
@@ -1,136 +1,7 @@
const fs = require('fs');
const _ = require('lodash');
const childProcess = require('child_process');
const fetch = require('node-fetch');
const readline = require('readline');

const GITHUB_TOKEN = 'xxxx';
let LATEST_VERSION = '5.14.0';
let NEW_VERSION = '5.15.0';
let releaseNotes;

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(`What is the current version? `, currentVersion => {
rl.question('What is the next version for release? ', newVersion => {
LATEST_VERSION = currentVersion;
NEW_VERSION = newVersion;
rl.close();
});
});

rl.on('close', () => {
console.info(`Current latest version is v${LATEST_VERSION}`);
console.info(`Generating release notes out or PRs for v${NEW_VERSION}`);
run();
});

async function run() {
const latestReleaseDate = await fetchLatestReleaseDate(LATEST_VERSION);
const PRs = await fetchMergedPRs(latestReleaseDate);
generateNotes(PRs);
}

async function fetchLatestReleaseDate(version) {
const relesae = childProcess.execSync(`gh release view ${version}`).toString();
const releaseMetaData = relesae.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);
}

async function fetchMergedPRs(postMergedDate) {
const page = 1;
// process.stderr.write(`Loading page ${page}..`);
const url =
'https://api.github.com/repos/wix/react-native-ui-lib/pulls?' +
`state=closed&base=master&page=${page}&per_page=100`;
const headers = {Authorization: `token ${GITHUB_TOKEN}`};
const response = await fetch(url, {headers});
const PRs = await response.json();

const relevantPRs = _.flow(prs => _.filter(prs, pr => !!pr.merged_at && new Date(pr.merged_at) > postMergedDate),
prs => _.sortBy(prs, 'merged_at'),
prs =>
_.map(prs, pr => ({
state: pr.state,
merged_at: pr.merged_at,
html_url: pr.html_url,
branch: pr.head.ref,
number: pr.number,
title: pr.title,
info: parsePR(pr.body)
})))(PRs);

return relevantPRs;
}

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;
}

function generateNotes(PRs) {
const features = [],
fixes = [],
infra = [],
others = [];

PRs.forEach(pr => {
if (pr.branch.startsWith('feat/')) {
fixes.push(pr);
} else if (pr.branch.startsWith('fix/')) {
features.push(pr);
} else if (pr.branch.startsWith('infra/')) {
infra.push(pr);
} else {
others.push(pr);
}
});

// features
addTitle(':gift: Features');
features.forEach(addEntry);
// bug fixes
addTitle(':wrench: Fixes');
fixes.forEach(addEntry);
// migrations
addTitle(':bulb: Deprecations & Migrations');
infra.forEach(addEntry);
// Maintenance & Infra
addTitle(':gear: Maintenance & Infra');
// others
addTitle('OTHERS');
others.forEach(addEntry);

fs.writeFileSync(`${process.env.HOME}/Downloads/uilib-release-notes_${NEW_VERSION}.txt`, releaseNotes, {
encoding: 'utf8'
});

console.log(`\x1b[1m\x1b[32m✔\x1b[0m \x1b[32muilib-release-notes.txt was successfully written to ${process.env.HOME}/Downloads\x1b[0m \x1b[1m\x1b[32m✔\x1b[0m`);
}

function addTitle(title) {
releaseNotes += `\n\n${title}\n\n`;
}
const LATEST_VERSION = '7.3.0';
const NEW_VERSION = '7.4.0';
const PREFIX = 'uilib';
const REPO = 'wix/react-native-ui-lib';

function addEntry(pr) {
releaseNotes += `• ${pr.info.changelog || pr.title} (#${pr.number}) \n`;
}
require('./prReleaseNotesCommon').generateReleaseNotes(LATEST_VERSION, NEW_VERSION, GITHUB_TOKEN, PREFIX, REPO);
207 changes: 207 additions & 0 deletions scripts/prReleaseNotesCommon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
const fs = require('fs');
const _ = require('lodash');
const childProcess = require('child_process');
const fetch = require('node-fetch');
const readline = require('readline');

function fetchLatestReleaseDate(version) {
const relesae = childProcess.execSync(`gh release view ${version}`).toString();
const releaseMetaData = relesae.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, githubToken) {
console.log('Find all merged PRs since - ', postMergedDate);
const page = 1;
// process.stderr.write(`Loading page ${page}..`);
const url = `https://api.github.com/repos/${repo}/pulls?state=closed&base=master&page=${page}&per_page=100`;
const headers = {Authorization: `token ${githubToken}`};
const response = await fetch(url, {headers});
const PRs = await response.json();

if (PRs.message) {
console.log('\x1b[31m', 'Something went wrong', PRs.message);
return;
}

const relevantPRs = _.flow(prs => _.filter(prs, pr => !!pr.merged_at && new Date(pr.merged_at) > postMergedDate),
prs => _.sortBy(prs, 'merged_at'),
prs =>
_.map(prs, pr => ({
state: pr.state,
merged_at: pr.merged_at,
html_url: pr.html_url,
branch: pr.head.ref,
number: pr.number,
title: pr.title,
info: parsePR(pr.body)
})))(PRs);

return relevantPRs;
}

function isSilent(pr) {
if (!pr.info.changelog) {
return true;
} else {
const changelog = pr.info.changelog.toLowerCase();
if (changelog === 'none' || changelog === 'n/a') {
return true;
}
}

return false;
}

function getPRsByType(PRs) {
const silentPRs = [],
features = [],
web = [],
fixes = [],
infra = [],
others = [];

PRs.forEach(pr => {
if (isSilent(pr)) {
silentPRs.push(pr);
} else if (pr.branch.startsWith('feat/')) {
features.push(pr);
} else if (pr.branch.startsWith('web/')) {
web.push(pr);
} else if (pr.branch.startsWith('fix/')) {
fixes.push(pr);
} else if (pr.branch.startsWith('infra/')) {
infra.push(pr);
} else {
others.push(pr);
}
});

return {silentPRs, features, web, fixes, infra, others};
}

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;
}

async function _generateReleaseNotes(latestVersion, newVersion, githubToken, prefix, repo, header) {
const latestReleaseDate = fetchLatestReleaseDate(latestVersion);
const PRs = await fetchMergedPRs(latestReleaseDate, repo, githubToken);
if (!PRs) {
return;
}

const {silentPRs, features, web, fixes, infra, others} = getPRsByType(PRs);

let releaseNotes = header;

releaseNotes += getTitle(':rocket: What’s New?');

releaseNotes += getReleaseNotesForType(features, ':gift: Features');

releaseNotes += getReleaseNotesForType(web, ':spider_web: Web support');

releaseNotes += getReleaseNotesForType(fixes, ':wrench: Fixes');

releaseNotes += getReleaseNotesForType(infra, ':gear: Maintenance & Infra');

releaseNotes += getTitle(':bulb: Deprecations & Migrations');

releaseNotes += getReleaseNotesForType(others, 'OTHERS');

releaseNotes += getReleaseNotesForType(silentPRs,
'// Silent - these PRs did not have a changelog or were left out for some other reason, is it on purpose?');

fs.writeFileSync(`${process.env.HOME}/Downloads/${prefix}-release-notes_${newVersion}.txt`, releaseNotes, {
encoding: 'utf8'
});

console.log(`\x1b[1m\x1b[32m✔\x1b[0m \x1b[32m${prefix}-release-notes.txt was successfully written to ${process.env.HOME}/Downloads\x1b[0m \x1b[1m\x1b[32m✔\x1b[0m`);
}

async function generateReleaseNotes(latestVersion, newVersion, githubToken, prefix, repo, header = '') {
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', () => {
console.info(`Current latest version is v${latestVer}`);
console.info(`Generating release notes out or PRs for v${newVer}`);
_generateReleaseNotes(latestVer, newVer, githubToken, prefix, repo, header);
});
}

module.exports = {generateReleaseNotes};