@@ -175,11 +175,16 @@ This configuration is a `.json` file in the following format.
|
||||
"flags": "gu"
|
||||
},
|
||||
{
|
||||
"pattern": "(.) (.+)",
|
||||
"target": "$1",
|
||||
"on_property": "title"
|
||||
"pattern": "\\[Issue\\]",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
}
|
||||
],
|
||||
"duplicate_filter": {
|
||||
"pattern": "\\[ABC-....\\]",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
},
|
||||
"transformers": [
|
||||
{
|
||||
"pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)",
|
||||
@@ -290,17 +295,19 @@ Table of descriptions for the `configuration.json` options to configure the resu
|
||||
| categories | An array of `category` specifications, offering a flexible way to group changes into categories |
|
||||
| category.title | The display name of a category in the changelog |
|
||||
| category.labels | An array of labels, to match pull request labels against. If any PR label matches any category label, the pull request will show up under this category |
|
||||
| category.exhaustive | Will require all labels defined within this category to be present on the matching PR. |
|
||||
| ignore_labels | An array of labels, to match pull request labels against. If any PR label overlaps, the pull request will be ignored from the changelog. This takes precedence over category labels |
|
||||
| sort | The sort order of pull requests. [ASC, DESC] |
|
||||
| template | Specifies the global template to pick for creating the changelog. See [Template placeholders](#template-placeholders) for possible values |
|
||||
| pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values |
|
||||
| empty_template | Template to pick if no changes are detected. See [Template placeholders](#template-placeholders) for possible values |
|
||||
| label_extractor | An array of `transform` specifications, offering a flexible API to extract additinal labels from a PR (Default: `body`, Default in commit mode: `commit message`). |
|
||||
| label_extractor | An array of `Extractor` specifications, offering a flexible API to extract additinal labels from a PR (Default: `body`, Default in commit mode: `commit message`). |
|
||||
| label_extractor.pattern | A `regex` pattern, extracting values of the change message. |
|
||||
| label_extractor.target | The result pattern. The result text will be used as label. If empty, no label is created. (Unused for `match` method) |
|
||||
| label_extractor.on_property | The property to retrieve the text from. This is optional. Defaults to: `body`. Alternative values: `title`, `author`, `milestone`. |
|
||||
| label_extractor.method | The extraction method used. Defaults to: `replace`. Alternative value: `match`. The method specified references the JavaScript String method. |
|
||||
| label_extractor.flags | Defines the regex flags specified for the pattern. Default: `gu` |
|
||||
| duplicate_filter | Defines the `Extractor` to use for retrieving the identifier for a PR. In case of duplicates will keep the last matching pull request (depends on `sort`). See `label_extractor` for details on `Extractor` properties. |
|
||||
| transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified |
|
||||
| transformer.pattern | A `regex` pattern, extracting values of the change message. |
|
||||
| transformer.target | The result pattern, the regex groups will be filled into. Allows for full transformation of a pull request message. Including potentially specified texts |
|
||||
|
||||
@@ -54,9 +54,9 @@ test('should write result to file', () => {
|
||||
// should succeed
|
||||
expect(result).toBeDefined()
|
||||
|
||||
const readOutput = fs.readFileSync("test.md")
|
||||
const readOutput = fs.readFileSync('test.md')
|
||||
|
||||
fs.unlinkSync("test.md")
|
||||
fs.unlinkSync('test.md')
|
||||
|
||||
expect(readOutput.toString()).not.toBe('')
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ it('Should have empty changelog (tags)', async () => {
|
||||
toTag: 'v0.0.2',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -35,7 +35,7 @@ it('Should match generated changelog (tags)', async () => {
|
||||
toTag: 'v0.0.3',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -60,7 +60,7 @@ it('Should match generated changelog (refs)', async () => {
|
||||
toTag: 'fa3788c8c4b3373ef8424ce3eb008a5cd07cc5aa',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -93,7 +93,7 @@ it('Should match generated changelog and replace all occurrences (refs)', async
|
||||
toTag: 'fa3788c8c4b3373ef8424ce3eb008a5cd07cc5aa',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -128,7 +128,7 @@ it('Should match ordered ASC', async () => {
|
||||
toTag: 'v0.5.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -150,7 +150,7 @@ it('Should match ordered DESC', async () => {
|
||||
toTag: 'v0.5.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -172,7 +172,7 @@ it('Should ignore PRs not merged into develop branch', async () => {
|
||||
toTag: 'v1.4.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
@@ -192,7 +192,7 @@ it('Should ignore PRs not merged into main branch', async () => {
|
||||
toTag: 'v1.4.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
|
||||
@@ -45,7 +45,9 @@ it('Should match generated changelog (unspecified tags)', async () => {
|
||||
|
||||
const changeLog = await releaseNotesBuilder.build()
|
||||
console.log(changeLog)
|
||||
expect(changeLog).toStrictEqual(`## 🐛 Fixes\n\n- Stacktrace Data can be an array\n - PR: #39\n\n`)
|
||||
expect(changeLog).toStrictEqual(
|
||||
`## 🐛 Fixes\n\n- Stacktrace Data can be an array\n - PR: #39\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('Should use empty placeholder', async () => {
|
||||
@@ -168,7 +170,6 @@ it('Uncategorized category', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
it('Verify commit based changelog', async () => {
|
||||
const configuration = resolveConfiguration(
|
||||
'',
|
||||
@@ -194,7 +195,6 @@ it('Verify commit based changelog', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
it('Verify commit based changelog, with emoji categorisation', async () => {
|
||||
const configuration = resolveConfiguration(
|
||||
'',
|
||||
|
||||
+260
-111
@@ -1,191 +1,340 @@
|
||||
import {buildChangelog} from '../src/transform'
|
||||
import { PullRequestInfo } from '../src/pullRequests'
|
||||
import {PullRequestInfo} from '../src/pullRequests'
|
||||
import moment from 'moment'
|
||||
import { DefaultConfiguration } from '../src/configuration';
|
||||
import { DefaultConfiguration, Configuration } from '../src/configuration';
|
||||
|
||||
jest.setTimeout(180000)
|
||||
|
||||
let configuration = DefaultConfiguration
|
||||
configuration.categories = [
|
||||
const configuration = Object.assign({}, DefaultConfiguration)
|
||||
configuration.categories = [
|
||||
{
|
||||
"title": "## 🚀 Features",
|
||||
"labels": ["[Feature]"]
|
||||
title: '## 🚀 Features',
|
||||
labels: ['[Feature]']
|
||||
},
|
||||
{
|
||||
"title": "## 🐛 Fixes",
|
||||
"labels": ["[Bug]", "[Issue]"]
|
||||
title: '## 🐛 Fixes',
|
||||
labels: ['[Bug]', '[Issue]']
|
||||
},
|
||||
{
|
||||
"title": "## 🧪 Tests",
|
||||
"labels": ["[Test]"]
|
||||
title: '## 🧪 Tests',
|
||||
labels: ['[Test]']
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
let mergedPullRequests: PullRequestInfo[] = []
|
||||
mergedPullRequests.push({
|
||||
// list of PRs without labels assigned (extract from title)
|
||||
const mergedPullRequests: PullRequestInfo[] = []
|
||||
mergedPullRequests.push(
|
||||
{
|
||||
number: 1,
|
||||
title: "[Feature][AB-1234] - this is a PR 1 title message",
|
||||
htmlURL: "",
|
||||
baseBranch: "",
|
||||
title: '[Feature][AB-1234] - this is a PR 1 title message',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: "sha1",
|
||||
author: "Mike",
|
||||
repoName: "test-repo",
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: "",
|
||||
body: "no magic body for this matter",
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
}, {
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: "[Issue][AB-4321] - this is a PR 2 title message",
|
||||
htmlURL: "",
|
||||
baseBranch: "",
|
||||
title: '[Issue][AB-4321] - this is a PR 2 title message',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: "sha1",
|
||||
author: "Mike",
|
||||
repoName: "test-repo",
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: "",
|
||||
body: "no magic body for this matter",
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
}, {
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: "[Issue][Feature][AB-1234321] - this is a PR 3 title message",
|
||||
htmlURL: "",
|
||||
baseBranch: "",
|
||||
title: '[Issue][Feature][AB-1234321] - this is a PR 3 title message',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: "sha1",
|
||||
author: "Mike",
|
||||
repoName: "test-repo",
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: "",
|
||||
body: "no magic body for this matter",
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
}, {
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: "[AB-404] - not found label",
|
||||
htmlURL: "",
|
||||
baseBranch: "",
|
||||
title: '[AB-404] - not found label',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: "sha1",
|
||||
author: "Mike",
|
||||
repoName: "test-repo",
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: "",
|
||||
body: "no magic body for this matter",
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('Extract label from title, combined regex', async () => {
|
||||
configuration.label_extractor = [
|
||||
{
|
||||
"pattern": ".*(\\[Feature\\]|\\[Issue\\]).*",
|
||||
"target": "$1",
|
||||
"on_property": "title"
|
||||
pattern: '.*(\\[Feature\\]|\\[Issue\\]).*',
|
||||
target: '$1',
|
||||
on_property: 'title'
|
||||
}
|
||||
]
|
||||
|
||||
const resultChangelog = buildChangelog(
|
||||
mergedPullRequests,
|
||||
{
|
||||
owner: "mikepenz",
|
||||
repo: "test-repo",
|
||||
fromTag: "1.0.0",
|
||||
toTag: "2.0.0",
|
||||
const resultChangelog = buildChangelog(mergedPullRequests, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
}
|
||||
)
|
||||
configuration
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n\n`)
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('Extract label from title, split regex', async () => {
|
||||
configuration.label_extractor = [
|
||||
{
|
||||
"pattern": ".*(\\[Feature\\]).*",
|
||||
"target": "$1",
|
||||
"on_property": "title"
|
||||
pattern: '.*(\\[Feature\\]).*',
|
||||
target: '$1',
|
||||
on_property: 'title'
|
||||
},
|
||||
{
|
||||
"pattern": ".*(\\[Issue\\]).*",
|
||||
"target": "$1",
|
||||
"on_property": "title"
|
||||
pattern: '.*(\\[Issue\\]).*',
|
||||
target: '$1',
|
||||
on_property: 'title'
|
||||
}
|
||||
]
|
||||
|
||||
const resultChangelog = buildChangelog(
|
||||
mergedPullRequests,
|
||||
{
|
||||
owner: "mikepenz",
|
||||
repo: "test-repo",
|
||||
fromTag: "1.0.0",
|
||||
toTag: "2.0.0",
|
||||
const resultChangelog = buildChangelog(mergedPullRequests, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
}
|
||||
)
|
||||
configuration
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`)
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('Extract label from title, match', async () => {
|
||||
configuration.label_extractor = [
|
||||
{
|
||||
"pattern": "\\[Feature\\]",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
pattern: '\\[Feature\\]',
|
||||
on_property: 'title',
|
||||
method: 'match'
|
||||
},
|
||||
{
|
||||
"pattern": "\\[Issue\\]",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
pattern: '\\[Issue\\]',
|
||||
on_property: 'title',
|
||||
method: 'match'
|
||||
}
|
||||
]
|
||||
|
||||
const resultChangelog = buildChangelog(
|
||||
mergedPullRequests,
|
||||
{
|
||||
owner: "mikepenz",
|
||||
repo: "test-repo",
|
||||
fromTag: "1.0.0",
|
||||
toTag: "2.0.0",
|
||||
const resultChangelog = buildChangelog(mergedPullRequests, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
}
|
||||
)
|
||||
configuration
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`)
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('Extract label from title, match multiple', async () => {
|
||||
configuration.label_extractor = [
|
||||
{
|
||||
"pattern": "\\[Feature\\]|\\[Issue\\]",
|
||||
"on_property": "title",
|
||||
"method": "match"
|
||||
pattern: '\\[Feature\\]|\\[Issue\\]',
|
||||
on_property: 'title',
|
||||
method: 'match'
|
||||
}
|
||||
]
|
||||
|
||||
const resultChangelog = buildChangelog(
|
||||
mergedPullRequests,
|
||||
{
|
||||
owner: "mikepenz",
|
||||
repo: "test-repo",
|
||||
fromTag: "1.0.0",
|
||||
toTag: "2.0.0",
|
||||
const resultChangelog = buildChangelog(mergedPullRequests, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: configuration
|
||||
}
|
||||
)
|
||||
configuration
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`)
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
// test set of PRs with lables predefined
|
||||
const pullRequestsWithLabels: PullRequestInfo[] = []
|
||||
pullRequestsWithLabels.push(
|
||||
{
|
||||
number: 1,
|
||||
title: '[ABC-1234] - this is a PR 1 title message',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>().add('feature'),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: '[ABC-4321] - this is a PR 2 title message',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>().add('issue').add('fix'),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: '[ABC-1234] - this is a PR 3 title message',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment().add(1, 'days'),
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>().add('issue').add('feature').add('fix'),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: '[AB-404] - not found label',
|
||||
htmlURL: '',
|
||||
baseBranch: '',
|
||||
mergedAt: moment(),
|
||||
mergeCommitSha: 'sha1',
|
||||
author: 'Mike',
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>().add(''),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: []
|
||||
}
|
||||
)
|
||||
|
||||
it('Match multiple labels exhaustive for category', async () => {
|
||||
const customConfig = Object.assign({}, DefaultConfiguration)
|
||||
customConfig.categories = [
|
||||
{
|
||||
title: '## 🚀 Features and 🐛 Issues',
|
||||
labels: ['Feature', 'Issue'],
|
||||
exhaustive: true
|
||||
},
|
||||
{
|
||||
title: '## 🚀 Features',
|
||||
labels: ['Feature', 'Feature2'],
|
||||
exhaustive: true
|
||||
},
|
||||
{
|
||||
title: '## 🐛 Fixes',
|
||||
labels: ['Issue', 'Issue2'],
|
||||
exhaustive: true
|
||||
}
|
||||
]
|
||||
|
||||
const resultChangelog = buildChangelog(pullRequestsWithLabels, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: customConfig
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('Deduplicate duplicated PRs', async () => {
|
||||
const customConfig = Object.assign({}, DefaultConfiguration)
|
||||
customConfig.duplicate_filter = {
|
||||
pattern: '\\[ABC-....\\]',
|
||||
on_property: 'title',
|
||||
method: 'match'
|
||||
}
|
||||
|
||||
const resultChangelog = buildChangelog(pullRequestsWithLabels, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: customConfig
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('Deduplicate duplicated PRs DESC', async () => {
|
||||
const customConfig = Object.assign({}, DefaultConfiguration)
|
||||
customConfig.sort = "DESC"
|
||||
customConfig.duplicate_filter = {
|
||||
pattern: '\\[ABC-....\\]',
|
||||
on_property: 'title',
|
||||
method: 'match'
|
||||
}
|
||||
|
||||
const resultChangelog = buildChangelog(pullRequestsWithLabels, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: '1.0.0',
|
||||
toTag: '2.0.0',
|
||||
failOnError: false,
|
||||
commitMode: false,
|
||||
configuration: customConfig
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(
|
||||
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n\n`
|
||||
)
|
||||
})
|
||||
+105
-55
@@ -80,7 +80,7 @@ class Commits {
|
||||
sha: commit.sha || '',
|
||||
summary: commit.commit.message.split('\n')[0],
|
||||
message: commit.commit.message,
|
||||
date: moment_1.default((_a = commit.commit.committer) === null || _a === void 0 ? void 0 : _a.date),
|
||||
date: (0, moment_1.default)((_a = commit.commit.committer) === null || _a === void 0 ? void 0 : _a.date),
|
||||
author: ((_b = commit.commit.author) === null || _b === void 0 ? void 0 : _b.name) || '',
|
||||
prNumber: undefined
|
||||
});
|
||||
@@ -169,6 +169,7 @@ exports.DefaultConfiguration = {
|
||||
],
|
||||
ignore_labels: ['ignore'],
|
||||
label_extractor: [],
|
||||
duplicate_filter: undefined,
|
||||
transformers: [],
|
||||
tag_resolver: {
|
||||
// defines the logic on how to resolve the previous tag, only relevant if `fromTag` is not specified
|
||||
@@ -269,7 +270,7 @@ class GitCommandManager {
|
||||
}
|
||||
execGit(args, allowAllExitCodes = false, silent = false) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
utils_1.directoryExistsSync(this.workingDirectory, true);
|
||||
(0, utils_1.directoryExistsSync)(this.workingDirectory, true);
|
||||
const result = new GitOutput();
|
||||
const stdout = [];
|
||||
const options = {
|
||||
@@ -349,10 +350,10 @@ function run() {
|
||||
try {
|
||||
// read in path specification, resolve github workspace, and repo path
|
||||
const inputPath = core.getInput('path');
|
||||
const repositoryPath = utils_1.retrieveRepositoryPath(inputPath);
|
||||
const repositoryPath = (0, utils_1.retrieveRepositoryPath)(inputPath);
|
||||
// read in configuration file if possible
|
||||
const configurationFile = core.getInput('configuration');
|
||||
const configuration = utils_1.resolveConfiguration(repositoryPath, configurationFile);
|
||||
const configuration = (0, utils_1.resolveConfiguration)(repositoryPath, configurationFile);
|
||||
// read in repository inputs
|
||||
const token = core.getInput('token');
|
||||
const owner = core.getInput('owner') || github.context.repo.owner;
|
||||
@@ -370,10 +371,10 @@ function run() {
|
||||
const outputFile = core.getInput('outputFile');
|
||||
if (outputFile !== '') {
|
||||
core.debug(`Enabled writing the changelog to disk`);
|
||||
utils_1.writeOutput(repositoryPath, outputFile, result);
|
||||
(0, utils_1.writeOutput)(repositoryPath, outputFile, result);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
catch (error /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
});
|
||||
@@ -444,7 +445,7 @@ class PullRequests {
|
||||
});
|
||||
return mapPullRequest(data);
|
||||
}
|
||||
catch (e) {
|
||||
catch (e /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
core.warning(`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
@@ -471,7 +472,7 @@ class PullRequests {
|
||||
}
|
||||
const firstPR = prs[0];
|
||||
if (firstPR === undefined ||
|
||||
(firstPR.merged_at && fromDate.isAfter(moment_1.default(firstPR.merged_at))) ||
|
||||
(firstPR.merged_at && fromDate.isAfter((0, moment_1.default)(firstPR.merged_at))) ||
|
||||
mergedPRs.length >= maxPullRequests) {
|
||||
if (mergedPRs.length >= maxPullRequests) {
|
||||
core.warning(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests}`);
|
||||
@@ -526,7 +527,7 @@ const mapPullRequest = (pr) => {
|
||||
title: pr.title,
|
||||
htmlURL: pr.html_url,
|
||||
baseBranch: pr.base.ref,
|
||||
mergedAt: moment_1.default(pr.merged_at),
|
||||
mergedAt: (0, moment_1.default)(pr.merged_at),
|
||||
mergeCommitSha: pr.merge_commit_sha || '',
|
||||
author: ((_a = pr.user) === null || _a === void 0 ? void 0 : _a.login) || '',
|
||||
repoName: pr.base.repo.full_name,
|
||||
@@ -612,7 +613,7 @@ class ReleaseNotes {
|
||||
return null;
|
||||
}
|
||||
core.startGroup('📦 Build changelog');
|
||||
const resultChangelog = transform_1.buildChangelog(mergedPullRequests, this.options);
|
||||
const resultChangelog = (0, transform_1.buildChangelog)(mergedPullRequests, this.options);
|
||||
core.endGroup();
|
||||
return resultChangelog;
|
||||
});
|
||||
@@ -627,7 +628,7 @@ class ReleaseNotes {
|
||||
commits = yield commitsApi.getDiff(owner, repo, fromTag, toTag);
|
||||
}
|
||||
catch (error) {
|
||||
utils_1.failOrError(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError);
|
||||
(0, utils_1.failOrError)(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError);
|
||||
return [];
|
||||
}
|
||||
if (commits.length === 0) {
|
||||
@@ -659,7 +660,7 @@ class ReleaseNotes {
|
||||
const pullRequestsApi = new pullRequests_1.PullRequests(octokit);
|
||||
const pullRequests = yield pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate, configuration.max_pull_requests || configuration_1.DefaultConfiguration.max_pull_requests);
|
||||
core.info(`ℹ️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`);
|
||||
const prCommits = commits_1.filterCommits(commits, configuration.exclude_merge_branches ||
|
||||
const prCommits = (0, commits_1.filterCommits)(commits, configuration.exclude_merge_branches ||
|
||||
configuration_1.DefaultConfiguration.exclude_merge_branches);
|
||||
core.info(`ℹ️ Retrieved ${prCommits.length} release commits for ${owner}/${repo}`);
|
||||
// create array of commits for this release
|
||||
@@ -691,7 +692,7 @@ class ReleaseNotes {
|
||||
if (commits.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const prCommits = commits_1.filterCommits(commits, configuration.exclude_merge_branches ||
|
||||
const prCommits = (0, commits_1.filterCommits)(commits, configuration.exclude_merge_branches ||
|
||||
configuration_1.DefaultConfiguration.exclude_merge_branches);
|
||||
core.info(`ℹ️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`);
|
||||
return prCommits.map(function (commit) {
|
||||
@@ -778,7 +779,7 @@ class ReleaseNotesBuilder {
|
||||
var _a, _b;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (!this.owner) {
|
||||
utils_1.failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
|
||||
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
@@ -786,7 +787,7 @@ class ReleaseNotesBuilder {
|
||||
core.debug(`Resolved 'owner' as ${this.owner}`);
|
||||
}
|
||||
if (!this.repo) {
|
||||
utils_1.failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
|
||||
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
@@ -805,7 +806,7 @@ class ReleaseNotesBuilder {
|
||||
configuration_1.DefaultConfiguration.max_tags_to_fetch, this.configuration.tag_resolver || configuration_1.DefaultConfiguration.tag_resolver);
|
||||
const thisTag = (_a = tagRange.to) === null || _a === void 0 ? void 0 : _a.name;
|
||||
if (!thisTag) {
|
||||
utils_1.failOrError(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError);
|
||||
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError);
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
@@ -815,7 +816,7 @@ class ReleaseNotesBuilder {
|
||||
}
|
||||
const previousTag = (_b = tagRange.from) === null || _b === void 0 ? void 0 : _b.name;
|
||||
if (previousTag == null) {
|
||||
utils_1.failOrError(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError);
|
||||
(0, utils_1.failOrError)(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError);
|
||||
return null;
|
||||
}
|
||||
this.fromTag = previousTag;
|
||||
@@ -832,7 +833,7 @@ class ReleaseNotesBuilder {
|
||||
};
|
||||
const releaseNotes = new releaseNotes_1.ReleaseNotes(octokit, options);
|
||||
return ((yield releaseNotes.pull()) ||
|
||||
transform_1.fillAdditionalPlaceholders(this.configuration.empty_template ||
|
||||
(0, transform_1.fillAdditionalPlaceholders)(this.configuration.empty_template ||
|
||||
configuration_1.DefaultConfiguration.empty_template, options));
|
||||
});
|
||||
}
|
||||
@@ -953,7 +954,7 @@ class Tags {
|
||||
else {
|
||||
core.info(`ℹ️ Only one tag found for the given repository. Usually this is the case for the initial release.`);
|
||||
// if not specified try to retrieve tag from git
|
||||
const gitHelper = yield gitHelper_1.createCommandManager(repositoryPath);
|
||||
const gitHelper = yield (0, gitHelper_1.createCommandManager)(repositoryPath);
|
||||
const initialCommit = yield gitHelper.initialCommit();
|
||||
core.info(`🔖 Resolved initial commit (${initialCommit}) from 'git rev-list --max-parents=0 HEAD'`);
|
||||
return { name: initialCommit, commit: initialCommit };
|
||||
@@ -991,7 +992,7 @@ class Tags {
|
||||
}
|
||||
else {
|
||||
// if not specified try to retrieve tag from git
|
||||
const gitHelper = yield gitHelper_1.createCommandManager(repositoryPath);
|
||||
const gitHelper = yield (0, gitHelper_1.createCommandManager)(repositoryPath);
|
||||
const latestTag = yield gitHelper.latestTag();
|
||||
core.info(`🔖 Resolved current tag (${latestTag}) from 'git rev-list --tags --skip=0 --max-count=1'`);
|
||||
resultToTag = {
|
||||
@@ -1133,38 +1134,40 @@ function buildChangelog(prs, options) {
|
||||
const config = options.configuration;
|
||||
const sort = config.sort || configuration_1.DefaultConfiguration.sort;
|
||||
const sortAsc = sort.toUpperCase() === 'ASC';
|
||||
prs = pullRequests_1.sortPullRequests(prs, sortAsc);
|
||||
prs = (0, pullRequests_1.sortPullRequests)(prs, sortAsc);
|
||||
core.info(`ℹ️ Sorted all pull requests ascending: ${sort}`);
|
||||
// drop duplicate pull requests
|
||||
if (config.duplicate_filter !== undefined) {
|
||||
const extractor = validateTransformer(config.duplicate_filter);
|
||||
if (extractor != null) {
|
||||
core.info(`ℹ️ Remove duplicated pull requests using \`duplicate_filter\``);
|
||||
const deduplicatedMap = new Map();
|
||||
for (const pr of prs) {
|
||||
const extracted = extractValues(pr, extractor, 'dupliate_filter');
|
||||
if (extracted !== null && extracted.length > 0) {
|
||||
deduplicatedMap.set(extracted[0], pr);
|
||||
}
|
||||
else {
|
||||
core.debug(`ℹ️ PR (${pr.number}) did not resolve a ID using the \`duplicate_filter\``);
|
||||
}
|
||||
}
|
||||
const deduplicatedPRs = Array.from(deduplicatedMap.values());
|
||||
const removedElements = prs.length - deduplicatedPRs.length;
|
||||
core.info(`ℹ️ Removed ${removedElements} pull requests during deduplication`);
|
||||
prs = deduplicatedPRs;
|
||||
}
|
||||
else {
|
||||
core.warning(`⚠️ Configured \`duplicate_filter\` invalid.`);
|
||||
}
|
||||
}
|
||||
// extract additional labels from the commit message
|
||||
const labelExtractors = validateTransformers(config.label_extractor);
|
||||
for (const extractor of labelExtractors) {
|
||||
if (extractor.pattern != null) {
|
||||
for (const pr of prs) {
|
||||
let onValue;
|
||||
if (extractor.onProperty !== undefined) {
|
||||
let value = pr[extractor.onProperty];
|
||||
if (value === undefined) {
|
||||
core.warning(`⚠️ the provided property '${extractor.onProperty}' for \`label_extractor\` is not valid`);
|
||||
value = pr['body'];
|
||||
}
|
||||
onValue = value;
|
||||
}
|
||||
else {
|
||||
onValue = pr.body;
|
||||
}
|
||||
if (extractor.method === 'match') {
|
||||
const lables = onValue.match(extractor.pattern);
|
||||
if (lables !== null) {
|
||||
for (const label of lables) {
|
||||
pr.labels.add(label.toLocaleLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const label = onValue.replace(extractor.pattern, extractor.target);
|
||||
if (label !== '') {
|
||||
pr.labels.add(label.toLocaleLowerCase());
|
||||
}
|
||||
const extracted = extractValues(pr, extractor, 'label_extractor');
|
||||
if (extracted !== null) {
|
||||
for (const label of extracted) {
|
||||
pr.labels.add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1195,11 +1198,19 @@ function buildChangelog(prs, options) {
|
||||
}
|
||||
let matched = false;
|
||||
for (const [category, pullRequests] of categorized) {
|
||||
if (category.exhaustive === true) {
|
||||
if (haveEveryElements(category.labels.map(lbl => lbl.toLocaleLowerCase()), pr.labels)) {
|
||||
pullRequests.push(body);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (haveCommonElements(category.labels.map(lbl => lbl.toLocaleLowerCase()), pr.labels)) {
|
||||
pullRequests.push(body);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
// we allow to have pull requests included in an "uncategorized" category
|
||||
for (const [category, pullRequests] of categorized) {
|
||||
@@ -1264,6 +1275,9 @@ exports.fillAdditionalPlaceholders = fillAdditionalPlaceholders;
|
||||
function haveCommonElements(arr1, arr2) {
|
||||
return arr1.some(item => arr2.has(item));
|
||||
}
|
||||
function haveEveryElements(arr1, arr2) {
|
||||
return arr1.every(item => arr2.has(item));
|
||||
}
|
||||
function fillTemplate(pr, template) {
|
||||
var _a, _b, _c;
|
||||
let transformed = template;
|
||||
@@ -1295,7 +1309,18 @@ function validateTransformers(specifiedTransformers) {
|
||||
const transformers = specifiedTransformers || configuration_1.DefaultConfiguration.transformers;
|
||||
return transformers
|
||||
.map(transformer => {
|
||||
return validateTransformer(transformer);
|
||||
})
|
||||
.filter(transformer => (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) != null)
|
||||
.map(transformer => {
|
||||
return transformer;
|
||||
});
|
||||
}
|
||||
function validateTransformer(transformer) {
|
||||
var _a;
|
||||
if (transformer === undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
let onProperty = undefined;
|
||||
let method = undefined;
|
||||
@@ -1312,13 +1337,38 @@ function validateTransformers(specifiedTransformers) {
|
||||
}
|
||||
catch (e) {
|
||||
core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`);
|
||||
return {
|
||||
pattern: null,
|
||||
target: ''
|
||||
};
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(transformer => transformer.pattern != null);
|
||||
}
|
||||
function extractValues(pr, extractor, extractor_usecase) {
|
||||
if (extractor.pattern == null) {
|
||||
return null;
|
||||
}
|
||||
let onValue;
|
||||
if (extractor.onProperty !== undefined) {
|
||||
let value = pr[extractor.onProperty];
|
||||
if (value === undefined) {
|
||||
core.warning(`⚠️ the provided property '${extractor.onProperty}' for \`${extractor_usecase}\` is not valid`);
|
||||
value = pr['body'];
|
||||
}
|
||||
onValue = value;
|
||||
}
|
||||
else {
|
||||
onValue = pr.body;
|
||||
}
|
||||
if (extractor.method === 'match') {
|
||||
const lables = onValue.match(extractor.pattern);
|
||||
if (lables !== null) {
|
||||
return lables.map(label => label.toLocaleLowerCase());
|
||||
}
|
||||
}
|
||||
else {
|
||||
const label = onValue.replace(extractor.pattern, extractor.target);
|
||||
if (label !== '') {
|
||||
return [label.toLocaleLowerCase()];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1432,7 +1482,7 @@ function directoryExistsSync(inputPath, required) {
|
||||
try {
|
||||
stats = fs.statSync(inputPath);
|
||||
}
|
||||
catch (error) {
|
||||
catch (error /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (!required) {
|
||||
return false;
|
||||
@@ -1460,7 +1510,7 @@ function writeOutput(githubWorkspacePath, outputFile, changelog) {
|
||||
try {
|
||||
fs.writeFileSync(outputPath, changelog);
|
||||
}
|
||||
catch (error) {
|
||||
catch (error /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
core.warning(`⚠️ Could not write the file to disk - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Generated
+7
-7
@@ -31,7 +31,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "2.3.2",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "^4.3.5"
|
||||
"typescript": "^4.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
@@ -6616,9 +6616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
|
||||
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -12049,9 +12049,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
|
||||
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
|
||||
+1
-1
@@ -54,6 +54,6 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "2.3.2",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "^4.3.5"
|
||||
"typescript": "^4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,32 +3,34 @@ export interface Configuration {
|
||||
max_pull_requests: number
|
||||
max_back_track_time_days: number
|
||||
exclude_merge_branches: string[]
|
||||
sort: string
|
||||
sort: string // "ASC" or "DESC"
|
||||
template: string
|
||||
pr_template: string
|
||||
empty_template: string
|
||||
categories: Category[]
|
||||
ignore_labels: string[]
|
||||
label_extractor: Extractor[]
|
||||
duplicate_filter?: Extractor // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`)
|
||||
transformers: Transformer[]
|
||||
tag_resolver: TagResolver
|
||||
base_branches: string[]
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
title: string
|
||||
labels: string[]
|
||||
title: string // the title of this category
|
||||
labels: string[] // labels to associate PRs to this category
|
||||
exhaustive?: boolean // requires all labels to be present in the PR
|
||||
}
|
||||
|
||||
export interface Transformer {
|
||||
pattern: string
|
||||
target?: string
|
||||
pattern: string // the regex pattern to match
|
||||
target?: string // the target string to transform the source string using the regex to
|
||||
flags?: string // the regex flag to use for RegExp
|
||||
}
|
||||
|
||||
export interface Extractor extends Transformer {
|
||||
on_property?: 'title' | 'author' | 'milestone' | 'body' | undefined // retrieve the property to extract the value from
|
||||
method?: 'replace' | 'match' | undefined // the method to use to extract the value
|
||||
method?: 'replace' | 'match' | undefined // the method to use to extract the value, `match` will not use the `target` property
|
||||
}
|
||||
|
||||
export interface TagResolver {
|
||||
@@ -60,6 +62,7 @@ export const DefaultConfiguration: Configuration = {
|
||||
], // the categories to support for the ordering
|
||||
ignore_labels: ['ignore'], // list of lables being ignored from the changelog
|
||||
label_extractor: [], // extracts additional labels from the commit message given a regex
|
||||
duplicate_filter: undefined, // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`)
|
||||
transformers: [], // transformers to apply on the PR description according to the `pr_template`
|
||||
tag_resolver: {
|
||||
// defines the logic on how to resolve the previous tag, only relevant if `fromTag` is not specified
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ async function run(): Promise<void> {
|
||||
core.debug(`Enabled writing the changelog to disk`)
|
||||
writeOutput(repositoryPath, outputFile, result)
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
core.setFailed(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export class PullRequests {
|
||||
})
|
||||
|
||||
return mapPullRequest(data)
|
||||
} catch (e) {
|
||||
} catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
core.warning(
|
||||
`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`
|
||||
)
|
||||
|
||||
+99
-32
@@ -19,37 +19,42 @@ export function buildChangelog(
|
||||
prs = sortPullRequests(prs, sortAsc)
|
||||
core.info(`ℹ️ Sorted all pull requests ascending: ${sort}`)
|
||||
|
||||
// drop duplicate pull requests
|
||||
if (config.duplicate_filter !== undefined) {
|
||||
const extractor = validateTransformer(config.duplicate_filter)
|
||||
if (extractor != null) {
|
||||
core.info(`ℹ️ Remove duplicated pull requests using \`duplicate_filter\``)
|
||||
|
||||
const deduplicatedMap = new Map<string, PullRequestInfo>()
|
||||
for (const pr of prs) {
|
||||
const extracted = extractValues(pr, extractor, 'dupliate_filter')
|
||||
if (extracted !== null && extracted.length > 0) {
|
||||
deduplicatedMap.set(extracted[0], pr)
|
||||
} else {
|
||||
core.debug(
|
||||
`ℹ️ PR (${pr.number}) did not resolve a ID using the \`duplicate_filter\``
|
||||
)
|
||||
}
|
||||
}
|
||||
const deduplicatedPRs = Array.from(deduplicatedMap.values())
|
||||
const removedElements = prs.length - deduplicatedPRs.length
|
||||
core.info(
|
||||
`ℹ️ Removed ${removedElements} pull requests during deduplication`
|
||||
)
|
||||
prs = deduplicatedPRs
|
||||
} else {
|
||||
core.warning(`⚠️ Configured \`duplicate_filter\` invalid.`)
|
||||
}
|
||||
}
|
||||
|
||||
// extract additional labels from the commit message
|
||||
const labelExtractors = validateTransformers(config.label_extractor)
|
||||
for (const extractor of labelExtractors) {
|
||||
if (extractor.pattern != null) {
|
||||
for (const pr of prs) {
|
||||
let onValue
|
||||
if (extractor.onProperty !== undefined) {
|
||||
let value: string = pr[extractor.onProperty]
|
||||
if (value === undefined) {
|
||||
core.warning(
|
||||
`⚠️ the provided property '${extractor.onProperty}' for \`label_extractor\` is not valid`
|
||||
)
|
||||
value = pr['body']
|
||||
}
|
||||
onValue = value
|
||||
} else {
|
||||
onValue = pr.body
|
||||
}
|
||||
|
||||
if (extractor.method === 'match') {
|
||||
const lables = onValue.match(extractor.pattern)
|
||||
if (lables !== null) {
|
||||
for (const label of lables) {
|
||||
pr.labels.add(label.toLocaleLowerCase())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const label = onValue.replace(extractor.pattern, extractor.target)
|
||||
if (label !== '') {
|
||||
pr.labels.add(label.toLocaleLowerCase())
|
||||
}
|
||||
const extracted = extractValues(pr, extractor, 'label_extractor')
|
||||
if (extracted !== null) {
|
||||
for (const label of extracted) {
|
||||
pr.labels.add(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,6 +108,17 @@ export function buildChangelog(
|
||||
|
||||
let matched = false
|
||||
for (const [category, pullRequests] of categorized) {
|
||||
if (category.exhaustive === true) {
|
||||
if (
|
||||
haveEveryElements(
|
||||
category.labels.map(lbl => lbl.toLocaleLowerCase()),
|
||||
pr.labels
|
||||
)
|
||||
) {
|
||||
pullRequests.push(body)
|
||||
matched = true
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
haveCommonElements(
|
||||
category.labels.map(lbl => lbl.toLocaleLowerCase()),
|
||||
@@ -113,6 +129,7 @@ export function buildChangelog(
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
// we allow to have pull requests included in an "uncategorized" category
|
||||
@@ -213,6 +230,10 @@ function haveCommonElements(arr1: string[], arr2: Set<string>): Boolean {
|
||||
return arr1.some(item => arr2.has(item))
|
||||
}
|
||||
|
||||
function haveEveryElements(arr1: string[], arr2: Set<string>): Boolean {
|
||||
return arr1.every(item => arr2.has(item))
|
||||
}
|
||||
|
||||
function fillTemplate(pr: PullRequestInfo, template: string): string {
|
||||
let transformed = template
|
||||
transformed = transformed.replace(/\${{NUMBER}}/g, pr.number.toString())
|
||||
@@ -260,6 +281,20 @@ function validateTransformers(
|
||||
specifiedTransformers || DefaultConfiguration.transformers
|
||||
return transformers
|
||||
.map(transformer => {
|
||||
return validateTransformer(transformer)
|
||||
})
|
||||
.filter(transformer => transformer?.pattern != null)
|
||||
.map(transformer => {
|
||||
return transformer as RegexTransformer
|
||||
})
|
||||
}
|
||||
|
||||
function validateTransformer(
|
||||
transformer?: Transformer
|
||||
): RegexTransformer | null {
|
||||
if (transformer === undefined) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
let onProperty = undefined
|
||||
let method = undefined
|
||||
@@ -279,13 +314,45 @@ function validateTransformers(
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`)
|
||||
return {
|
||||
pattern: null,
|
||||
target: ''
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function extractValues(
|
||||
pr: PullRequestInfo,
|
||||
extractor: RegexTransformer,
|
||||
extractor_usecase: string
|
||||
): string[] | null {
|
||||
if (extractor.pattern == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let onValue
|
||||
if (extractor.onProperty !== undefined) {
|
||||
let value: string = pr[extractor.onProperty]
|
||||
if (value === undefined) {
|
||||
core.warning(
|
||||
`⚠️ the provided property '${extractor.onProperty}' for \`${extractor_usecase}\` is not valid`
|
||||
)
|
||||
value = pr['body']
|
||||
}
|
||||
onValue = value
|
||||
} else {
|
||||
onValue = pr.body
|
||||
}
|
||||
|
||||
if (extractor.method === 'match') {
|
||||
const lables = onValue.match(extractor.pattern)
|
||||
if (lables !== null) {
|
||||
return lables.map(label => label.toLocaleLowerCase())
|
||||
}
|
||||
} else {
|
||||
const label = onValue.replace(extractor.pattern, extractor.target)
|
||||
if (label !== '') {
|
||||
return [label.toLocaleLowerCase()]
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(transformer => transformer.pattern != null)
|
||||
return null
|
||||
}
|
||||
|
||||
interface RegexTransformer {
|
||||
|
||||
+2
-2
@@ -96,7 +96,7 @@ export function directoryExistsSync(
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(inputPath)
|
||||
} catch (error) {
|
||||
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (!required) {
|
||||
return false
|
||||
@@ -132,7 +132,7 @@ export function writeOutput(
|
||||
core.debug(`outputPath = '${outputPath}'`)
|
||||
try {
|
||||
fs.writeFileSync(outputPath, changelog)
|
||||
} catch (error) {
|
||||
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
||||
core.warning(`⚠️ Could not write the file to disk - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user