Skip to content

Commit f64e045

Browse files
committed
feat: include referenced issues from PRs
1 parent d4736e8 commit f64e045

8 files changed

+2033
-35
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,4 @@ typings/
103103
.tern-port
104104

105105
workflow
106+
test-output.md

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ jobs:
109109
* `toTag`: The tag up to which the changelog is to be determined (oldest) - **REQUIRED (unless using `tag`)**
110110
* `excludeTypes`: A comma-separated list of commit types you want to exclude from the changelog (e.g. `doc,chore,perf`) - **Optional** - Default: `build,docs,other,style`
111111
* `writeToFile`: Should CHANGELOG.md be updated with latest changelog - **Optional** - Default: `true`
112+
* `includeRefIssues`: Should the changelog include the issues referenced for each PR. - **Optional** - Default: `true`
112113
* `useGitmojis`: Should type headers be prepended with their related gitmoji - **Optional** - Default: `true`
113114
* `includeInvalidCommits`: Whether to include commits that don't respect the Conventional Commits format - **Optional** - Default: `false`
114115

action.yml

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ inputs:
2222
description: Should CHANGELOG.md be updated with latest changelog
2323
required: false
2424
default: 'true'
25+
includeRefIssues:
26+
description: Should the changelog include the issues referenced for each PR.
27+
required: false
28+
default: 'true'
2529
useGitmojis:
2630
description: Prepend type headers with their corresponding gitmoji
2731
required: false

dist/index.js

+66-13
Original file line numberDiff line numberDiff line change
@@ -27982,6 +27982,14 @@ module.exports = require("stream");
2798227982

2798327983
/***/ }),
2798427984

27985+
/***/ 8670:
27986+
/***/ ((module) => {
27987+
27988+
"use strict";
27989+
module.exports = require("timers/promises");
27990+
27991+
/***/ }),
27992+
2798527993
/***/ 4404:
2798627994
/***/ ((module) => {
2798727995

@@ -28080,10 +28088,11 @@ const core = __nccwpck_require__(2186)
2808028088
const _ = __nccwpck_require__(250)
2808128089
const cc = __nccwpck_require__(4523)
2808228090
const fs = (__nccwpck_require__(7147).promises)
28091+
const { setTimeout } = __nccwpck_require__(8670)
2808328092

2808428093
const types = [
2808528094
{ types: ['feat', 'feature'], header: 'New Features', icon: ':sparkles:' },
28086-
{ types: ['fix', 'bugfix'], header: 'Bug Fixes', icon: ':bug:' },
28095+
{ types: ['fix', 'bugfix'], header: 'Bug Fixes', icon: ':bug:', relIssuePrefix: 'fixes' },
2808728096
{ types: ['perf'], header: 'Performance Improvements', icon: ':zap:' },
2808828097
{ types: ['refactor'], header: 'Refactors', icon: ':recycle:' },
2808928098
{ types: ['test', 'tests'], header: 'Tests', icon: ':white_check_mark:' },
@@ -28099,31 +28108,37 @@ const rePrEnding = /\(#([0-9]+)\)$/
2809928108

2810028109
function buildSubject ({ writeToFile, subject, author, authorUrl, owner, repo }) {
2810128110
const hasPR = rePrEnding.test(subject)
28102-
let final = subject
28111+
const prs = []
28112+
let output = subject
2810328113
if (writeToFile) {
2810428114
if (hasPR) {
2810528115
const prMatch = subject.match(rePrEnding)
2810628116
const msgOnly = subject.slice(0, prMatch[0].length * -1)
28107-
final = msgOnly.replace(rePrId, (m, prId) => {
28117+
output = msgOnly.replace(rePrId, (m, prId) => {
28118+
prs.push(prId)
2810828119
return `[#${prId}](https://github.com/${owner}/${repo}/pull/${prId})`
2810928120
})
28110-
final += `*(PR [#${prMatch[1]}](https://github.com/${owner}/${repo}/pull/${prMatch[1]}) by [@${author}](${authorUrl}))*`
28121+
output += `*(PR [#${prMatch[1]}](https://github.com/${owner}/${repo}/pull/${prMatch[1]}) by [@${author}](${authorUrl}))*`
2811128122
} else {
28112-
final = subject.replace(rePrId, (m, prId) => {
28123+
output = subject.replace(rePrId, (m, prId) => {
2811328124
return `[#${prId}](https://github.com/${owner}/${repo}/pull/${prId})`
2811428125
})
28115-
final += ` *(commit by [@${author}](${authorUrl}))*`
28126+
output += ` *(commit by [@${author}](${authorUrl}))*`
2811628127
}
2811728128
} else {
2811828129
if (hasPR) {
28119-
final = subject.replace(rePrEnding, (m, prId) => {
28130+
output = subject.replace(rePrEnding, (m, prId) => {
28131+
prs.push(prId)
2812028132
return `*(PR #${prId} by @${author})*`
2812128133
})
2812228134
} else {
28123-
final = `${subject} *(commit by @${author})*`
28135+
output = `${subject} *(commit by @${author})*`
2812428136
}
2812528137
}
28126-
return final
28138+
return {
28139+
output,
28140+
prs
28141+
}
2812728142
}
2812828143

2812928144
async function main () {
@@ -28133,6 +28148,7 @@ async function main () {
2813328148
const toTag = core.getInput('toTag')
2813428149
const excludeTypes = (core.getInput('excludeTypes') || '').split(',').map(t => t.trim())
2813528150
const writeToFile = core.getBooleanInput('writeToFile')
28151+
const includeRefIssues = core.getBooleanInput('includeRefIssues')
2813628152
const useGitmojis = core.getBooleanInput('useGitmojis')
2813728153
const includeInvalidCommits = core.getBooleanInput('includeInvalidCommits')
2813828154
const gh = github.getOctokit(token)
@@ -28262,6 +28278,7 @@ async function main () {
2826228278
author: commit.author.login,
2826328279
authorUrl: commit.author.html_url
2826428280
})
28281+
core.info(`[OK] Commit ${commit.sha} with invalid type, falling back to other - ${commit.commit.message}`)
2826528282
} else {
2826628283
core.info(`[INVALID] Skipping commit ${commit.sha} as it doesn't follow conventional commit format.`)
2826728284
}
@@ -28299,8 +28316,8 @@ async function main () {
2829928316
owner,
2830028317
repo
2830128318
})
28302-
changesFile.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectFile}:\n\n${body}\n`)
28303-
changesVar.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectVar}:\n\n${body}\n`)
28319+
changesFile.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectFile.output}:\n\n${body}\n`)
28320+
changesVar.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectVar.output}:\n\n${body}\n`)
2830428321
}
2830528322
idx++
2830628323
}
@@ -28319,6 +28336,9 @@ async function main () {
2831928336
}
2832028337
changesFile.push(useGitmojis ? `### ${type.icon} ${type.header}` : `### ${type.header}`)
2832128338
changesVar.push(useGitmojis ? `### ${type.icon} ${type.header}` : `### ${type.header}`)
28339+
28340+
const relIssuePrefix = type.relIssuePrefix || 'addresses'
28341+
2832228342
for (const commit of matchingCommits) {
2832328343
const scope = commit.scope ? `**${commit.scope}**: ` : ''
2832428344
const subjectFile = buildSubject({
@@ -28337,8 +28357,41 @@ async function main () {
2833728357
owner,
2833828358
repo
2833928359
})
28340-
changesFile.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectFile}`)
28341-
changesVar.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectVar}`)
28360+
changesFile.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectFile.output}`)
28361+
changesVar.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectVar.output}`)
28362+
28363+
if (includeRefIssues && subjectVar.prs.length > 0) {
28364+
for (const prId of subjectVar.prs) {
28365+
core.info(`Querying related issues for PR ${prId}...`)
28366+
await setTimeout(500) // Make sure we don't go over GitHub API rate limits
28367+
const issuesRaw = await gh.graphql(`
28368+
query relIssues ($owner: String!, $repo: String!, $prId: Int!) {
28369+
repository (owner: $owner, name: $repo) {
28370+
pullRequest(number: $prId) {
28371+
closingIssuesReferences(first: 50) {
28372+
nodes {
28373+
number
28374+
author {
28375+
login
28376+
url
28377+
}
28378+
}
28379+
}
28380+
}
28381+
}
28382+
}
28383+
`, {
28384+
owner,
28385+
repo,
28386+
prId: parseInt(prId)
28387+
})
28388+
const relIssues = _.get(issuesRaw, 'repository.pullRequest.closingIssuesReferences.nodes')
28389+
for (const relIssue of relIssues) {
28390+
changesFile.push(` - :arrow_lower_right: *${relIssuePrefix} issue [#${relIssue.number}](${relIssue.url}) opened by [@${relIssue.author.login}](${relIssue.author.url})*`)
28391+
changesVar.push(` - :arrow_lower_right: *${relIssuePrefix} issue #${relIssue.number} opened by @${relIssue.author.login}*`)
28392+
}
28393+
}
28394+
}
2834228395
}
2834328396
idx++
2834428397
}

index.js

+58-13
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ const core = require('@actions/core')
33
const _ = require('lodash')
44
const cc = require('@conventional-commits/parser')
55
const fs = require('fs').promises
6+
const { setTimeout } = require('timers/promises')
67

78
const types = [
89
{ types: ['feat', 'feature'], header: 'New Features', icon: ':sparkles:' },
9-
{ types: ['fix', 'bugfix'], header: 'Bug Fixes', icon: ':bug:' },
10+
{ types: ['fix', 'bugfix'], header: 'Bug Fixes', icon: ':bug:', relIssuePrefix: 'fixes' },
1011
{ types: ['perf'], header: 'Performance Improvements', icon: ':zap:' },
1112
{ types: ['refactor'], header: 'Refactors', icon: ':recycle:' },
1213
{ types: ['test', 'tests'], header: 'Tests', icon: ':white_check_mark:' },
@@ -22,31 +23,37 @@ const rePrEnding = /\(#([0-9]+)\)$/
2223

2324
function buildSubject ({ writeToFile, subject, author, authorUrl, owner, repo }) {
2425
const hasPR = rePrEnding.test(subject)
25-
let final = subject
26+
const prs = []
27+
let output = subject
2628
if (writeToFile) {
2729
if (hasPR) {
2830
const prMatch = subject.match(rePrEnding)
2931
const msgOnly = subject.slice(0, prMatch[0].length * -1)
30-
final = msgOnly.replace(rePrId, (m, prId) => {
32+
output = msgOnly.replace(rePrId, (m, prId) => {
33+
prs.push(prId)
3134
return `[#${prId}](https://github.com/${owner}/${repo}/pull/${prId})`
3235
})
33-
final += `*(PR [#${prMatch[1]}](https://github.com/${owner}/${repo}/pull/${prMatch[1]}) by [@${author}](${authorUrl}))*`
36+
output += `*(PR [#${prMatch[1]}](https://github.com/${owner}/${repo}/pull/${prMatch[1]}) by [@${author}](${authorUrl}))*`
3437
} else {
35-
final = subject.replace(rePrId, (m, prId) => {
38+
output = subject.replace(rePrId, (m, prId) => {
3639
return `[#${prId}](https://github.com/${owner}/${repo}/pull/${prId})`
3740
})
38-
final += ` *(commit by [@${author}](${authorUrl}))*`
41+
output += ` *(commit by [@${author}](${authorUrl}))*`
3942
}
4043
} else {
4144
if (hasPR) {
42-
final = subject.replace(rePrEnding, (m, prId) => {
45+
output = subject.replace(rePrEnding, (m, prId) => {
46+
prs.push(prId)
4347
return `*(PR #${prId} by @${author})*`
4448
})
4549
} else {
46-
final = `${subject} *(commit by @${author})*`
50+
output = `${subject} *(commit by @${author})*`
4751
}
4852
}
49-
return final
53+
return {
54+
output,
55+
prs
56+
}
5057
}
5158

5259
async function main () {
@@ -56,6 +63,7 @@ async function main () {
5663
const toTag = core.getInput('toTag')
5764
const excludeTypes = (core.getInput('excludeTypes') || '').split(',').map(t => t.trim())
5865
const writeToFile = core.getBooleanInput('writeToFile')
66+
const includeRefIssues = core.getBooleanInput('includeRefIssues')
5967
const useGitmojis = core.getBooleanInput('useGitmojis')
6068
const includeInvalidCommits = core.getBooleanInput('includeInvalidCommits')
6169
const gh = github.getOctokit(token)
@@ -185,6 +193,7 @@ async function main () {
185193
author: commit.author.login,
186194
authorUrl: commit.author.html_url
187195
})
196+
core.info(`[OK] Commit ${commit.sha} with invalid type, falling back to other - ${commit.commit.message}`)
188197
} else {
189198
core.info(`[INVALID] Skipping commit ${commit.sha} as it doesn't follow conventional commit format.`)
190199
}
@@ -222,8 +231,8 @@ async function main () {
222231
owner,
223232
repo
224233
})
225-
changesFile.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectFile}:\n\n${body}\n`)
226-
changesVar.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectVar}:\n\n${body}\n`)
234+
changesFile.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectFile.output}:\n\n${body}\n`)
235+
changesVar.push(`- due to [\`${breakChange.sha.substring(0, 7)}\`](${breakChange.url}) - ${subjectVar.output}:\n\n${body}\n`)
227236
}
228237
idx++
229238
}
@@ -242,6 +251,9 @@ async function main () {
242251
}
243252
changesFile.push(useGitmojis ? `### ${type.icon} ${type.header}` : `### ${type.header}`)
244253
changesVar.push(useGitmojis ? `### ${type.icon} ${type.header}` : `### ${type.header}`)
254+
255+
const relIssuePrefix = type.relIssuePrefix || 'addresses'
256+
245257
for (const commit of matchingCommits) {
246258
const scope = commit.scope ? `**${commit.scope}**: ` : ''
247259
const subjectFile = buildSubject({
@@ -260,8 +272,41 @@ async function main () {
260272
owner,
261273
repo
262274
})
263-
changesFile.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectFile}`)
264-
changesVar.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectVar}`)
275+
changesFile.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectFile.output}`)
276+
changesVar.push(`- [\`${commit.sha.substring(0, 7)}\`](${commit.url}) - ${scope}${subjectVar.output}`)
277+
278+
if (includeRefIssues && subjectVar.prs.length > 0) {
279+
for (const prId of subjectVar.prs) {
280+
core.info(`Querying related issues for PR ${prId}...`)
281+
await setTimeout(500) // Make sure we don't go over GitHub API rate limits
282+
const issuesRaw = await gh.graphql(`
283+
query relIssues ($owner: String!, $repo: String!, $prId: Int!) {
284+
repository (owner: $owner, name: $repo) {
285+
pullRequest(number: $prId) {
286+
closingIssuesReferences(first: 50) {
287+
nodes {
288+
number
289+
author {
290+
login
291+
url
292+
}
293+
}
294+
}
295+
}
296+
}
297+
}
298+
`, {
299+
owner,
300+
repo,
301+
prId: parseInt(prId)
302+
})
303+
const relIssues = _.get(issuesRaw, 'repository.pullRequest.closingIssuesReferences.nodes')
304+
for (const relIssue of relIssues) {
305+
changesFile.push(` - :arrow_lower_right: *${relIssuePrefix} issue [#${relIssue.number}](${relIssue.url}) opened by [@${relIssue.author.login}](${relIssue.author.url})*`)
306+
changesVar.push(` - :arrow_lower_right: *${relIssuePrefix} issue #${relIssue.number} opened by @${relIssue.author.login}*`)
307+
}
308+
}
309+
}
265310
}
266311
idx++
267312
}

index.test.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const process = require('process')
2+
3+
// shows how the runner will run a javascript action with env / stdout protocol
4+
test('test run', () => {
5+
process.env['GITHUB_REPOSITORY'] = '__TEST_VALUE__'
6+
process.env['INPUT_TOKEN'] = '__TEST_VALUE__'
7+
process.env['INPUT_FROMTAG'] = '__TEST_VALUE__'
8+
process.env['INPUT_TOTAG'] = '__TEST_VALUE__'
9+
process.env['INPUT_WRITETOFILE'] = 'false'
10+
process.env['INPUT_INCLUDEREFISSUES'] = 'true'
11+
process.env['INPUT_USEGITMOJIS'] = 'true'
12+
process.env['INPUT_INCLUDEINVALIDCOMMITS'] = 'false'
13+
14+
require('./index.js')
15+
})

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "GitHub Action to generate changelog from conventional commits",
55
"main": "dist/index.js",
66
"scripts": {
7-
"build": "ncc build index.js -o dist"
7+
"build": "ncc build index.js -o dist",
8+
"test": "jest"
89
},
910
"repository": {
1011
"type": "git",
@@ -25,6 +26,7 @@
2526
"@actions/core": "1.10.0",
2627
"@actions/github": "5.1.1",
2728
"@conventional-commits/parser": "0.4.1",
29+
"jest": "29.3.1",
2830
"lodash": "4.17.21"
2931
},
3032
"devDependencies": {

0 commit comments

Comments
 (0)