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
+30 -21
View File
@@ -61,7 +61,9 @@ Specify the action as part of your GitHub actions workflow:
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
@@ -213,7 +215,7 @@ For advanced use cases additional settings can be provided to the action
### 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** |
|------------------|-------------------------------------------------------------|
@@ -230,33 +232,40 @@ Table of supported placeholders allowed to be used in the `template` configurati
### 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
Table of descriptions for the `configuration.json` options.
| **Input** | **Description** |
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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 |
| **Input** | **Description** |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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 |
| 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. Does not support placeholders |
| 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 |
| 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 |
| max_tags_to_fetch | The maximum amount of tags to load from the API to find the previous tag. Loaded paginated with 100 per page |
| max_pull_requests | The maximum amount of pull requests to load from the API. Loaded paginated with 30 per page |
| max_back_track_time_days | Defines the max amount of days to go back in time per changelog |
| exclude_merge_branches | An array of branches to be ignored from processing as merge commits |
| 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 |
| max_tags_to_fetch | The maximum amount of tags to load from the API to find the previous tag. Loaded paginated with 100 per page |
| max_pull_requests | The maximum amount of pull requests to load from the API. Loaded paginated with 30 per page |
| max_back_track_time_days | Defines the max amount of days to go back in time per changelog |
| exclude_merge_branches | An array of branches to be ignored from processing as merge commits |
## Contribute 🧬
+25 -143
View File
@@ -1,152 +1,34 @@
import {ReleaseNotes} from '../src/releaseNotes'
import { resolveConfiguration } from '../src/utils';
import * as path from 'path';
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'
const ip = path.join(__dirname, '..', 'lib', 'main.js')
const options: cp.ExecSyncOptions = {
env: process.env
}
console.log(cp.execSync(`node ${ip}`, options).toString())
})
*/
it('Should have empty changelog (tags)', async () => {
jest.setTimeout(180000)
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)
try {
cp.execSync(`node ${ip}`, options).toString()
fail("Should not succeed, because values miss")
} catch (error) {
console.log(`correctly failed due to: ${error}`)
}
})
it('Should match generated changelog (tags)', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.0.1',
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 (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`)
test('missing values should result in failure', () => {
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 ip = path.join(__dirname, '..', 'lib', 'main.js')
const options: cp.ExecSyncOptions = {
env: process.env
}
const result = cp.execSync(`node ${ip}`, options).toString()
// should succeed
})
+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
+2348 -131
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.
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
MIT
The MIT License (MIT)
+33 -7
View File
@@ -1740,9 +1740,9 @@
"dev": true
},
"@types/node": {
"version": "14.11.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz",
"integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw=="
"version": "14.11.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz",
"integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -1756,6 +1756,11 @@
"integrity": "sha512-IiPhNnenzkqdSdQH3ifk9LoX7oQe61ZlDdDO4+MUv6FyWdPGDPr26gCPVs3oguZEMq//nFZZpwUZcVuNJsG+DQ==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -5168,6 +5173,14 @@
"@babel/types": "^7.4.0",
"istanbul-lib-coverage": "^2.0.5",
"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": {
@@ -6031,6 +6044,12 @@
"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": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -7109,6 +7128,14 @@
"natural-compare": "^1.4.0",
"pretty-format": "^24.9.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": {
@@ -8457,10 +8484,9 @@
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
},
"set-blocking": {
"version": "2.0.0",
+4 -2
View File
@@ -29,11 +29,13 @@
"@actions/exec": "^1.0.4",
"@actions/github": "^4.0.0",
"@octokit/rest": "^18.0.6",
"moment": "^2.29.1"
"@types/semver": "^7.3.4",
"moment": "^2.29.1",
"semver": "^7.3.2"
},
"devDependencies": {
"@types/jest": "^26.0.14",
"@types/node": "^14.11.8",
"@types/node": "^14.11.10",
"@typescript-eslint/parser": "^4.4.1",
"@vercel/ncc": "^0.24.1",
"eslint": "^7.11.0",
+9 -62
View File
@@ -1,13 +1,7 @@
import * as core from '@actions/core'
import {
failOrError,
retrieveRepositoryPath,
resolveConfiguration
} from './utils'
import {ReleaseNotes} from './releaseNotes'
import {createCommandManager} from './gitHelper'
import {retrieveRepositoryPath, resolveConfiguration} from './utils'
import * as github from '@actions/github'
import {DefaultConfiguration} from './configuration'
import {ReleaseNotesBuilder} from './releaseNotesBuilder'
async function run(): Promise<void> {
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
// read in from, to tag inputs
const fromTag = core.getInput('fromTag')
let toTag = core.getInput('toTag')
const toTag = core.getInput('toTag')
// read in flags
const ignorePreReleases = core.getInput('ignorePreReleases') === 'true'
const failOnError = core.getInput('failOnError') === 'true'
// ensure to resolve the toTag if it was not provided
if (!toTag) {
// if not specified try to retrieve tag from github.context.ref
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({
const result = await new ReleaseNotesBuilder(
token,
repositoryPath,
owner,
repo,
fromTag,
toTag,
ignorePreReleases,
failOnError,
ignorePreReleases,
configuration
})
).build()
core.setOutput(
'changelog',
(await releaseNotes.pull(token)) ||
configuration.empty_template ||
DefaultConfiguration.empty_template
)
core.setOutput('changelog', result)
} catch (error) {
core.setFailed(error.message)
}
+11 -52
View File
@@ -3,71 +3,26 @@ import {Commits, CommitInfo} from './commits'
import {PullRequestInfo, PullRequests} from './pullRequests'
import {buildChangelog} from './transform'
import * as core from '@actions/core'
import {Tags} from './tags'
import {Configuration, DefaultConfiguration} from './configuration'
import {failOrError} from './utils'
export interface ReleaseNotesOptions {
owner: string // the owner of 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
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
configuration: Configuration // the configuration as defined in `configuration.ts`
}
export class ReleaseNotes {
constructor(private options: ReleaseNotesOptions) {}
constructor(private octokit: Octokit, private options: ReleaseNotesOptions) {}
async pull(token?: string): Promise<string | null> {
const octokit = new Octokit({
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)
}
async pull(): Promise<string | null> {
const {configuration} = this.options
core.startGroup(`🚀 Load pull requests`)
const mergedPullRequests = await this.getMergedPullRequests(octokit)
const mergedPullRequests = await this.getMergedPullRequests(this.octokit)
core.endGroup()
if (mergedPullRequests.length === 0) {
@@ -76,7 +31,11 @@ export class ReleaseNotes {
}
core.startGroup('📦 Build changelog')
const resultChangelog = buildChangelog(mergedPullRequests, configuration)
const resultChangelog = buildChangelog(
mergedPullRequests,
configuration,
this.options
)
core.endGroup()
return resultChangelog
}
@@ -97,7 +56,7 @@ export class ReleaseNotes {
const commitsApi = new Commits(octokit)
let commits: CommitInfo[]
try {
commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
commits = await commitsApi.getDiff(owner, repo, fromTag, toTag)
} catch (error) {
failOrError(
`💥 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
)
)
}
}
+20 -21
View File
@@ -1,5 +1,7 @@
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import * as core from '@actions/core'
import * as semver from 'semver'
import {SemVer} from 'semver'
export interface TagInfo {
name: string
@@ -52,7 +54,7 @@ export class Tags {
ignorePreReleases: boolean,
maxTagsToFetch: number
): Promise<TagInfo | null> {
const tags = this.sortTags(await this.getTags(owner, repo, maxTagsToFetch))
const tags = sortTags(await this.getTags(owner, repo, maxTagsToFetch))
try {
const length = tags.length
@@ -76,8 +78,9 @@ export class Tags {
return null
}
}
}
/*
/*
Sorts an array of tags as shown below:
2020.4.0
@@ -91,23 +94,19 @@ export class Tags {
2020.3.1-a01
2020.3.0
*/
private sortTags(commits: TagInfo[]): TagInfo[] {
commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-')
const partsB = b.name.replace(/^v/, '').split('-')
const versionCompare = partsA[0].localeCompare(partsB[0])
if (versionCompare !== 0) {
return versionCompare
} else {
if (partsA.length === 1) {
return 0
} else if (partsB.length === 1) {
return 1
} else {
return partsA[1].localeCompare(partsB[1])
}
}
})
return commits
}
export function sortTags(tags: TagInfo[]): TagInfo[] {
// filter out tags which do not follow semver
const validatedTags = tags.filter(tag => {
const isValid = semver.valid(tag.name) !== null
if(!isValid) {
core.debug(`⚠️ dropped tag ${tag.name} because it is not a valid semver tag`)
}
return isValid
})
// 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 * as core from '@actions/core'
import {ReleaseNotesOptions} from './releaseNotes'
import {
Category,
Configuration,
@@ -9,7 +10,8 @@ import {
export function buildChangelog(
prs: PullRequestInfo[],
config: Configuration
config: Configuration,
options: ReleaseNotesOptions
): string {
// sort to target order
const sort = config.sort || DefaultConfiguration.sort
@@ -26,9 +28,7 @@ export function buildChangelog(
transform(
fillTemplate(
pr,
config.pr_template
? config.pr_template
: DefaultConfiguration.pr_template
config.pr_template || DefaultConfiguration.pr_template
),
validatedTransformers
)
@@ -45,6 +45,7 @@ export function buildChangelog(
for (const category of categories) {
categorized.set(category, [])
}
const categorizedPrs: string[] = []
const uncategorized: string[] = []
// bring elements in order
@@ -60,6 +61,8 @@ export function buildChangelog(
if (!matched) {
uncategorized.push(body)
} else {
categorizedPrs.push(body)
}
}
core.info(`️ Ordered all pull requests into ${categories.length} categories`)
@@ -98,10 +101,37 @@ export function buildChangelog(
'${{UNCATEGORIZED}}',
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`)
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 {
return arr1.some(item => arr2.includes(item))
}
@@ -133,7 +163,9 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
}
let transformed = filled
for (const {target, pattern} of transformers) {
transformed = transformed.replace(pattern!!, target)
if (pattern) {
transformed = transformed.replace(pattern, target)
}
}
return transformed
}