Skip to content

Commit

Permalink
chore(ci): improved contributors & PRs sections generator; (axios#5453)
Browse files Browse the repository at this point in the history
  • Loading branch information
DigitalBrainJS authored Jan 9, 2023
1 parent 18772ed commit e2a1e28
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 139 deletions.
128 changes: 109 additions & 19 deletions bin/contributors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import util from "util";
import cp from "child_process";
import Handlebars from "handlebars";
import fs from "fs/promises";
import {colorize} from "./helpers/colorize.js";

const exec = util.promisify(cp.exec);

Expand All @@ -20,6 +21,8 @@ const getUserFromCommit = ((commitCache) => async (sha) => {
return commitCache[sha];
}

console.log(colorize()`fetch github commit info (${sha})`);

const {data} = await axios.get(`https://api.github.com/repos/axios/axios/commits/${sha}`);

return commitCache[sha] = {
Expand All @@ -32,18 +35,32 @@ const getUserFromCommit = ((commitCache) => async (sha) => {
}
})({});

const getIssueById = ((cache) => async (id) => {
if(cache[id] !== undefined) {
return cache[id];
}

try {
const {data} = await axios.get(`https://api.github.com/repos/axios/axios/issues/${id}`);

return cache[id] = data;
} catch (err) {
return null;
}
})({});

const getUserInfo = ((userCache) => async (userEntry) => {
const {email, commits} = userEntry;

if (userCache[email] !== undefined) {
return userCache[email];
}

console.log(`fetch github user info [${userEntry.name}]`);
console.log(colorize()`fetch github user info [${userEntry.name}]`);

return userCache[email] = {
...userEntry,
...await getUserFromCommit(commits[0])
...await getUserFromCommit(commits[0].hash)
}
})({});

Expand Down Expand Up @@ -76,16 +93,24 @@ const deduplicate = (authors) => {
return combined;
}

const getReleaseInfo = async (version, useGithub) => {
version = 'v' + version.replace(/^v/, '');
const getReleaseInfo = ((releaseCache) => async (tag) => {
if(releaseCache[tag] !== undefined) {
return releaseCache[tag];
}

const isUnreleasedTag = !tag;

const releases = JSON.parse((await exec(
const version = 'v' + tag.replace(/^v/, '');

const command = isUnreleasedTag ?
`npx auto-changelog --unreleased-only --stdout --commit-limit false --template json` :
`npx auto-changelog ${
version ? '--starting-version ' + version + ' --ending-version ' + version: ''
} --stdout --commit-limit false --template json`)).stdout
);
version ? '--starting-version ' + version + ' --ending-version ' + version : ''
} --stdout --commit-limit false --template json`;

const release = JSON.parse((await exec(command)).stdout)[0];

for(const release of releases) {
if(release) {
const authors = {};

const commits = [
Expand All @@ -94,15 +119,30 @@ const getReleaseInfo = async (version, useGithub) => {
...release.merges.map(fix => fix.commit)
].filter(Boolean);

for(const {hash, author, email, insertions, deletions} of commits) {
const commitMergeMap = {};

for(const merge of release.merges) {
commitMergeMap[merge.commit.hash] = merge.id;
}

for (const {hash, author, email, insertions, deletions} of commits) {
const entry = authors[email] = (authors[email] || {
name: author,
prs: [],
email,
commits: [],
insertions: 0, deletions: 0
});

entry.commits.push(hash);
entry.commits.push({hash});

let pr;

if((pr = commitMergeMap[hash])) {
entry.prs.push(pr);
}

console.log(colorize()`Found commit [${hash}]`);

entry.displayName = entry.name || author || entry.login;

Expand All @@ -111,22 +151,63 @@ const getReleaseInfo = async (version, useGithub) => {
entry.insertions += insertions;
entry.deletions += deletions;
entry.points = entry.insertions + entry.deletions;
entry.isBot = entry.type === "Bot"
}

for(const [email, author] of Object.entries(authors)) {
authors[email] = await getUserInfo(author);
for (const [email, author] of Object.entries(authors)) {
const entry = authors[email] = await getUserInfo(author);

entry.isBot = entry.type === "Bot";
}

release.authors = Object.values(deduplicate(authors)).sort((a, b) => b.points - a.points);
release.authors = Object.values(deduplicate(authors))
.sort((a, b) => b.points - a.points);

release.allCommits = commits;
}

return releases;
releaseCache[tag] = release;

return release;
})({});

const renderContributorsList = async (tag, template) => {
const release = await getReleaseInfo(tag);

const compile = Handlebars.compile(String(await fs.readFile(template)))

const content = compile(release);

return removeExtraLineBreaks(cleanTemplate(content));
}

const renderContributorsList = async (version, useGithub = false, template) => {
const release = (await getReleaseInfo(version, useGithub))[0];
const renderPRsList = async (tag, template, {comments_threshold= 5, awesome_threshold= 5, label = 'add_to_changelog'} = {}) => {
const release = await getReleaseInfo(tag);

const prs = {};

for(const merge of release.merges) {
const pr = await getIssueById(merge.id);

if (pr && pr.labels.find(({name})=> name === label)) {
const {reactions, body} = pr;
prs[pr.number] = pr;
pr.isHot = pr.comments > comments_threshold;
const points = reactions['+1'] +
reactions['hooray'] + reactions['rocket'] + reactions['heart'] + reactions['laugh'] - reactions['-1'];

pr.isAwesome = points > awesome_threshold;

let match;

pr.messages = [];

if (body && (match = /```+changelog(.+)?```/gms.exec(body)) && match[1]) {
pr.messages.push(match[1]);
}
}
}

release.prs = Object.values(prs);

const compile = Handlebars.compile(String(await fs.readFile(template)))

Expand All @@ -135,6 +216,15 @@ const renderContributorsList = async (version, useGithub = false, template) => {
return removeExtraLineBreaks(cleanTemplate(content));
}

const getTagRef = async (tag) => {
try {
return (await exec(`git show-ref --tags "refs/tags/${tag}"`)).stdout.split(' ')[0];
} catch(e) {
}
}

export {
renderContributorsList
renderContributorsList,
renderPRsList,
getTagRef
}
14 changes: 14 additions & 0 deletions bin/helpers/colorize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import chalk from 'chalk';

export const colorize = (...colors)=> {
if(!colors.length) {
colors = ['green', 'magenta', 'cyan', 'blue', 'yellow', 'red'];
}

const colorsCount = colors.length;

return (strings, ...values) => {
const {length} = values;
return strings.map((str, i) => i < length ? str + chalk[colors[i%colorsCount]].bold(values[i]) : str).join('');
}
}
49 changes: 32 additions & 17 deletions bin/injectContributorsList.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
import fs from 'fs/promises';
import path from 'path';
import {renderContributorsList} from './contributors.js';
import {renderContributorsList, getTagRef, renderPRsList} from './contributors.js';
import asyncReplace from 'string-replace-async';
import {fileURLToPath} from "url";
import {colorize} from "./helpers/colorize.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const injectContributors = async (infile, injector) => {
console.log(`Checking contributors sections in ${infile}`);
const CONTRIBUTORS_TEMPLATE = path.resolve(__dirname, '../templates/contributors.hbs');
const PRS_TEMPLATE = path.resolve(__dirname, '../templates/prs.hbs');

const injectSection = async (name, contributorsRE, injector, infile = '../CHANGELOG.md') => {
console.log(colorize()`Checking ${name} sections in ${infile}`);

infile = path.resolve(__dirname, infile);

const content = String(await fs.readFile(infile));
const headerRE = /^#+\s+\[([-_\d.\w]+)].+?$/mig;
const contributorsRE = /^\s*### Contributors/mi;

let tag;
let index = 0;
let isFirstTag = true;

const newContent = await asyncReplace(content, headerRE, async (match, nextTag, offset) => {
const releaseContent = content.slice(index, offset);

const hasContributorsSection = contributorsRE.test(releaseContent);
const hasSection = contributorsRE.test(releaseContent);

const currentTag = tag;

tag = nextTag;
index = offset + match.length;

if(currentTag) {
if (hasContributorsSection) {
console.log(`[${currentTag}]: ✓ OK`);
if (hasSection) {
console.log(colorize()`[${currentTag}]: ✓ OK`);
} else {
console.log(`[${currentTag}]: ❌ MISSED`);
console.log(`Generating contributors list...`);
const target = isFirstTag && (!await getTagRef(currentTag)) ? '' : currentTag;

console.log(colorize()`[${currentTag}]: ❌ MISSED` + (!target ? ' (UNRELEASED)' : ''));

isFirstTag = false;

const section = await injector(currentTag);
console.log(`Generating section...`);

console.log(`\nRENDERED CONTRIBUTORS LIST [${currentTag}]:`);
const section = await injector(target);

console.log(colorize()`\nRENDERED SECTION [${name}] for [${currentTag}]:`);
console.log('-------------BEGIN--------------\n');
console.log(section);
console.log('--------------END---------------\n');

return section + match;
return section + '\n' + match;
}
}

Expand All @@ -52,8 +61,14 @@ const injectContributors = async (infile, injector) => {
await fs.writeFile(infile, newContent);
}


await injectContributors(
'../CHANGELOG.md',
(tag) => renderContributorsList(tag, true, path.resolve(__dirname, '../templates/contributors.hbs')
));
await injectSection(
'PRs',
/^\s*### PRs/mi,
(tag) => !tag && renderPRsList(tag, PRS_TEMPLATE, {awesome_threshold: 5, comments_threshold: 7}),
);

await injectSection(
'contributors',
/^\s*### Contributors/mi,
(tag) => renderContributorsList(tag, CONTRIBUTORS_TEMPLATE)
);
Loading

0 comments on commit e2a1e28

Please sign in to comment.