Skip to content

Commit c086bc2

Browse files
authored
chore: add release notes tooling (#665)
1 parent 1b1ab01 commit c086bc2

File tree

4 files changed

+122
-0
lines changed

4 files changed

+122
-0
lines changed

tools/release-notes/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## Requirements
2+
```
3+
npm install -g git-release-notes
4+
```
5+
6+
## Usage
7+
```
8+
git release-notes -f release-notes.json PREVIOUS..CURRENT release-notes-md.ejs
9+
```
10+
Where PREVIOUS is the previous release tag, and CURRENT is the current release tag
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<%
2+
const typeGroups = {
3+
feats: { title: 'Features:', types: ['feat'] },
4+
fixes: { title: 'Fixes:', types: ['fix'] },
5+
etc: {
6+
title: 'Other changes (not related to library code):',
7+
types: ['docs','style','refactor','perf','test','build','ci','chore']
8+
},
9+
unknown: { title: 'Unknown:', types: ['?'] },
10+
}
11+
12+
const commitTypes = {
13+
feat: '', fix: '🐛', docs: '📚', style: '💎',
14+
refactor: '🔨', perf: '🚀', test: '🚨', build: '📦',
15+
ci: '⚙️', chore: '🔧', ['?']: '',
16+
}
17+
18+
for(const group of Object.values(typeGroups)){
19+
const groupCommits = commits.filter(c => group.types.includes(c.type));
20+
if (groupCommits.length < 1) continue;
21+
%>
22+
## <%=group.title%>
23+
<% for (const {issue, title, authorName, authorUser, scope, type} of groupCommits) { %>
24+
* <%=commitTypes[type]%>
25+
<%=issue ? ` [[#${issue}](https://github.com/icsharpcode/SharpZipLib/pull/${issue})]\n` : ''-%>
26+
<%=scope ? ` \`${scope}\`\n` : ''-%>
27+
__<%=title-%>__
28+
by <%=authorUser ? `[_${authorName}_](https://github.com/${authorUser})` : `_${authorName}_`%>
29+
<% } %>
30+
31+
<% } %>

tools/release-notes/release-notes.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const https = require('https')
2+
3+
const authorUsers = {}
4+
5+
/**
6+
* @param {string} email
7+
* @param {string} prId
8+
* @returns {Promise<string | null>} User login if found */
9+
const getAuthorUser = async (email, prId) => {
10+
const lookupUser = authorUsers[email];
11+
if (lookupUser) return lookupUser;
12+
13+
const match = /[0-9]+\+([^@]+)@users\.noreply\.github\.com/.exec(email);
14+
if (match) {
15+
return match[1];
16+
}
17+
18+
const pr = await new Promise((resolve, reject) => {
19+
console.warn(`Looking up GitHub user for PR #${prId} (${email})...`)
20+
https.get(`https://api.github.com/repos/icsharpcode/sharpziplib/pulls/${prId}`, {
21+
headers: {Accept: 'application/vnd.github.v3+json', 'User-Agent': 'release-notes-script/0.3.1'}
22+
}, (res) => {
23+
res.setEncoding('utf8');
24+
let chunks = '';
25+
res.on('data', (chunk) => chunks += chunk);
26+
res.on('end', () => resolve(JSON.parse(chunks)));
27+
res.on('error', reject);
28+
}).on('error', reject);
29+
}).catch(e => {
30+
console.error(`Could not get GitHub user (${email}): ${e}}`)
31+
return null;
32+
});
33+
34+
if (!pr) {
35+
console.error(`Could not get GitHub user (${email})}`)
36+
return null;
37+
} else {
38+
const user = pr.user.login;
39+
console.warn(`Resolved email ${email} to user ${user}`)
40+
authorUsers[email] = user;
41+
return user;
42+
}
43+
}
44+
45+
/**
46+
* @typedef {{issue?: string, sha1: string, authorEmail: string, title: string, type: string}} Commit
47+
* @param {{commits: Commit[], range: string, dateFnsFormat: ()=>any, debug: (...p[]) => void}} data
48+
* @param {(data: {commits: Commit[], extra: {[key: string]: any}}) => void} callback
49+
* */
50+
module.exports = (data, callback) => {
51+
// Migrates commits in the old format to conventional commit style, omitting any commits in neither format
52+
const normalizedCommits = data.commits.flatMap(c => {
53+
if (c.type) return [c]
54+
const match = /^(?:Merge )?(?:PR ?)?#(\d+):? (.*)/.exec(c.title)
55+
if (match != null) {
56+
const [, issue, title] = match
57+
return [{...c, title, issue, type: '?'}]
58+
} else {
59+
console.warn(`Skipping commit [${c.sha1.substr(0, 7)}] "${c.title}"!`);
60+
return [];
61+
}
62+
});
63+
64+
const commitAuthoredBy = email => commit => commit.authorEmail === email && commit.issue ? [commit.issue] : []
65+
const authorEmails = new Set(normalizedCommits.map(c => c.authorEmail));
66+
Promise.all(
67+
Array
68+
.from(authorEmails.values(), e => [e, normalizedCommits.flatMap(commitAuthoredBy(e))])
69+
.map(async ([email, prs]) => [email, await getAuthorUser(email, ...prs)])
70+
)
71+
.then(Object.fromEntries)
72+
.then(authorUsers => callback({
73+
commits: normalizedCommits.map(c => ({...c, authorUser: authorUsers[c.authorEmail]})),
74+
extra: {}
75+
}))
76+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title" : "^([a-z]+)(?:\\(([\\w\\$\\.]*)\\))?\\: (.*?)(?: \\(#(\\d+)\\))?$",
3+
"meaning": ["type", "scope", "title", "issue"],
4+
"script": "release-notes.js"
5+
}

0 commit comments

Comments
 (0)