Merge pull request #52 from mikepenz/develop

dev -> main
This commit is contained in:
Mike Penz
2020-10-19 20:36:19 +02:00
committed by GitHub
16 changed files with 2919 additions and 445 deletions
+18 -9
View File
@@ -61,7 +61,9 @@ Specify the action as part of your GitHub actions workflow:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
By default the action will try to automatically retrieve the `tag` from the current commit and automatically resolve the `tag` before. Read more about this here. By default the action will try to automatically retrieve the `tag` from the current commit and automatically resolve the `tag` before. Automatic previous tag resolving is done using `semver`.
If you require a different versioning scheme please open an issue [issue](https://github.com/mikepenz/release-changelog-builder-action/issues). Alternative you can always specifically supply the `fromTag` via the [configuration](#advanced-workflow-specification).
### Action outputs ### Action outputs
@@ -213,7 +215,7 @@ For advanced use cases additional settings can be provided to the action
### PR Template placeholders ### PR Template placeholders
Table of supported placeholders allowed to be used in the `template` configuration. Table of supported placeholders allowed to be used in the `pr_template` configuration.
| **Placeholder** | **Description** | | **Placeholder** | **Description** |
|------------------|-------------------------------------------------------------| |------------------|-------------------------------------------------------------|
@@ -230,26 +232,33 @@ Table of supported placeholders allowed to be used in the `template` configurati
### Template placeholders ### Template placeholders
Table of supported placeholders allowed to be used in the `pr_template` configuration. Table of supported placeholders allowed to be used in the `template` and `empty_template` (only supports placeholder marked for empty) configuration.
| **Placeholder** | **Description** | **Empty** |
|----------------------------|----------------------------------------------------------------------------------------------------|:---------:|
| `${{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration | |
| `${{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories | |
| `${{OWNER}}` | Describes the owner of the repository the changelog was generated for | x |
| `${{REPO}}` | The repository name of the repo the changelog was generated for | x |
| `${{FROM_TAG}}` | Defines the 'start' from where the changelog did consider merged pull requests | x |
| `${{TO_TAG}}` | Defines until which tag the changelog did consider merged pull requests | x |
| `${{CATEGORIZED_COUNT}}` | The count of PRs which were categorized | |
| `${{UNCATEGORIZED_COUNT}}` | The count of PRs and changes which were not categorized. No label overlapping with category labels | |
| **Placeholder** | **Description** |
|----------------------|-------------------------------------------------------------------------------------------------|
| `${{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration |
| `${{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories |
### Configuration Specification ### Configuration Specification
Table of descriptions for the `configuration.json` options. Table of descriptions for the `configuration.json` options.
| **Input** | **Description** | | **Input** | **Description** |
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| categories | An array of `category` specifications, offering a flexible way to group changes into categories | | 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.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.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 |
| sort | The sort order of pull requests. [ASC, DESC] | | 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 | | 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 | | 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. Does not support placeholders | | empty_template | Template to pick if no changes are detected. See [Template placeholders](#template-placeholders) for possible values |
| 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 | | 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.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 | | 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 |
+24 -142
View File
@@ -1,152 +1,34 @@
import {ReleaseNotes} from '../src/releaseNotes' import * as path from 'path';
import { resolveConfiguration } from '../src/utils'; import * as process from 'process'
import * as cp from 'child_process'
// shows how the runner will run a javascript action with env / stdout protocol
/*
test('test runs', () => {
jest.setTimeout(180000);
test('missing values should result in failure', () => {
process.env['GITHUB_WORKSPACE'] = '.'
process.env['INPUT_CONFIGURATION'] = 'configuration.json' process.env['INPUT_CONFIGURATION'] = 'configuration.json'
const ip = path.join(__dirname, '..', 'lib', 'main.js') const ip = path.join(__dirname, '..', 'lib', 'main.js')
const options: cp.ExecSyncOptions = { const options: cp.ExecSyncOptions = {
env: process.env env: process.env
} }
console.log(cp.execSync(`node ${ip}`, options).toString()) try {
}) cp.execSync(`node ${ip}`, options).toString()
*/ fail("Should not succeed, because values miss")
it('Should have empty changelog (tags)', async () => { } catch (error) {
jest.setTimeout(180000) console.log(`correctly failed due to: ${error}`)
}
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.0.1',
toTag: 'v0.0.2',
ignorePreReleases: false,
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(null)
}) })
it('Should match generated changelog (tags)', async () => { test('missing values should result in failure', () => {
jest.setTimeout(180000) process.env['GITHUB_WORKSPACE'] = '.'
process.env['INPUT_CONFIGURATION'] = 'configuration.json'
process.env['INPUT_OWNER'] = 'mikepenz'
process.env['INPUT_REPO'] = 'release-changelog-builder-action'
process.env['INPUT_FROMTAG'] = 'v0.3.0'
process.env['INPUT_TOTAG'] = 'v0.5.0'
const configuration = resolveConfiguration('', 'configs/configuration.json') const ip = path.join(__dirname, '..', 'lib', 'main.js')
const releaseNotes = new ReleaseNotes({ const options: cp.ExecSyncOptions = {
owner: 'mikepenz', env: process.env
repo: 'release-changelog-builder-action', }
fromTag: 'v0.0.1', const result = cp.execSync(`node ${ip}`, options).toString()
toTag: 'v0.0.3', // should succeed
ignorePreReleases: false,
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
- [CI] Specify Test Case
- PR: #10
`)
})
it('Should match generated changelog (unspecified fromTag)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: null,
toTag: 'v0.0.3',
ignorePreReleases: false,
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
- [CI] Specify Test Case
- PR: #10
`)
})
it('Should match generated changelog (refs)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_all_placeholders.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: '5ec7a2d86fe9f43fdd38d5e254a1117c8a51b4c3',
toTag: 'fa3788c8c4b3373ef8424ce3eb008a5cd07cc5aa',
ignorePreReleases: false,
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
[CI] Specify Test Case
10
https://github.com/mikepenz/release-changelog-builder-action/pull/10
2020-10-16T13:59:36.000Z
mikepenz
test
1.0.0
- specify test case
mikepenz, nhoelzl
nhoelzl
`)
})
it('Should match ordered ASC', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_asc.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.3.0',
toTag: 'v0.5.0',
ignorePreReleases: false,
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n22\n24\n25\n26\n28\n\n## 🐛 Fixes\n\n23\n\n`)
})
it('Should match ordered DESC', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_desc.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.3.0',
toTag: 'v0.5.0',
ignorePreReleases: false,
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n28\n26\n25\n24\n22\n\n## 🐛 Fixes\n\n23\n\n`)
}) })
+117
View File
@@ -0,0 +1,117 @@
import { ReleaseNotes } from '../src/releaseNotes'
import { resolveConfiguration } from '../src/utils';
import { Octokit } from '@octokit/rest';
// load octokit instance
const octokit = new Octokit({
auth: `token ${process.env.GITHUB_TOKEN}`
})
it('Should have empty changelog (tags)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotes = new ReleaseNotes(octokit, {
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.0.1',
toTag: 'v0.0.2',
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(null)
})
it('Should match generated changelog (tags)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotes = new ReleaseNotes(octokit, {
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.0.1',
toTag: 'v0.0.3',
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
- [CI] Specify Test Case
- PR: #10
`)
})
it('Should match generated changelog (refs)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_all_placeholders.json')
const releaseNotes = new ReleaseNotes(octokit, {
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: '5ec7a2d86fe9f43fdd38d5e254a1117c8a51b4c3',
toTag: 'fa3788c8c4b3373ef8424ce3eb008a5cd07cc5aa',
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
[CI] Specify Test Case
10
https://github.com/mikepenz/release-changelog-builder-action/pull/10
2020-10-16T13:59:36.000Z
mikepenz
test
1.0.0
- specify test case
mikepenz, nhoelzl
nhoelzl
`)
})
it('Should match ordered ASC', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_asc.json')
const releaseNotes = new ReleaseNotes(octokit, {
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.3.0',
toTag: 'v0.5.0',
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n22\n24\n25\n26\n28\n\n## 🐛 Fixes\n\n23\n\n`)
})
it('Should match ordered DESC', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_desc.json')
const releaseNotes = new ReleaseNotes(octokit, {
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.3.0',
toTag: 'v0.5.0',
failOnError: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n28\n26\n25\n24\n22\n\n## 🐛 Fixes\n\n23\n\n`)
})
+91
View File
@@ -0,0 +1,91 @@
import { resolveConfiguration } from '../src/utils';
import { ReleaseNotesBuilder } from '../src/releaseNotesBuilder';
it('Should match generated changelog (unspecified fromTag)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
null,
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
- [CI] Specify Test Case
- PR: #10
`)
})
it('Should use empty placeholder', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
'v0.0.2',
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`- no changes`)
})
it('Should fill empty placeholders', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_empty_all_placeholders.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
'v0.0.2',
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`mikepenz\nrelease-changelog-builder-action\nv0.0.2\nv0.0.3`)
})
it('Should fill `template` placeholders', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_empty_all_placeholders.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
'v0.0.1',
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests\n\n- [CI] Specify Test Case\n - PR: #10\n\n\n\nmikepenz\nrelease-changelog-builder-action\nv0.0.1\nv0.0.3\n1\n0`)
})
+51
View File
@@ -0,0 +1,51 @@
import { resolveConfiguration } from '../src/utils';
import { ReleaseNotesBuilder } from '../src/releaseNotesBuilder';
import { TagInfo, sortTags } from '../src/tags';
it('Should order tags correctly', async () => {
jest.setTimeout(180000)
const tags: TagInfo[] = [
{ name: "2020.4.0", commit: "" },
{ name: "2020.4.0-rc02", commit: "" },
{ name: "2020.3.2", commit: "" },
{ name: "v2020.3.1", commit: "" },
{ name: "2020.3.1-rc03", commit: "" },
{ name: "2020.3.1-rc01", commit: "" },
{ name: "2020.3.1-b01", commit: "" },
{ name: "v2020.3.0", commit: "" }
]
const sorted = sortTags(tags).map(function (tag) {
return tag.name
}).join(",")
expect(sorted).toStrictEqual(`2020.4.0,2020.4.0-rc02,2020.3.2,v2020.3.1,2020.3.1-rc03,2020.3.1-rc01,2020.3.1-b01,v2020.3.0`)
})
it('Should order tags correctly', async () => {
jest.setTimeout(180000)
const tags: TagInfo[] = [
{ name: "0.0.1", commit: "" },
{ name: "0.0.1-rc01", commit: "" },
{ name: "0.1.0", commit: "" },
{ name: "0.1.0-b01", commit: "" },
{ name: "1.0.0", commit: "" },
{ name: "1.0.0-a01", commit: "" },
{ name: "2.0.0", commit: "" },
{ name: "10.0.0", commit: "" },
{ name: "10.1.0", commit: "" },
{ name: "10.1.0-2", commit: "" },
{ name: "20.0.2", commit: "" },
{ name: "100.0.0", commit: "" },
{ name: "1000.0.0", commit: "" },
]
const sorted = sortTags(tags).map(function (tag) {
return tag.name
}).join(",")
expect(sorted).toStrictEqual(`1000.0.0,100.0.0,20.0.2,10.1.0,10.1.0-2,10.0.0,2.0.0,1.0.0,1.0.0-a01,0.1.0,0.1.0-b01,0.0.1,0.0.1-rc01`)
})
@@ -0,0 +1,4 @@
{
"template": "${{CHANGELOG}}\n${{UNCATEGORIZED}}\n${{OWNER}}\n${{REPO}}\n${{FROM_TAG}}\n${{TO_TAG}}\n${{CATEGORIZED_COUNT}}\n${{UNCATEGORIZED_COUNT}}",
"empty_template": "${{OWNER}}\n${{REPO}}\n${{FROM_TAG}}\n${{TO_TAG}}"
}
Generated Vendored
+2333 -116
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+19
View File
@@ -583,6 +583,25 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
semver
ISC
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
tunnel tunnel
MIT MIT
The MIT License (MIT) The MIT License (MIT)
+33 -7
View File
@@ -1740,9 +1740,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "14.11.8", "version": "14.11.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz",
"integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==" "integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA=="
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
@@ -1756,6 +1756,11 @@
"integrity": "sha512-IiPhNnenzkqdSdQH3ifk9LoX7oQe61ZlDdDO4+MUv6FyWdPGDPr26gCPVs3oguZEMq//nFZZpwUZcVuNJsG+DQ==", "integrity": "sha512-IiPhNnenzkqdSdQH3ifk9LoX7oQe61ZlDdDO4+MUv6FyWdPGDPr26gCPVs3oguZEMq//nFZZpwUZcVuNJsG+DQ==",
"dev": true "dev": true
}, },
"@types/semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ=="
},
"@types/stack-utils": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -5168,6 +5173,14 @@
"@babel/types": "^7.4.0", "@babel/types": "^7.4.0",
"istanbul-lib-coverage": "^2.0.5", "istanbul-lib-coverage": "^2.0.5",
"semver": "^6.0.0" "semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
} }
}, },
"istanbul-lib-report": { "istanbul-lib-report": {
@@ -6031,6 +6044,12 @@
"lodash": "^4.17.19" "lodash": "^4.17.19"
} }
}, },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"source-map": { "source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -7109,6 +7128,14 @@
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"pretty-format": "^24.9.0", "pretty-format": "^24.9.0",
"semver": "^6.2.0" "semver": "^6.2.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
} }
}, },
"jest-util": { "jest-util": {
@@ -8457,10 +8484,9 @@
} }
}, },
"semver": { "semver": {
"version": "6.3.0", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
"dev": true
}, },
"set-blocking": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
+4 -2
View File
@@ -29,11 +29,13 @@
"@actions/exec": "^1.0.4", "@actions/exec": "^1.0.4",
"@actions/github": "^4.0.0", "@actions/github": "^4.0.0",
"@octokit/rest": "^18.0.6", "@octokit/rest": "^18.0.6",
"moment": "^2.29.1" "@types/semver": "^7.3.4",
"moment": "^2.29.1",
"semver": "^7.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.14", "@types/jest": "^26.0.14",
"@types/node": "^14.11.8", "@types/node": "^14.11.10",
"@typescript-eslint/parser": "^4.4.1", "@typescript-eslint/parser": "^4.4.1",
"@vercel/ncc": "^0.24.1", "@vercel/ncc": "^0.24.1",
"eslint": "^7.11.0", "eslint": "^7.11.0",
+9 -62
View File
@@ -1,13 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import { import {retrieveRepositoryPath, resolveConfiguration} from './utils'
failOrError,
retrieveRepositoryPath,
resolveConfiguration
} from './utils'
import {ReleaseNotes} from './releaseNotes'
import {createCommandManager} from './gitHelper'
import * as github from '@actions/github' import * as github from '@actions/github'
import {DefaultConfiguration} from './configuration' import {ReleaseNotesBuilder} from './releaseNotesBuilder'
async function run(): Promise<void> { async function run(): Promise<void> {
core.setOutput('failed', false) // mark the action not failed by default core.setOutput('failed', false) // mark the action not failed by default
@@ -31,71 +25,24 @@ async function run(): Promise<void> {
const repo = core.getInput('repo') || github.context.repo.repo const repo = core.getInput('repo') || github.context.repo.repo
// read in from, to tag inputs // read in from, to tag inputs
const fromTag = core.getInput('fromTag') const fromTag = core.getInput('fromTag')
let toTag = core.getInput('toTag') const toTag = core.getInput('toTag')
// read in flags // read in flags
const ignorePreReleases = core.getInput('ignorePreReleases') === 'true' const ignorePreReleases = core.getInput('ignorePreReleases') === 'true'
const failOnError = core.getInput('failOnError') === 'true' const failOnError = core.getInput('failOnError') === 'true'
// ensure to resolve the toTag if it was not provided const result = await new ReleaseNotesBuilder(
if (!toTag) { token,
// if not specified try to retrieve tag from github.context.ref repositoryPath,
if (github.context.ref.startsWith('refs/tags/')) {
toTag = github.context.ref.replace('refs/tags/', '')
core.info(
`🔖 Resolved current tag (${toTag}) from the 'github.context.ref'`
)
} else {
// if not specified try to retrieve tag from git
const gitHelper = await createCommandManager(repositoryPath)
const latestTag = await gitHelper.latestTag()
toTag = latestTag
core.info(
`🔖 Resolved current tag (${toTag}) from 'git rev-list --tags --skip=0 --max-count=1'`
)
}
}
if (!owner) {
failOrError(`💥 Missing or couldn't resolve 'owner'`, failOnError)
return
} else {
core.setOutput('owner', owner)
core.debug(`Resolved 'owner' as ${owner}`)
}
if (!repo) {
failOrError(`💥 Missing or couldn't resolve 'owner'`, failOnError)
return
} else {
core.setOutput('repo', repo)
core.debug(`Resolved 'repo' as ${repo}`)
}
if (!toTag) {
failOrError(`💥 Missing or couldn't resolve 'toTag'`, failOnError)
return
} else {
core.setOutput('toTag', toTag)
core.debug(`Resolved 'toTag' as ${toTag}`)
}
core.endGroup()
const releaseNotes = new ReleaseNotes({
owner, owner,
repo, repo,
fromTag, fromTag,
toTag, toTag,
ignorePreReleases,
failOnError, failOnError,
ignorePreReleases,
configuration configuration
}) ).build()
core.setOutput( core.setOutput('changelog', result)
'changelog',
(await releaseNotes.pull(token)) ||
configuration.empty_template ||
DefaultConfiguration.empty_template
)
} catch (error) { } catch (error) {
core.setFailed(error.message) core.setFailed(error.message)
} }
+11 -52
View File
@@ -3,71 +3,26 @@ import {Commits, CommitInfo} from './commits'
import {PullRequestInfo, PullRequests} from './pullRequests' import {PullRequestInfo, PullRequests} from './pullRequests'
import {buildChangelog} from './transform' import {buildChangelog} from './transform'
import * as core from '@actions/core' import * as core from '@actions/core'
import {Tags} from './tags'
import {Configuration, DefaultConfiguration} from './configuration' import {Configuration, DefaultConfiguration} from './configuration'
import {failOrError} from './utils' import {failOrError} from './utils'
export interface ReleaseNotesOptions { export interface ReleaseNotesOptions {
owner: string // the owner of the repository owner: string // the owner of the repository
repo: string // the repository repo: string // the repository
fromTag: string | null // the tag/ref to start from fromTag: string // the tag/ref to start from
toTag: string // the tag/ref up to toTag: string // the tag/ref up to
ignorePreReleases: boolean // defines if we should ignore any pre-releases for matching, only relevant if fromTag is null
failOnError: boolean // defines if we should fail the action in case of an error failOnError: boolean // defines if we should fail the action in case of an error
configuration: Configuration // the configuration as defined in `configuration.ts` configuration: Configuration // the configuration as defined in `configuration.ts`
} }
export class ReleaseNotes { export class ReleaseNotes {
constructor(private options: ReleaseNotesOptions) {} constructor(private octokit: Octokit, private options: ReleaseNotesOptions) {}
async pull(token?: string): Promise<string | null> { async pull(): Promise<string | null> {
const octokit = new Octokit({ const {configuration} = this.options
auth: `token ${token || process.env.GITHUB_TOKEN}`
})
const {
owner,
repo,
toTag,
ignorePreReleases,
failOnError,
configuration
} = this.options
if (!this.options.fromTag) {
core.startGroup(`🔖 Resolve previous tag`)
core.debug(`fromTag undefined, trying to resolve via API`)
const tagsApi = new Tags(octokit)
const previousTag = await tagsApi.findPredecessorTag(
owner,
repo,
toTag,
ignorePreReleases,
configuration.max_tags_to_fetch ||
DefaultConfiguration.max_tags_to_fetch
)
if (previousTag == null) {
failOrError(
`💥 Unable to retrieve previous tag given ${toTag}`,
failOnError
)
return null
}
this.options.fromTag = previousTag.name
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
core.endGroup()
}
if (!this.options.fromTag) {
failOrError(`💥 Missing or couldn't resolve 'fromTag'`, failOnError)
return null
} else {
core.setOutput('fromTag', this.options.fromTag)
}
core.startGroup(`🚀 Load pull requests`) core.startGroup(`🚀 Load pull requests`)
const mergedPullRequests = await this.getMergedPullRequests(octokit) const mergedPullRequests = await this.getMergedPullRequests(this.octokit)
core.endGroup() core.endGroup()
if (mergedPullRequests.length === 0) { if (mergedPullRequests.length === 0) {
@@ -76,7 +31,11 @@ export class ReleaseNotes {
} }
core.startGroup('📦 Build changelog') core.startGroup('📦 Build changelog')
const resultChangelog = buildChangelog(mergedPullRequests, configuration) const resultChangelog = buildChangelog(
mergedPullRequests,
configuration,
this.options
)
core.endGroup() core.endGroup()
return resultChangelog return resultChangelog
} }
@@ -97,7 +56,7 @@ export class ReleaseNotes {
const commitsApi = new Commits(octokit) const commitsApi = new Commits(octokit)
let commits: CommitInfo[] let commits: CommitInfo[]
try { try {
commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag) commits = await commitsApi.getDiff(owner, repo, fromTag, toTag)
} catch (error) { } catch (error) {
failOrError( failOrError(
`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, `💥 Failed to retrieve - Invalid tag? - Because of: ${error}`,
+119
View File
@@ -0,0 +1,119 @@
import {Configuration, DefaultConfiguration} from './configuration'
import * as github from '@actions/github'
import * as core from '@actions/core'
import {createCommandManager} from './gitHelper'
import {failOrError} from './utils'
import {Octokit} from '@octokit/rest'
import {Tags} from './tags'
import {ReleaseNotes} from './releaseNotes'
import {fillAdditionalPlaceholders} from './transform'
export class ReleaseNotesBuilder {
constructor(
private token: string | null,
private repositoryPath: string,
private owner: string | null,
private repo: string | null,
private fromTag: string | null,
private toTag: string | null,
private failOnError: boolean,
private ignorePreReleases: boolean,
private configuration: Configuration
) {}
async build(): Promise<string | null> {
// ensure to resolve the toTag if it was not provided
if (!this.toTag) {
// if not specified try to retrieve tag from github.context.ref
if (github.context.ref.startsWith('refs/tags/')) {
this.toTag = github.context.ref.replace('refs/tags/', '')
core.info(
`🔖 Resolved current tag (${this.toTag}) from the 'github.context.ref'`
)
} else {
// if not specified try to retrieve tag from git
const gitHelper = await createCommandManager(this.repositoryPath)
const latestTag = await gitHelper.latestTag()
this.toTag = latestTag
core.info(
`🔖 Resolved current tag (${this.toTag}) from 'git rev-list --tags --skip=0 --max-count=1'`
)
}
}
if (!this.owner) {
failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError)
return null
} else {
core.setOutput('owner', this.owner)
core.debug(`Resolved 'owner' as ${this.owner}`)
}
if (!this.repo) {
failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError)
return null
} else {
core.setOutput('repo', this.repo)
core.debug(`Resolved 'repo' as ${this.repo}`)
}
if (!this.toTag) {
failOrError(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError)
return null
} else {
core.setOutput('toTag', this.toTag)
core.debug(`Resolved 'toTag' as ${this.toTag}`)
}
core.endGroup()
// load octokit instance
const octokit = new Octokit({
auth: `token ${this.token || process.env.GITHUB_TOKEN}`
})
// ensure to resolve the fromTag if it was not provided specifically
if (!this.fromTag) {
core.startGroup(`🔖 Resolve previous tag`)
core.debug(`fromTag undefined, trying to resolve via API`)
const tagsApi = new Tags(octokit)
const previousTag = await tagsApi.findPredecessorTag(
this.owner,
this.repo,
this.toTag,
this.ignorePreReleases,
this.configuration.max_tags_to_fetch ||
DefaultConfiguration.max_tags_to_fetch
)
if (previousTag == null) {
failOrError(
`💥 Unable to retrieve previous tag given ${this.toTag}`,
this.failOnError
)
return null
}
this.fromTag = previousTag.name
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
core.endGroup()
}
const options = {
owner: this.owner,
repo: this.repo,
fromTag: this.fromTag,
toTag: this.toTag,
failOnError: this.failOnError,
configuration: this.configuration
}
const releaseNotes = new ReleaseNotes(octokit, options)
return (
(await releaseNotes.pull()) ||
fillAdditionalPlaceholders(
this.configuration.empty_template ||
DefaultConfiguration.empty_template,
options
)
)
}
}
+18 -19
View File
@@ -1,5 +1,7 @@
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest' import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as semver from 'semver'
import {SemVer} from 'semver'
export interface TagInfo { export interface TagInfo {
name: string name: string
@@ -52,7 +54,7 @@ export class Tags {
ignorePreReleases: boolean, ignorePreReleases: boolean,
maxTagsToFetch: number maxTagsToFetch: number
): Promise<TagInfo | null> { ): Promise<TagInfo | null> {
const tags = this.sortTags(await this.getTags(owner, repo, maxTagsToFetch)) const tags = sortTags(await this.getTags(owner, repo, maxTagsToFetch))
try { try {
const length = tags.length const length = tags.length
@@ -76,8 +78,9 @@ export class Tags {
return null return null
} }
} }
}
/* /*
Sorts an array of tags as shown below: Sorts an array of tags as shown below:
2020.4.0 2020.4.0
@@ -91,23 +94,19 @@ export class Tags {
2020.3.1-a01 2020.3.1-a01
2020.3.0 2020.3.0
*/ */
private sortTags(commits: TagInfo[]): TagInfo[] { export function sortTags(tags: TagInfo[]): TagInfo[] {
commits.sort((b, a) => { // filter out tags which do not follow semver
const partsA = a.name.replace(/^v/, '').split('-') const validatedTags = tags.filter(tag => {
const partsB = b.name.replace(/^v/, '').split('-') const isValid = semver.valid(tag.name) !== null
const versionCompare = partsA[0].localeCompare(partsB[0]) if(!isValid) {
if (versionCompare !== 0) { core.debug(`⚠️ dropped tag ${tag.name} because it is not a valid semver tag`)
return versionCompare
} else {
if (partsA.length === 1) {
return 0
} else if (partsB.length === 1) {
return 1
} else {
return partsA[1].localeCompare(partsB[1])
}
} }
return isValid
}) })
return commits
} // sort using semver
validatedTags.sort((b, a) => {
return new SemVer(a.name).compare(b.name)
})
return validatedTags
} }
+37 -5
View File
@@ -1,5 +1,6 @@
import {PullRequestInfo, sortPullRequests} from './pullRequests' import {PullRequestInfo, sortPullRequests} from './pullRequests'
import * as core from '@actions/core' import * as core from '@actions/core'
import {ReleaseNotesOptions} from './releaseNotes'
import { import {
Category, Category,
Configuration, Configuration,
@@ -9,7 +10,8 @@ import {
export function buildChangelog( export function buildChangelog(
prs: PullRequestInfo[], prs: PullRequestInfo[],
config: Configuration config: Configuration,
options: ReleaseNotesOptions
): string { ): string {
// sort to target order // sort to target order
const sort = config.sort || DefaultConfiguration.sort const sort = config.sort || DefaultConfiguration.sort
@@ -26,9 +28,7 @@ export function buildChangelog(
transform( transform(
fillTemplate( fillTemplate(
pr, pr,
config.pr_template config.pr_template || DefaultConfiguration.pr_template
? config.pr_template
: DefaultConfiguration.pr_template
), ),
validatedTransformers validatedTransformers
) )
@@ -45,6 +45,7 @@ export function buildChangelog(
for (const category of categories) { for (const category of categories) {
categorized.set(category, []) categorized.set(category, [])
} }
const categorizedPrs: string[] = []
const uncategorized: string[] = [] const uncategorized: string[] = []
// bring elements in order // bring elements in order
@@ -60,6 +61,8 @@ export function buildChangelog(
if (!matched) { if (!matched) {
uncategorized.push(body) uncategorized.push(body)
} else {
categorizedPrs.push(body)
} }
} }
core.info(`️ Ordered all pull requests into ${categories.length} categories`) core.info(`️ Ordered all pull requests into ${categories.length} categories`)
@@ -98,10 +101,37 @@ export function buildChangelog(
'${{UNCATEGORIZED}}', '${{UNCATEGORIZED}}',
changelogUncategorized changelogUncategorized
) )
// fill other placeholders
transformedChangelog = transformedChangelog.replace(
'${{CATEGORIZED_COUNT}}',
categorizedPrs.length.toString()
)
transformedChangelog = transformedChangelog.replace(
'${{UNCATEGORIZED_COUNT}}',
uncategorized.length.toString()
)
transformedChangelog = fillAdditionalPlaceholders(
transformedChangelog,
options
)
core.info(`️ Filled template`) core.info(`️ Filled template`)
return transformedChangelog return transformedChangelog
} }
export function fillAdditionalPlaceholders(
text: string,
options: ReleaseNotesOptions
): string {
let transformed = text
transformed = transformed.replace('${{OWNER}}', options.owner)
transformed = transformed.replace('${{REPO}}', options.repo)
transformed = transformed.replace('${{FROM_TAG}}', options.fromTag)
transformed = transformed.replace('${{TO_TAG}}', options.toTag)
return transformed
}
function haveCommonElements(arr1: string[], arr2: string[]): Boolean { function haveCommonElements(arr1: string[], arr2: string[]): Boolean {
return arr1.some(item => arr2.includes(item)) return arr1.some(item => arr2.includes(item))
} }
@@ -133,7 +163,9 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
} }
let transformed = filled let transformed = filled
for (const {target, pattern} of transformers) { for (const {target, pattern} of transformers) {
transformed = transformed.replace(pattern!!, target) if (pattern) {
transformed = transformed.replace(pattern, target)
}
} }
return transformed return transformed
} }