@@ -1,8 +1,8 @@
|
|||||||
name: 'CI'
|
name: "CI"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- "*"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -25,13 +25,13 @@ jobs:
|
|||||||
- name: Install NPM
|
- name: Install NPM
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
- name: Run NPM
|
- name: Run NPM
|
||||||
run: |
|
run: |
|
||||||
npm run all
|
npm run all
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
test:
|
test:
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Echo Configuration without Checkout Changelog
|
- name: Echo Configuration without Checkout Changelog
|
||||||
env:
|
env:
|
||||||
CHANGELOG: ${{ steps.without_checkout.outputs.changelog }}
|
CHANGELOG: ${{ steps.without_checkout.outputs.changelog }}
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
uses: ./
|
uses: ./
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Echo Minimal Configuration Changelog
|
- name: Echo Minimal Configuration Changelog
|
||||||
env:
|
env:
|
||||||
CHANGELOG: ${{ steps.minimal_release.outputs.changelog }}
|
CHANGELOG: ${{ steps.minimal_release.outputs.changelog }}
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
token: ${{ secrets.PERSONAL_TOKEN }}
|
token: ${{ secrets.PERSONAL_TOKEN }}
|
||||||
exportCache: true
|
exportCache: true
|
||||||
exportOnly: true
|
exportOnly: true
|
||||||
|
|
||||||
- name: "External Repo Configuration"
|
- name: "External Repo Configuration"
|
||||||
id: external_changelog
|
id: external_changelog
|
||||||
uses: ./
|
uses: ./
|
||||||
@@ -188,4 +188,3 @@ jobs:
|
|||||||
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-b') || contains(github.ref, '-a') }}
|
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-b') || contains(github.ref, '-a') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -99,4 +99,5 @@ __tests__/runner/*
|
|||||||
lib/**/*
|
lib/**/*
|
||||||
|
|
||||||
lib
|
lib
|
||||||
src/pr-collector/dist
|
src/pr-collector/dist
|
||||||
|
.idea
|
||||||
+9
-1
@@ -6,5 +6,13 @@
|
|||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"bracketSpacing": false,
|
"bracketSpacing": false,
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.yml", "*.yaml"],
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ Specify the action as part of your GitHub actions workflow:
|
|||||||
uses: mikepenz/release-changelog-builder-action@{latest-release}
|
uses: mikepenz/release-changelog-builder-action@{latest-release}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> When upgrading from v4 to v5, please ensure to read the upgrade warning in the [release notes](https://github.com/mikepenz/release-changelog-builder-action/releases), especially if [regexes](https://github.com/mikepenz/release-changelog-builder-action?tab=readme-ov-file#regex-configuration) are used.
|
||||||
|
|
||||||
## Full Sample 🖥️
|
## Full Sample 🖥️
|
||||||
|
|
||||||
Below is a complete example showcasing how to define a build, which is executed when tagging the project. It consists of:
|
Below is a complete example showcasing how to define a build, which is executed when tagging the project. It consists of:
|
||||||
@@ -69,9 +72,6 @@ Below is a complete example showcasing how to define a build, which is executed
|
|||||||
- Build changelog, given the tag
|
- Build changelog, given the tag
|
||||||
- Create a release on GitHub - specifying body with a constructed changelog
|
- Create a release on GitHub - specifying body with a constructed changelog
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Pre v4 PRs will only show up in the changelog if assigned one of the default label categories "feature", "fix" or "test". Starting with v4 these PRs will be in the `Uncategorized` section.
|
|
||||||
|
|
||||||
<details><summary><b>Example</b></summary>
|
<details><summary><b>Example</b></summary>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)",
|
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)",
|
||||||
"target": "$1"
|
"target": "$1"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -279,7 +279,7 @@ A full set list of possible output values for this action.
|
|||||||
> 2. Create an initial tag on the commit you want to begin a changelog from (for example `v0.0.1`).
|
> 2. Create an initial tag on the commit you want to begin a changelog from (for example `v0.0.1`).
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> By default not specifying `fromTag` or `toTag` will resolve `toTag` from either the `ref` or alternatively fallback to the latest tag from the git API. `fromTag` is resolved by sorting tags using [semver](https://semver.org/). Check the [configuration](#configuration-specification) for alternatives.
|
> By default not specifying `fromTag` or `toTag` will resolve `toTag` from either the `ref` or alternatively fallback to the latest tag from the git API. `fromTag` is resolved by sorting tags using [semver](https://semver.org/). Tags not following semver are filtered out. Check the [configuration](#configuration-specification) for alternatives.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you are behind a corporate HTTP proxy, you can set the `https_proxy` environment variable to the proxy URL. For reference, please see the Octokit [documentation](https://github.com/octokit/octokit.js/#proxy-servers-nodejs-only).
|
> If you are behind a corporate HTTP proxy, you can set the `https_proxy` environment variable to the proxy URL. For reference, please see the Octokit [documentation](https://github.com/octokit/octokit.js/#proxy-servers-nodejs-only).
|
||||||
@@ -577,7 +577,7 @@ This applies to all configurations outlined in `Configuration Specification` and
|
|||||||
| <parent>.method | The extraction method used. Defaults to: `replace`. Alternative values: `replaceAll`, `match`. These methods specified references the JavaScript String method. And a special method `regexr`, that functions similar to the `list` within the regexr tool. |
|
| <parent>.method | The extraction method used. Defaults to: `replace`. Alternative values: `replaceAll`, `match`. These methods specified references the JavaScript String method. And a special method `regexr`, that functions similar to the `list` within the regexr tool. |
|
||||||
| <parent>.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
|
| <parent>.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
|
||||||
| <parent>.on_empty | Defines the placeholder to be filled in, if the regex does not lead to a result. |
|
| <parent>.on_empty | Defines the placeholder to be filled in, if the regex does not lead to a result. |
|
||||||
| <parent>.on_property | This is available for `Extractor` type regex objects. With the property describing a field available in PRs. (e.g.: title, body, ...) |
|
| <parent>.on_property | This is available for `Extractor` type regex objects. With the property describing a field available in PRs. (e.g.: title, body, ...) Default: `body`. |
|
||||||
|
|
||||||
<details><summary><b>Example regex configuration block</b></summary>
|
<details><summary><b>Example regex configuration block</b></summary>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test('missing values should result in failure', () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
cp.execSync(`node ${ip}`, options).toString()
|
cp.execSync(`node ${ip}`, options).toString()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
expect(true).toBe(true)
|
expect(true).toBe(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ it('[Github] Uncategorized category', async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('[Github] Verify commit based changelog', async () => {
|
it('[Github] Verify commit based changelog', async () => {
|
||||||
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits.json'))
|
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits.json'), 'COMMIT')
|
||||||
const releaseNotesBuilder = new ReleaseNotesBuilder(
|
const releaseNotesBuilder = new ReleaseNotesBuilder(
|
||||||
null,
|
null,
|
||||||
githubRepository,
|
githubRepository,
|
||||||
@@ -257,7 +257,7 @@ it('[Github] Verify commit based changelog', async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('[Github] Verify commit based changelog, with emoji categorisation', async () => {
|
it('[Github] Verify commit based changelog, with emoji categorisation', async () => {
|
||||||
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits_emoji.json'))
|
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits_emoji.json'), 'COMMIT')
|
||||||
const releaseNotesBuilder = new ReleaseNotesBuilder(
|
const releaseNotesBuilder = new ReleaseNotesBuilder(
|
||||||
null,
|
null,
|
||||||
githubRepository,
|
githubRepository,
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ it('Extract label from title, split regex', async () => {
|
|||||||
on_property: 'title'
|
on_property: 'title'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(buildChangelogTest(configuration, mergedPullRequests, repositoryUtils)).toStrictEqual(
|
expect(buildChangelogTest(configuration, mergedPullRequests, repositoryUtils)).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`
|
`## 🚀 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`
|
||||||
)
|
)
|
||||||
@@ -448,7 +449,8 @@ it('Use empty_content for empty category', async () => {
|
|||||||
labels: ['Feature']
|
labels: ['Feature']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(
|
const changelog = buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)
|
||||||
|
expect(changelog).toStrictEqual(
|
||||||
`## 🚀 Features and 🐛 Issues\n\n- No PRs in this category\n\n## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
|
`## 🚀 Features and 🐛 Issues\n\n- No PRs in this category\n\n## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+1425
-1840
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-14
@@ -668,7 +668,7 @@ ms
|
|||||||
MIT
|
MIT
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 Zeit, Inc.
|
Copyright (c) 2020 Vercel, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -801,19 +801,6 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
uuid
|
|
||||||
MIT
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
wrappy
|
wrappy
|
||||||
ISC
|
ISC
|
||||||
The ISC License
|
The ISC License
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Generated
+542
-742
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -36,7 +36,7 @@
|
|||||||
"author": "Mike Penz",
|
"author": "Mike Penz",
|
||||||
"license": "Apache 2.0",
|
"license": "Apache 2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.11.1",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.0",
|
||||||
"@octokit/rest": "^20.1.1",
|
"@octokit/rest": "^20.1.1",
|
||||||
@@ -46,12 +46,12 @@
|
|||||||
"semver": "^7.6.3"
|
"semver": "^7.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.1.0",
|
"@types/node": "^22.8.6",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||||
"@typescript-eslint/parser": "^7.17.0",
|
"@typescript-eslint/parser": "^7.17.0",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-github": "^5.0.1",
|
"eslint-plugin-github": "^5.0.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"jest-circus": "^29.7.0",
|
"jest-circus": "^29.7.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-24
@@ -1,11 +1,6 @@
|
|||||||
import {Extractor, PullConfiguration, Regex, Rule, Sort} from './pr-collector/types'
|
import {Extractor, PullConfiguration, Regex, Rule} from './pr-collector/types'
|
||||||
|
|
||||||
export interface Configuration extends PullConfiguration {
|
export interface Configuration extends PullConfiguration {
|
||||||
max_tags_to_fetch: number
|
|
||||||
max_pull_requests: number
|
|
||||||
max_back_track_time_days: number
|
|
||||||
exclude_merge_branches: string[]
|
|
||||||
sort: Sort | string // "ASC" or "DESC"
|
|
||||||
template: string
|
template: string
|
||||||
pr_template: string
|
pr_template: string
|
||||||
empty_template: string
|
empty_template: string
|
||||||
@@ -15,8 +10,6 @@ export interface Configuration extends PullConfiguration {
|
|||||||
duplicate_filter?: Extractor // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`)
|
duplicate_filter?: Extractor // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`)
|
||||||
reference?: Extractor // extracts a reference from a PR, used to establish parent child relations. This will remove the child from the main PR list.
|
reference?: Extractor // extracts a reference from a PR, used to establish parent child relations. This will remove the child from the main PR list.
|
||||||
transformers: Regex[]
|
transformers: Regex[]
|
||||||
tag_resolver: TagResolver
|
|
||||||
base_branches: string[]
|
|
||||||
custom_placeholders?: Placeholder[]
|
custom_placeholders?: Placeholder[]
|
||||||
trim_values: boolean
|
trim_values: boolean
|
||||||
}
|
}
|
||||||
@@ -63,6 +56,8 @@ export interface Placeholder {
|
|||||||
transformer: Regex // the transformer to use to transform the original placeholder into the custom placeheolder
|
transformer: Regex // the transformer to use to transform the original placeholder into the custom placeheolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PlaceholderGroup extends Map<string, Placeholder[]> {}
|
||||||
|
|
||||||
export const DefaultConfiguration: Configuration = {
|
export const DefaultConfiguration: Configuration = {
|
||||||
max_tags_to_fetch: 200, // the amount of tags to fetch from the github API
|
max_tags_to_fetch: 200, // the amount of tags to fetch from the github API
|
||||||
max_pull_requests: 200, // the amount of pull requests to process
|
max_pull_requests: 200, // the amount of pull requests to process
|
||||||
@@ -110,14 +105,8 @@ export const DefaultConfiguration: Configuration = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultCommitConfiguration: Configuration = {
|
export const DefaultCommitConfiguration: Configuration = {
|
||||||
max_tags_to_fetch: DefaultConfiguration.max_tags_to_fetch,
|
...DefaultConfiguration,
|
||||||
max_pull_requests: DefaultConfiguration.max_pull_requests,
|
pr_template: '- #{{TITLE}}', // the per PR template to pick
|
||||||
max_back_track_time_days: DefaultConfiguration.max_back_track_time_days,
|
|
||||||
exclude_merge_branches: DefaultConfiguration.exclude_merge_branches,
|
|
||||||
sort: DefaultConfiguration.sort,
|
|
||||||
template: '#{{CHANGELOG}}', // the global template to host the changelog
|
|
||||||
pr_template: '- #{{TITLE}}', // the per PR template to pick for commit based mode
|
|
||||||
empty_template: DefaultConfiguration.empty_template,
|
|
||||||
categories: [
|
categories: [
|
||||||
{
|
{
|
||||||
title: '## 🚀 Features',
|
title: '## 🚀 Features',
|
||||||
@@ -135,17 +124,11 @@ export const DefaultCommitConfiguration: Configuration = {
|
|||||||
title: '## 📦 Other',
|
title: '## 📦 Other',
|
||||||
labels: []
|
labels: []
|
||||||
}
|
}
|
||||||
], // the categories to support for the ordering
|
],
|
||||||
ignore_labels: DefaultConfiguration.ignore_labels,
|
|
||||||
label_extractor: [
|
label_extractor: [
|
||||||
{
|
{
|
||||||
pattern: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)',
|
pattern: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)',
|
||||||
target: '$1'
|
target: '$1'
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
transformers: DefaultConfiguration.transformers,
|
|
||||||
tag_resolver: DefaultConfiguration.tag_resolver,
|
|
||||||
base_branches: DefaultConfiguration.base_branches,
|
|
||||||
custom_placeholders: DefaultConfiguration.custom_placeholders,
|
|
||||||
trim_values: DefaultConfiguration.trim_values
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export interface PullRequestInfo {
|
|||||||
status: 'open' | 'merged'
|
status: 'open' | 'merged'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PullRequestData extends PullRequestInfo {
|
||||||
|
childPrs?: PullRequestInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommentInfo {
|
export interface CommentInfo {
|
||||||
id: number
|
id: number
|
||||||
htmlURL: string
|
htmlURL: string
|
||||||
@@ -79,7 +83,7 @@ export class PullRequests {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
return sortPrs(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests))
|
return sortPrsByMergedAt(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBetweenDates(
|
async getBetweenDates(
|
||||||
@@ -89,11 +93,11 @@ export class PullRequests {
|
|||||||
toDate: moment.Moment,
|
toDate: moment.Moment,
|
||||||
maxPullRequests: number
|
maxPullRequests: number
|
||||||
): Promise<PullRequestInfo[]> {
|
): Promise<PullRequestInfo[]> {
|
||||||
return sortPrs(await this.repositoryUtils.getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests))
|
return sortPrsByMergedAt(await this.repositoryUtils.getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
return sortPrs(await this.repositoryUtils.getOpen(owner, repo, maxPullRequests))
|
return sortPrsByMergedAt(await this.repositoryUtils.getOpen(owner, repo, maxPullRequests))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
|
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
|
||||||
@@ -221,7 +225,7 @@ export class PullRequests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortPrs(pullRequests: PullRequestInfo[]): PullRequestInfo[] {
|
function sortPrsByMergedAt(pullRequests: PullRequestInfo[]): PullRequestInfo[] {
|
||||||
return sortPullRequests(pullRequests, {
|
return sortPullRequests(pullRequests, {
|
||||||
order: 'ASC',
|
order: 'ASC',
|
||||||
on_property: 'mergedAt'
|
on_property: 'mergedAt'
|
||||||
|
|||||||
@@ -58,3 +58,20 @@ export interface RegexTransformer {
|
|||||||
method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
|
method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
|
||||||
onEmpty?: string
|
onEmpty?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class GroupedTemplateContext extends Map<string, string[]> {}
|
||||||
|
export class TemplateContext extends Map<string, string> {}
|
||||||
|
|
||||||
|
export type ChangelogStrings = {
|
||||||
|
categorized: string
|
||||||
|
uncategorized: string
|
||||||
|
open: string
|
||||||
|
ignored: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PrStrings = {
|
||||||
|
categorizedList: string[]
|
||||||
|
uncategorizedList: string[]
|
||||||
|
openList: string[]
|
||||||
|
ignoredList: string[]
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import {validateRegex} from './pr-collector/regexUtils'
|
|||||||
/**
|
/**
|
||||||
* Checks if any of the rules match the given PR
|
* Checks if any of the rules match the given PR
|
||||||
*/
|
*/
|
||||||
export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: Boolean): boolean {
|
export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: boolean): boolean {
|
||||||
const transformers: RegexTransformer[] = rules.map(rule => validateRegex(rule)).filter(t => t !== null) as RegexTransformer[]
|
const transformers: RegexTransformer[] = rules.map(rule => validateRegex(rule)).filter(t => t !== null) as RegexTransformer[]
|
||||||
if (exhaustive) {
|
if (exhaustive) {
|
||||||
return transformers.every(transformer => {
|
return transformers.every(transformer => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {Configuration} from './configuration'
|
import {Configuration} from './configuration'
|
||||||
import {checkExportedData, writeCacheData} from './utils'
|
import {checkExportedData, writeCacheData} from './utils'
|
||||||
import {buildChangelog, PullRequestData} from './transform'
|
import {buildChangelog} from './transform'
|
||||||
import {PullRequestCollector} from './pr-collector/prCollector'
|
import {PullRequestCollector} from './pr-collector/prCollector'
|
||||||
import {failOrError} from './pr-collector/utils'
|
import {failOrError} from './pr-collector/utils'
|
||||||
import {TagInfo} from './pr-collector/tags'
|
import {TagInfo} from './pr-collector/tags'
|
||||||
import {DiffInfo} from './pr-collector/commits'
|
import {DiffInfo} from './pr-collector/commits'
|
||||||
import {PullRequestInfo} from './pr-collector/pullRequests'
|
import {PullRequestData, PullRequestInfo} from './pr-collector/pullRequests'
|
||||||
import {BaseRepository} from './repositories/BaseRepository'
|
import {BaseRepository} from './repositories/BaseRepository'
|
||||||
|
|
||||||
export interface ReleaseNotesOptions {
|
export interface ReleaseNotesOptions {
|
||||||
|
|||||||
+368
-199
@@ -1,38 +1,34 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {Category, Configuration, Placeholder, Property} from './configuration'
|
import {Category, Configuration, Placeholder, PlaceholderGroup, Property} from './configuration'
|
||||||
import {createOrSet, haveCommonElementsArr, haveEveryElementsArr} from './utils'
|
import {createOrSet, groupPlaceholders, haveCommonElementsArr, haveEveryElementsArr, mergeMaps} from './utils'
|
||||||
import {
|
import {
|
||||||
CommentInfo,
|
CommentInfo,
|
||||||
EMPTY_COMMENT_INFO,
|
EMPTY_COMMENT_INFO,
|
||||||
EMPTY_PULL_REQUEST_INFO,
|
EMPTY_PULL_REQUEST_INFO,
|
||||||
|
PullRequestData,
|
||||||
PullRequestInfo,
|
PullRequestInfo,
|
||||||
retrieveProperty,
|
retrieveProperty,
|
||||||
sortPullRequests
|
sortPullRequests
|
||||||
} from './pr-collector/pullRequests'
|
} from './pr-collector/pullRequests'
|
||||||
import {DiffInfo} from './pr-collector/commits'
|
import {DiffInfo} from './pr-collector/commits'
|
||||||
import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils'
|
import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils'
|
||||||
import {Regex, RegexTransformer} from './pr-collector/types'
|
import {ChangelogStrings, GroupedTemplateContext, PrStrings, Regex, RegexTransformer, TemplateContext} from './pr-collector/types'
|
||||||
import {ReleaseNotesOptions} from './releaseNotesBuilder'
|
import {ReleaseNotesOptions} from './releaseNotesBuilder'
|
||||||
import {matchesRules} from './regexUtils'
|
import {matchesRules} from './regexUtils'
|
||||||
|
|
||||||
const EMPTY_MAP = new Map<string, string>()
|
|
||||||
let CLEAR = false
|
let CLEAR = false
|
||||||
|
|
||||||
export function clear(): void {
|
export function clear(): void {
|
||||||
CLEAR = true
|
CLEAR = true
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PullRequestData extends PullRequestInfo {
|
|
||||||
childPrs?: PullRequestInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], options: ReleaseNotesOptions): string {
|
export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], options: ReleaseNotesOptions): string {
|
||||||
core.startGroup('📦 Build changelog')
|
core.startGroup('📦 Build changelog')
|
||||||
|
|
||||||
let prs: PullRequestData[] = origPrs
|
let prs: PullRequestData[] = origPrs
|
||||||
if (prs.length === 0) {
|
if (prs.length === 0) {
|
||||||
core.warning(`⚠️ No pull requests found`)
|
core.warning(`⚠️ No pull requests found`)
|
||||||
const result = replaceEmptyTemplate(options.configuration.empty_template, options)
|
const result = renderEmptyChangelogTemplate(options.configuration.empty_template, options)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -123,31 +119,33 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep reference for the placeholder values
|
const groupedPlaceholders = groupPlaceholders(config.custom_placeholders || [])
|
||||||
const placeholders = new Map<string, Placeholder[]>()
|
|
||||||
for (const ph of config.custom_placeholders || []) {
|
const customPlaceholdersTemplateContext = new GroupedTemplateContext()
|
||||||
createOrSet(placeholders, ph.source, ph)
|
|
||||||
}
|
|
||||||
const placeholderPrMap = new Map<string, string[]>()
|
|
||||||
|
|
||||||
const validatedTransformers = validateTransformers(config.transformers)
|
const validatedTransformers = validateTransformers(config.transformers)
|
||||||
const transformedMap = new Map<PullRequestInfo, string>()
|
|
||||||
// convert PRs to their text representation
|
core.info(`ℹ️ Using ${validatedTransformers.length} transformers to rewrite content`)
|
||||||
for (const pr of prs) {
|
|
||||||
transformedMap.set(pr, transform(fillPrTemplate(pr, config.pr_template, placeholders, placeholderPrMap, config), validatedTransformers))
|
if (validatedTransformers.length > 0) {
|
||||||
|
for (const pr of prs) {
|
||||||
|
const prAsObject = pr as unknown as Record<string, unknown>
|
||||||
|
transformObject(prAsObject, validatedTransformers)
|
||||||
|
}
|
||||||
|
core.info(`✒️ Transformed ${prs.length} pull requests`)
|
||||||
}
|
}
|
||||||
core.info(`ℹ️ Used ${validatedTransformers.length} transformers to adjust message`)
|
|
||||||
core.info(`✒️ Wrote messages for ${prs.length} pull requests`)
|
const prInfoMap = buildInfoMapAndFillPlaceholderContext(
|
||||||
|
prs,
|
||||||
|
config.pr_template,
|
||||||
|
groupedPlaceholders,
|
||||||
|
customPlaceholdersTemplateContext,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
// bring PRs into the order of categories
|
// bring PRs into the order of categories
|
||||||
const categories = config.categories
|
const categories = config.categories
|
||||||
const ignoredLabels = config.ignore_labels
|
|
||||||
|
|
||||||
const flatCategories = flatten(config.categories)
|
const flatCategories = flatten(config.categories)
|
||||||
const categorizedPrs: string[] = []
|
|
||||||
const ignoredPrs: string[] = []
|
|
||||||
const openPrs: string[] = []
|
|
||||||
const uncategorizedPrs: string[] = []
|
|
||||||
|
|
||||||
// set-up the category object
|
// set-up the category object
|
||||||
for (const category of flatCategories) {
|
for (const category of flatCategories) {
|
||||||
@@ -156,8 +154,109 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prStrings = buildPrStringsAndFillCategoryEntries(prInfoMap, config.ignore_labels, categories, flatCategories)
|
||||||
|
core.info(`ℹ️ Ordered all pull requests into ${categories.length} categories`)
|
||||||
|
|
||||||
|
// serialize and provide the categorized content as json
|
||||||
|
const transformedCategorized = buildCategorizedOutput(flatCategories)
|
||||||
|
core.setOutput('categorized', JSON.stringify(transformedCategorized))
|
||||||
|
|
||||||
|
// construct final changelog
|
||||||
|
const changelogStrings = buildChangelogStrings(flatCategories, prStrings)
|
||||||
|
|
||||||
|
core.info(`✒️ Wrote ${changelogStrings.categorized.length} categorized pull requests down`)
|
||||||
|
core.info(`✒️ Wrote ${changelogStrings.uncategorized.length} non categorized pull requests down`)
|
||||||
|
core.info(`✒️ Wrote ${changelogStrings.open.length} open pull requests down`)
|
||||||
|
core.info(`✒️ Wrote ${changelogStrings.ignored.length} ignored pull requests down`)
|
||||||
|
|
||||||
|
core.setOutput('categorized_prs', changelogStrings.categorized.length)
|
||||||
|
core.setOutput('uncategorized_prs', changelogStrings.uncategorized.length)
|
||||||
|
core.setOutput('open_prs', changelogStrings.open.length)
|
||||||
|
core.setOutput('ignored_prs', changelogStrings.ignored.length)
|
||||||
|
|
||||||
|
// collect all contributors
|
||||||
|
const contributorsSet: Set<string> = new Set(prs.map(pr => `@${pr.author}`))
|
||||||
|
const contributorsArray = Array.from(contributorsSet)
|
||||||
|
const contributorsString = contributorsArray.join(', ')
|
||||||
|
const externalContributorString = contributorsArray.filter(value => value !== options.owner).join(', ')
|
||||||
|
core.setOutput('contributors', JSON.stringify(contributorsSet))
|
||||||
|
|
||||||
|
const releaseNotesTemplateContext = buildReleaseNotesTemplateContext(
|
||||||
|
changelogStrings,
|
||||||
|
contributorsString,
|
||||||
|
externalContributorString,
|
||||||
|
prStrings,
|
||||||
|
diffInfo,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
let renderedReleaseNotesTemplate = renderTemplateAndFillPlaceholderContext(
|
||||||
|
config.template,
|
||||||
|
releaseNotesTemplateContext,
|
||||||
|
groupedPlaceholders,
|
||||||
|
customPlaceholdersTemplateContext,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
renderedReleaseNotesTemplate = renderTemplateWithContext(renderedReleaseNotesTemplate, customPlaceholdersTemplateContext, config)
|
||||||
|
renderedReleaseNotesTemplate = cleanupPrPlaceholders(renderedReleaseNotesTemplate, groupedPlaceholders)
|
||||||
|
renderedReleaseNotesTemplate = cleanupPlaceholders(renderedReleaseNotesTemplate)
|
||||||
|
|
||||||
|
core.info(`ℹ️ Filled template`)
|
||||||
|
core.endGroup()
|
||||||
|
|
||||||
|
return renderedReleaseNotesTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInfoMapAndFillPlaceholderContext(
|
||||||
|
prData: PullRequestData[],
|
||||||
|
template: string,
|
||||||
|
groupedPlaceholders: Map<string, Placeholder[]>,
|
||||||
|
customPlaceholdersTemplateContext: GroupedTemplateContext,
|
||||||
|
config: Configuration
|
||||||
|
): Map<PullRequestInfo, string> {
|
||||||
|
const infoMap = new Map<PullRequestInfo, string>()
|
||||||
|
|
||||||
|
for (const pr of prData) {
|
||||||
|
const [prTemplateContext, prArrayTemplateContext] = buildPrTemplateContext(pr)
|
||||||
|
|
||||||
|
let renderedTemplate = template
|
||||||
|
|
||||||
|
renderedTemplate = renderTemplateAndFillPlaceholderContext(
|
||||||
|
renderedTemplate,
|
||||||
|
prArrayTemplateContext,
|
||||||
|
groupedPlaceholders,
|
||||||
|
customPlaceholdersTemplateContext,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
renderedTemplate = renderTemplateAndFillPlaceholderContext(
|
||||||
|
renderedTemplate,
|
||||||
|
prTemplateContext,
|
||||||
|
groupedPlaceholders,
|
||||||
|
customPlaceholdersTemplateContext,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
infoMap.set(pr, renderedTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return infoMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrStringsAndFillCategoryEntries(
|
||||||
|
prInfoMap: Map<PullRequestInfo, string>,
|
||||||
|
ignoredLabels: string[],
|
||||||
|
categories: Category[],
|
||||||
|
flatCategories: Category[]
|
||||||
|
): PrStrings {
|
||||||
|
const categorizedPrs: string[] = []
|
||||||
|
const ignoredPrs: string[] = []
|
||||||
|
const openPrs: string[] = []
|
||||||
|
const uncategorizedPrs: string[] = []
|
||||||
|
|
||||||
// bring elements in order
|
// bring elements in order
|
||||||
prLoop: for (const [pr, body] of transformedMap) {
|
prLoop: for (const [pr, body] of prInfoMap) {
|
||||||
if (
|
if (
|
||||||
haveCommonElementsArr(
|
haveCommonElementsArr(
|
||||||
ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')),
|
ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||||
@@ -184,7 +283,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
|||||||
if (!matchedOnce) {
|
if (!matchedOnce) {
|
||||||
// we allow to have pull requests included in an "uncategorized" category
|
// we allow to have pull requests included in an "uncategorized" category
|
||||||
for (const category of flatCategories) {
|
for (const category of flatCategories) {
|
||||||
const pullRequests = category.entries || []
|
category.entries = category.entries || []
|
||||||
if ((category.labels === undefined || category.labels.length === 0) && category.rules === undefined) {
|
if ((category.labels === undefined || category.labels.length === 0) && category.rules === undefined) {
|
||||||
// check if any exclude label matches for the "uncategorized" category
|
// check if any exclude label matches for the "uncategorized" category
|
||||||
if (category.exclude_labels !== undefined) {
|
if (category.exclude_labels !== undefined) {
|
||||||
@@ -194,7 +293,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
|||||||
pr.labels
|
pr.labels
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
pullRequests.push(body)
|
category.entries.push(body)
|
||||||
} else if (core.isDebug()) {
|
} else if (core.isDebug()) {
|
||||||
const excludeLabels = JSON.stringify(category.exclude_labels)
|
const excludeLabels = JSON.stringify(category.exclude_labels)
|
||||||
core.debug(
|
core.debug(
|
||||||
@@ -202,7 +301,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pullRequests.push(body)
|
category.entries.push(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -215,106 +314,126 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
|||||||
categorizedPrs.push(body)
|
categorizedPrs.push(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core.info(`ℹ️ Ordered all pull requests into ${categories.length} categories`)
|
|
||||||
|
|
||||||
// serialize and provide the categorized content as json
|
const prStrings: PrStrings = {
|
||||||
const transformedCategorized = {}
|
categorizedList: categorizedPrs,
|
||||||
for (const category of flatCategories) {
|
uncategorizedList: uncategorizedPrs,
|
||||||
Object.assign(transformedCategorized, {[category.key || category.title]: category.entries})
|
openList: openPrs,
|
||||||
|
ignoredList: ignoredPrs
|
||||||
}
|
}
|
||||||
core.setOutput('categorized', JSON.stringify(transformedCategorized))
|
|
||||||
|
|
||||||
// construct final changelog
|
return prStrings
|
||||||
let changelog = ''
|
}
|
||||||
|
|
||||||
|
function buildChangelogStrings(flatCategories: Category[], prStrings: PrStrings): ChangelogStrings {
|
||||||
|
const {categorizedList, uncategorizedList, openList, ignoredList} = prStrings
|
||||||
|
|
||||||
|
let changelogCategorized = ''
|
||||||
for (const category of flatCategories) {
|
for (const category of flatCategories) {
|
||||||
const pullRequests = category.entries || []
|
const pullRequests = category.entries || []
|
||||||
changelog = attachCategoryChangelog(changelog, category, pullRequests)
|
changelogCategorized += buildCategorizedChangelogString(category, pullRequests)
|
||||||
}
|
}
|
||||||
core.info(`✒️ Wrote ${categorizedPrs.length} categorized pull requests down`)
|
|
||||||
if (core.isDebug()) {
|
if (core.isDebug()) {
|
||||||
for (const pr of categorizedPrs) {
|
for (const pr of categorizedList) {
|
||||||
core.debug(` ${pr}`)
|
core.debug(` ${pr}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core.setOutput('categorized_prs', categorizedPrs.length)
|
|
||||||
|
|
||||||
let changelogUncategorized = ''
|
let changelogUncategorized = ''
|
||||||
for (const pr of uncategorizedPrs) {
|
for (const pr of uncategorizedList) {
|
||||||
changelogUncategorized = `${changelogUncategorized + pr}\n`
|
changelogUncategorized = `${changelogUncategorized + pr}\n`
|
||||||
}
|
}
|
||||||
core.info(`✒️ Wrote ${uncategorizedPrs.length} non categorized pull requests down`)
|
|
||||||
if (core.isDebug()) {
|
if (core.isDebug()) {
|
||||||
for (const pr of uncategorizedPrs) {
|
for (const pr of uncategorizedList) {
|
||||||
core.debug(` ${pr}`)
|
core.debug(` ${pr}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core.setOutput('uncategorized_prs', uncategorizedPrs.length)
|
|
||||||
|
|
||||||
let changelogOpen = ''
|
let changelogOpen = ''
|
||||||
if (openPrs.length > 0) {
|
if (openList.length > 0) {
|
||||||
for (const pr of openPrs) {
|
for (const pr of openList) {
|
||||||
changelogOpen = `${changelogOpen + pr}\n`
|
changelogOpen = `${changelogOpen + pr}\n`
|
||||||
}
|
}
|
||||||
core.info(`✒️ Wrote ${openPrs.length} open pull requests down`)
|
|
||||||
if (core.isDebug()) {
|
if (core.isDebug()) {
|
||||||
for (const pr of openPrs) {
|
for (const pr of openList) {
|
||||||
core.debug(` ${pr}`)
|
core.debug(` ${pr}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core.setOutput('open_prs', openPrs.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let changelogIgnored = ''
|
let changelogIgnored = ''
|
||||||
for (const pr of ignoredPrs) {
|
for (const pr of ignoredList) {
|
||||||
changelogIgnored = `${changelogIgnored + pr}\n`
|
changelogIgnored = `${changelogIgnored + pr}\n`
|
||||||
}
|
}
|
||||||
if (core.isDebug()) {
|
if (core.isDebug()) {
|
||||||
for (const pr of ignoredPrs) {
|
for (const pr of ignoredList) {
|
||||||
core.debug(` ${pr}`)
|
core.debug(` ${pr}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`)
|
|
||||||
|
|
||||||
// collect all contributors
|
const changelogStrings: ChangelogStrings = {
|
||||||
const contributorsSet: Set<String> = new Set()
|
categorized: changelogCategorized,
|
||||||
for (const pr of prs) {
|
uncategorized: changelogUncategorized,
|
||||||
contributorsSet.add(`@${pr.author}`)
|
open: changelogOpen,
|
||||||
|
ignored: changelogIgnored
|
||||||
}
|
}
|
||||||
const contributorsArray = Array.from(contributorsSet)
|
|
||||||
const contributorsString = contributorsArray.join(', ')
|
|
||||||
const externalContributorString = contributorsArray.filter(value => value !== options.owner).join(', ')
|
|
||||||
core.setOutput('contributors', JSON.stringify(contributorsSet))
|
|
||||||
|
|
||||||
// fill template
|
return changelogStrings
|
||||||
const placeholderMap = new Map<string, string>()
|
}
|
||||||
placeholderMap.set('CHANGELOG', changelog)
|
|
||||||
placeholderMap.set('UNCATEGORIZED', changelogUncategorized)
|
function buildReleaseNotesTemplateContext(
|
||||||
placeholderMap.set('OPEN', changelogOpen)
|
changelogStrings: ChangelogStrings,
|
||||||
placeholderMap.set('IGNORED', changelogIgnored)
|
contributorsString: string,
|
||||||
|
externalContributorString: string,
|
||||||
|
prStrings: PrStrings,
|
||||||
|
diffInfo: DiffInfo,
|
||||||
|
options: ReleaseNotesOptions
|
||||||
|
): TemplateContext {
|
||||||
|
const {
|
||||||
|
categorized: changelogCategorized,
|
||||||
|
uncategorized: changelogUncategorized,
|
||||||
|
open: changelogOpen,
|
||||||
|
ignored: changelogIgnored
|
||||||
|
} = changelogStrings
|
||||||
|
|
||||||
|
const {categorizedList, uncategorizedList, openList, ignoredList} = prStrings
|
||||||
|
|
||||||
|
let releaseNotesTemplateContext = new TemplateContext()
|
||||||
|
|
||||||
|
releaseNotesTemplateContext.set('CHANGELOG', changelogCategorized)
|
||||||
|
releaseNotesTemplateContext.set('UNCATEGORIZED', changelogUncategorized)
|
||||||
|
releaseNotesTemplateContext.set('OPEN', changelogOpen)
|
||||||
|
releaseNotesTemplateContext.set('IGNORED', changelogIgnored)
|
||||||
// fill special collected contributors
|
// fill special collected contributors
|
||||||
placeholderMap.set('CONTRIBUTORS', contributorsString)
|
releaseNotesTemplateContext.set('CONTRIBUTORS', contributorsString)
|
||||||
placeholderMap.set('EXTERNAL_CONTRIBUTORS', externalContributorString)
|
releaseNotesTemplateContext.set('EXTERNAL_CONTRIBUTORS', externalContributorString)
|
||||||
// fill other placeholders
|
// fill other placeholders
|
||||||
placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString())
|
releaseNotesTemplateContext.set('CATEGORIZED_COUNT', categorizedList.length.toString())
|
||||||
placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString())
|
releaseNotesTemplateContext.set('UNCATEGORIZED_COUNT', uncategorizedList.length.toString())
|
||||||
placeholderMap.set('OPEN_COUNT', openPrs.length.toString())
|
releaseNotesTemplateContext.set('OPEN_COUNT', openList.length.toString())
|
||||||
placeholderMap.set('IGNORED_COUNT', ignoredPrs.length.toString())
|
releaseNotesTemplateContext.set('IGNORED_COUNT', ignoredList.length.toString())
|
||||||
// code change placeholders
|
// code change placeholders
|
||||||
placeholderMap.set('CHANGED_FILES', diffInfo.changedFiles.toString())
|
releaseNotesTemplateContext.set('CHANGED_FILES', diffInfo.changedFiles.toString())
|
||||||
placeholderMap.set('ADDITIONS', diffInfo.additions.toString())
|
releaseNotesTemplateContext.set('ADDITIONS', diffInfo.additions.toString())
|
||||||
placeholderMap.set('DELETIONS', diffInfo.deletions.toString())
|
releaseNotesTemplateContext.set('DELETIONS', diffInfo.deletions.toString())
|
||||||
placeholderMap.set('CHANGES', diffInfo.changes.toString())
|
releaseNotesTemplateContext.set('CHANGES', diffInfo.changes.toString())
|
||||||
placeholderMap.set('COMMITS', diffInfo.commits.toString())
|
releaseNotesTemplateContext.set('COMMITS', diffInfo.commits.toString())
|
||||||
fillAdditionalPlaceholders(options, placeholderMap)
|
|
||||||
|
|
||||||
let transformedChangelog = config.template
|
const coreReleasesNotesContext = buildCoreReleaseNotesTemplateContext(options)
|
||||||
transformedChangelog = replacePlaceholders(transformedChangelog, EMPTY_MAP, placeholderMap, placeholders, placeholderPrMap, config)
|
|
||||||
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap, config)
|
releaseNotesTemplateContext = mergeMaps(releaseNotesTemplateContext, coreReleasesNotesContext)
|
||||||
transformedChangelog = cleanupPrPlaceholders(transformedChangelog, placeholders)
|
|
||||||
transformedChangelog = cleanupPlaceholders(transformedChangelog)
|
return releaseNotesTemplateContext
|
||||||
core.info(`ℹ️ Filled template`)
|
}
|
||||||
core.endGroup()
|
|
||||||
return transformedChangelog
|
function buildCategorizedOutput(flatCategories: Category[]): Record<string, string[]> {
|
||||||
|
const transformedCategorized = {}
|
||||||
|
|
||||||
|
for (const category of flatCategories) {
|
||||||
|
Object.assign(transformedCategorized, {[category.key || category.title]: category.entries})
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedCategorized
|
||||||
}
|
}
|
||||||
|
|
||||||
function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: string): boolean[] {
|
function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: string): boolean[] {
|
||||||
@@ -334,10 +453,11 @@ function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: st
|
|||||||
|
|
||||||
// if consumed we don't handle it anymore, as it was matched in a child, don't handle anymore
|
// if consumed we don't handle it anymore, as it was matched in a child, don't handle anymore
|
||||||
if (!consumed && !matched) {
|
if (!consumed && !matched) {
|
||||||
const pullRequests = category.entries || []
|
category.entries = category.entries || []
|
||||||
|
|
||||||
matched = matchesParent
|
matched = matchesParent
|
||||||
if (matched) {
|
if (matched) {
|
||||||
pullRequests.push(body) // if matched add the PR to the list
|
category.entries.push(body) // if matched add the PR to the list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matched && category.consume) {
|
if (matched && category.consume) {
|
||||||
@@ -401,156 +521,185 @@ function categorizePr(category: Category, pr: PullRequestInfo): boolean {
|
|||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachCategoryChangelog(changelog: string, category: Category, pullRequests: string[]): string {
|
function buildCategorizedChangelogString(category: Category, pullRequests: string[]): string {
|
||||||
|
let categorizedString = ''
|
||||||
|
|
||||||
if (pullRequests.length > 0 || hasChildWithEntries(category)) {
|
if (pullRequests.length > 0 || hasChildWithEntries(category)) {
|
||||||
if (category.title) {
|
if (category.title) {
|
||||||
changelog = `${changelog + category.title}\n\n`
|
categorizedString = `${categorizedString + category.title}\n\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pr of pullRequests) {
|
for (const pr of pullRequests) {
|
||||||
changelog = `${changelog + pr}\n`
|
categorizedString = `${categorizedString + pr}\n`
|
||||||
}
|
}
|
||||||
changelog = `${changelog}\n` // add space between sections
|
categorizedString = `${categorizedString}\n` // add space between sections
|
||||||
} else if (category.empty_content !== undefined) {
|
} else if (category.empty_content !== undefined) {
|
||||||
if (category.title) {
|
if (category.title) {
|
||||||
changelog = `${changelog + category.title}\n\n`
|
categorizedString = `${categorizedString + category.title}\n\n`
|
||||||
}
|
}
|
||||||
changelog = `${changelog + category.empty_content}\n\n`
|
categorizedString = `${categorizedString + category.empty_content}\n\n`
|
||||||
}
|
}
|
||||||
return changelog
|
return categorizedString
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceEmptyTemplate(template: string, options: ReleaseNotesOptions): string {
|
export function renderEmptyChangelogTemplate(template: string, options: ReleaseNotesOptions): string {
|
||||||
const placeholders = new Map<string, Placeholder[]>()
|
const placeholders = new Map<string, Placeholder[]>()
|
||||||
for (const ph of options.configuration.custom_placeholders || []) {
|
for (const ph of options.configuration.custom_placeholders || []) {
|
||||||
createOrSet(placeholders, ph.source, ph)
|
createOrSet(placeholders, ph.source, ph)
|
||||||
}
|
}
|
||||||
const placeholderMap = new Map<string, string>()
|
|
||||||
fillAdditionalPlaceholders(options, placeholderMap)
|
const releaseNotesTemplateContext = buildCoreReleaseNotesTemplateContext(options)
|
||||||
return replacePlaceholders(template, EMPTY_MAP, placeholderMap, placeholders, undefined, options.configuration)
|
|
||||||
|
const renderedEmptyChangelogTemplate = renderTemplateAndFillPlaceholderContext(
|
||||||
|
template,
|
||||||
|
releaseNotesTemplateContext,
|
||||||
|
placeholders,
|
||||||
|
undefined,
|
||||||
|
options.configuration
|
||||||
|
)
|
||||||
|
|
||||||
|
return renderedEmptyChangelogTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillAdditionalPlaceholders(
|
function buildCoreReleaseNotesTemplateContext(options: ReleaseNotesOptions): TemplateContext {
|
||||||
options: ReleaseNotesOptions,
|
const templateContext = new TemplateContext()
|
||||||
placeholderMap: Map<string, string> /* placeholderKey and original value */
|
|
||||||
): void {
|
templateContext.set('OWNER', options.owner)
|
||||||
placeholderMap.set('OWNER', options.owner)
|
templateContext.set('REPO', options.repo)
|
||||||
placeholderMap.set('REPO', options.repo)
|
templateContext.set('FROM_TAG', options.fromTag.name)
|
||||||
placeholderMap.set('FROM_TAG', options.fromTag.name)
|
templateContext.set('FROM_TAG_DATE', options.fromTag.date?.toISOString() || '')
|
||||||
placeholderMap.set('FROM_TAG_DATE', options.fromTag.date?.toISOString() || '')
|
templateContext.set('TO_TAG', options.toTag.name)
|
||||||
placeholderMap.set('TO_TAG', options.toTag.name)
|
templateContext.set('TO_TAG_DATE', options.toTag.date?.toISOString() || '')
|
||||||
placeholderMap.set('TO_TAG_DATE', options.toTag.date?.toISOString() || '')
|
|
||||||
const fromDate = options.fromTag.date
|
const fromDate = options.fromTag.date
|
||||||
const toDate = options.toTag.date
|
const toDate = options.toTag.date
|
||||||
if (fromDate !== undefined && toDate !== undefined) {
|
if (fromDate !== undefined && toDate !== undefined) {
|
||||||
placeholderMap.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '')
|
templateContext.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '')
|
||||||
} else {
|
} else {
|
||||||
placeholderMap.set('DAYS_SINCE', '')
|
templateContext.set('DAYS_SINCE', '')
|
||||||
}
|
}
|
||||||
placeholderMap.set(
|
templateContext.set(
|
||||||
'RELEASE_DIFF',
|
'RELEASE_DIFF',
|
||||||
`${options.repositoryUtils.homeUrl}/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}`
|
`${options.repositoryUtils.homeUrl}/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return templateContext
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillPrTemplate(
|
function buildPrTemplateContext(pr: PullRequestData): [TemplateContext, TemplateContext] {
|
||||||
pr: PullRequestData,
|
const prTemplateContext = new TemplateContext()
|
||||||
template: string,
|
|
||||||
placeholders: Map<string, Placeholder[]> /* placeholders to apply */,
|
prTemplateContext.set('NUMBER', pr.number.toString())
|
||||||
placeholderPrMap: Map<string, string[]> /* map to keep replaced placeholder values with their key */,
|
prTemplateContext.set('TITLE', pr.title)
|
||||||
configuration: Configuration
|
prTemplateContext.set('URL', pr.htmlURL)
|
||||||
): string {
|
prTemplateContext.set('STATUS', pr.status)
|
||||||
const arrayPlaceholderMap = new Map<string, string>()
|
prTemplateContext.set('CREATED_AT', pr.createdAt.toISOString())
|
||||||
fillReviewPlaceholders(arrayPlaceholderMap, 'REVIEWS', pr.reviews || [])
|
prTemplateContext.set('MERGED_AT', pr.mergedAt?.toISOString() || '')
|
||||||
fillChildPrPlaceholders(arrayPlaceholderMap, 'REFERENCED', pr.childPrs || [])
|
prTemplateContext.set('MERGE_SHA', pr.mergeCommitSha)
|
||||||
const placeholderMap = new Map<string, string>()
|
prTemplateContext.set('AUTHOR', pr.author)
|
||||||
placeholderMap.set('NUMBER', pr.number.toString())
|
prTemplateContext.set('AUTHOR_NAME', pr.authorName || '')
|
||||||
placeholderMap.set('TITLE', pr.title)
|
prTemplateContext.set('LABELS', [...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || '')
|
||||||
placeholderMap.set('URL', pr.htmlURL)
|
prTemplateContext.set('MILESTONE', pr.milestone || '')
|
||||||
placeholderMap.set('STATUS', pr.status)
|
prTemplateContext.set('BODY', pr.body)
|
||||||
placeholderMap.set('CREATED_AT', pr.createdAt.toISOString())
|
prTemplateContext.set('ASSIGNEES', pr.assignees?.join(', ') || '')
|
||||||
placeholderMap.set('MERGED_AT', pr.mergedAt?.toISOString() || '')
|
prTemplateContext.set('REVIEWERS', pr.requestedReviewers?.join(', ') || '')
|
||||||
placeholderMap.set('MERGE_SHA', pr.mergeCommitSha)
|
prTemplateContext.set('APPROVERS', pr.approvedReviewers?.join(', ') || '')
|
||||||
placeholderMap.set('AUTHOR', pr.author)
|
prTemplateContext.set('BRANCH', pr.branch || '')
|
||||||
placeholderMap.set('AUTHOR_NAME', pr.authorName || '')
|
prTemplateContext.set('BASE_BRANCH', pr.baseBranch)
|
||||||
placeholderMap.set('LABELS', [...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || '')
|
|
||||||
placeholderMap.set('MILESTONE', pr.milestone || '')
|
const prArrayTemplateContext = new TemplateContext()
|
||||||
placeholderMap.set('BODY', pr.body)
|
fillReviewPlaceholders(prArrayTemplateContext, 'REVIEWS', pr.reviews || [])
|
||||||
fillArrayPlaceholders(arrayPlaceholderMap, 'ASSIGNEES', pr.assignees || [])
|
fillChildPrPlaceholders(prArrayTemplateContext, 'REFERENCED', pr.childPrs || [])
|
||||||
placeholderMap.set('ASSIGNEES', pr.assignees?.join(', ') || '')
|
fillArrayPlaceholders(prArrayTemplateContext, 'ASSIGNEES', pr.assignees || [])
|
||||||
fillArrayPlaceholders(arrayPlaceholderMap, 'REVIEWERS', pr.requestedReviewers || [])
|
fillArrayPlaceholders(prArrayTemplateContext, 'REVIEWERS', pr.requestedReviewers || [])
|
||||||
placeholderMap.set('REVIEWERS', pr.requestedReviewers?.join(', ') || '')
|
fillArrayPlaceholders(prArrayTemplateContext, 'APPROVERS', pr.approvedReviewers || [])
|
||||||
fillArrayPlaceholders(arrayPlaceholderMap, 'APPROVERS', pr.approvedReviewers || [])
|
|
||||||
placeholderMap.set('APPROVERS', pr.approvedReviewers?.join(', ') || '')
|
return [prTemplateContext, prArrayTemplateContext]
|
||||||
placeholderMap.set('BRANCH', pr.branch || '')
|
|
||||||
placeholderMap.set('BASE_BRANCH', pr.baseBranch)
|
|
||||||
return replacePlaceholders(template, arrayPlaceholderMap, placeholderMap, placeholders, placeholderPrMap, configuration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function replacePlaceholders(
|
function renderTemplateAndFillPlaceholderContext(
|
||||||
template: string,
|
template: string,
|
||||||
arrayPlaceholderMap: Map<string, string> /* arrayPlaceholderKey and original value */,
|
templateContext: TemplateContext /* placeholderKey and original value */,
|
||||||
placeholderMap: Map<string, string> /* placeholderKey and original value */,
|
customPlaceholders: PlaceholderGroup /* placeholders to apply */,
|
||||||
placeholders: Map<string, Placeholder[]> /* placeholders to apply */,
|
customPlaceholdersTemplateContext: GroupedTemplateContext | undefined /* map to keep replaced placeholder values with their key */,
|
||||||
placeholderPrMap: Map<string, string[]> | undefined /* map to keep replaced placeholder values with their key */,
|
|
||||||
configuration: Configuration
|
configuration: Configuration
|
||||||
): string {
|
): string {
|
||||||
let transformed = template
|
let transformed = template
|
||||||
|
|
||||||
// replace array placeholders first
|
const trimValues = configuration.trim_values
|
||||||
for (const [key, value] of arrayPlaceholderMap) {
|
|
||||||
transformed = handlePlaceholder(transformed, key, value, placeholders, placeholderPrMap, configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace traditional placeholders
|
// replace traditional placeholders
|
||||||
for (const [key, value] of placeholderMap) {
|
for (const [key, value] of templateContext) {
|
||||||
transformed = handlePlaceholder(transformed, key, value, placeholders, placeholderPrMap, configuration)
|
transformed = transformed.replaceAll(`#{{${key}}}`, trimValues ? value.trim() : value)
|
||||||
|
|
||||||
|
const extractedValues = extractPlaceholderValuesAndFillPlaceholderContext(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
customPlaceholders,
|
||||||
|
customPlaceholdersTemplateContext
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [placeholderName, extractedValue] of extractedValues) {
|
||||||
|
transformed = transformed.replaceAll(`#{{${placeholderName}}}`, trimValues ? extractedValue.trim() : extractedValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformed
|
return transformed
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePlaceholder(
|
function extractPlaceholderValuesAndFillPlaceholderContext(
|
||||||
template: string,
|
|
||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: string,
|
||||||
placeholders: Map<string, Placeholder[]> /* placeholders to apply */,
|
customPlaceholders: PlaceholderGroup,
|
||||||
placeholderPrMap: Map<string, string[]> | undefined /* map to keep replaced placeholder values with their key */,
|
customPlaceholdersTemplateContext: GroupedTemplateContext | undefined
|
||||||
configuration: Configuration
|
): TemplateContext {
|
||||||
): string {
|
// Replace custom placeholders
|
||||||
let transformed = template.replaceAll(`#{{${key}}}`, configuration.trim_values ? value.trim() : value)
|
const placeholdersForKey = customPlaceholders.get(key)
|
||||||
// replace custom placeholders
|
|
||||||
const phs = placeholders.get(key)
|
|
||||||
if (phs) {
|
|
||||||
for (const placeholder of phs) {
|
|
||||||
const transformer = validateRegex(placeholder.transformer)
|
|
||||||
if (transformer?.pattern) {
|
|
||||||
const extractedValue = transformStringToOptionalValue(value, transformer)
|
|
||||||
// note: `.replace` will return the full string again if there was no match
|
|
||||||
// note: This is mostly backwards compatibility
|
|
||||||
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
|
|
||||||
if (placeholderPrMap) {
|
|
||||||
createOrSet(placeholderPrMap, placeholder.name, extractedValue)
|
|
||||||
}
|
|
||||||
transformed = transformed.replaceAll(
|
|
||||||
`#{{${placeholder.name}}}`,
|
|
||||||
configuration.trim_values ? extractedValue.trim() : extractedValue
|
|
||||||
)
|
|
||||||
|
|
||||||
if (core.isDebug()) {
|
const extractedValues = new TemplateContext()
|
||||||
core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`)
|
|
||||||
}
|
if (!placeholdersForKey) {
|
||||||
} else if (core.isDebug() && extractedValue === value) {
|
return extractedValues
|
||||||
core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`)
|
}
|
||||||
}
|
|
||||||
|
for (const placeholder of placeholdersForKey) {
|
||||||
|
const extractedValue = extractTransformedValue(value, placeholder)
|
||||||
|
|
||||||
|
if (extractedValue) {
|
||||||
|
extractedValues.set(placeholder.name, extractedValue)
|
||||||
|
|
||||||
|
if (customPlaceholdersTemplateContext) {
|
||||||
|
createOrSet(customPlaceholdersTemplateContext, placeholder.name, extractedValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transformed
|
|
||||||
|
return extractedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTransformedValue(value: string, placeholder: Placeholder): string | undefined {
|
||||||
|
const transformer = validateRegex(placeholder.transformer)
|
||||||
|
|
||||||
|
if (!transformer?.pattern) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedValue = transformStringToOptionalValue(value, transformer)
|
||||||
|
|
||||||
|
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
|
||||||
|
if (core.isDebug()) {
|
||||||
|
core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`)
|
||||||
|
}
|
||||||
|
return extractedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (core.isDebug() && extractedValue === value) {
|
||||||
|
core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillArrayPlaceholders(
|
function fillArrayPlaceholders(
|
||||||
placeholderMap: Map<string, string> /* placeholderKey and original value */,
|
placeholderMap: TemplateContext /* placeholderKey and original value */,
|
||||||
key: string,
|
key: string,
|
||||||
values: string[]
|
values: string[]
|
||||||
): void {
|
): void {
|
||||||
@@ -562,7 +711,7 @@ function fillArrayPlaceholders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fillReviewPlaceholders(
|
function fillReviewPlaceholders(
|
||||||
placeholderMap: Map<string, string> /* placeholderKey and original value */,
|
placeholderMap: TemplateContext /* placeholderKey and original value */,
|
||||||
parentKey: string,
|
parentKey: string,
|
||||||
values: CommentInfo[]
|
values: CommentInfo[]
|
||||||
): void {
|
): void {
|
||||||
@@ -580,7 +729,7 @@ function fillReviewPlaceholders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fillChildPrPlaceholders(
|
function fillChildPrPlaceholders(
|
||||||
placeholderMap: Map<string, string> /* placeholderKey and original value */,
|
placeholderMap: TemplateContext /* placeholderKey and original value */,
|
||||||
parentKey: string,
|
parentKey: string,
|
||||||
values: PullRequestInfo[]
|
values: PullRequestInfo[]
|
||||||
): void {
|
): void {
|
||||||
@@ -597,13 +746,13 @@ function fillChildPrPlaceholders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replacePrPlaceholders(
|
function renderTemplateWithContext(
|
||||||
template: string,
|
template: string,
|
||||||
placeholderPrMap: Map<string, string[]> /* map with all pr related custom placeholder values */,
|
templateContext: GroupedTemplateContext /* map with all pr related custom placeholder values */,
|
||||||
configuration: Configuration
|
configuration: Configuration
|
||||||
): string {
|
): string {
|
||||||
let transformed = template
|
let transformed = template
|
||||||
for (const [key, values] of placeholderPrMap) {
|
for (const [key, values] of templateContext) {
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
transformed = transformed.replaceAll(`#{{${key}[${i}]}}`, configuration.trim_values ? values[i].trim() : values[i])
|
transformed = transformed.replaceAll(`#{{${key}[${i}]}}`, configuration.trim_values ? values[i].trim() : values[i])
|
||||||
}
|
}
|
||||||
@@ -612,7 +761,7 @@ function replacePrPlaceholders(
|
|||||||
return transformed
|
return transformed
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupPrPlaceholders(template: string, placeholders: Map<string, Placeholder[]>): string {
|
function cleanupPrPlaceholders(template: string, placeholders: PlaceholderGroup): string {
|
||||||
let transformed = template
|
let transformed = template
|
||||||
for (const [, phs] of placeholders) {
|
for (const [, phs] of placeholders) {
|
||||||
for (const ph of phs) {
|
for (const ph of phs) {
|
||||||
@@ -630,6 +779,27 @@ function cleanupPlaceholders(template: string): string {
|
|||||||
return transformed
|
return transformed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformObject(obj: Record<string, unknown>, transformers: RegexTransformer[]): void {
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.every(item => typeof item === 'string')) {
|
||||||
|
// If the array contains only strings, apply the transformation to each string
|
||||||
|
obj[key] = value.map(item => transform(item, transformers))
|
||||||
|
} else {
|
||||||
|
// If the array contains objects, recursively apply the transformation to each object
|
||||||
|
for (const child of value) {
|
||||||
|
transformObject(child as Record<string, unknown>, transformers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
obj[key] = transform(value, transformers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function transform(filled: string, transformers: RegexTransformer[]): string {
|
function transform(filled: string, transformers: RegexTransformer[]): string {
|
||||||
if (transformers.length === 0) {
|
if (transformers.length === 0) {
|
||||||
return filled
|
return filled
|
||||||
@@ -644,8 +814,7 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateTransformers(specifiedTransformers: Regex[]): RegexTransformer[] {
|
function validateTransformers(specifiedTransformers: Regex[]): RegexTransformer[] {
|
||||||
const transformers = specifiedTransformers
|
return specifiedTransformers
|
||||||
return transformers
|
|
||||||
.map(transformer => {
|
.map(transformer => {
|
||||||
return validateRegex(transformer)
|
return validateRegex(transformer)
|
||||||
})
|
})
|
||||||
|
|||||||
+27
-2
@@ -1,7 +1,7 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {Configuration, DefaultCommitConfiguration, DefaultConfiguration} from './configuration'
|
import {Configuration, DefaultCommitConfiguration, DefaultConfiguration, Placeholder} from './configuration'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import {DiffInfo} from './pr-collector/commits'
|
import {DiffInfo} from './pr-collector/commits'
|
||||||
import {PullRequestInfo} from './pr-collector/pullRequests'
|
import {PullRequestInfo} from './pr-collector/pullRequests'
|
||||||
@@ -123,7 +123,7 @@ export function resolveMode(mode: string | undefined, commitMode: boolean): 'PR'
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode !== undefined) {
|
if (mode !== undefined) {
|
||||||
const upperCaseMode = mode.toUpperCase();
|
const upperCaseMode = mode.toUpperCase()
|
||||||
if (upperCaseMode === 'COMMIT') {
|
if (upperCaseMode === 'COMMIT') {
|
||||||
return 'COMMIT'
|
return 'COMMIT'
|
||||||
} else if (upperCaseMode === 'HYBRID') {
|
} else if (upperCaseMode === 'HYBRID') {
|
||||||
@@ -243,6 +243,27 @@ export function createOrSet<T>(map: Map<string, T[]>, key: string, value: T): vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups an array of Placeholder objects by their source attribute.
|
||||||
|
*
|
||||||
|
* @param {Placeholder[]} placeholders - An array of Placeholder objects to be grouped.
|
||||||
|
* @returns {Map<string, Placeholder[]>} A map where the key is the source attribute of the Placeholder objects,
|
||||||
|
* and the value is an array of Placeholder objects that share the same source attribute.
|
||||||
|
*/
|
||||||
|
export function groupPlaceholders(placeholders: Placeholder[]): Map<string, Placeholder[]> {
|
||||||
|
const map = new Map<string, Placeholder[]>()
|
||||||
|
for (const ph of placeholders) {
|
||||||
|
const key = ph.source
|
||||||
|
const entry = map.get(key)
|
||||||
|
if (entry === undefined) {
|
||||||
|
map.set(key, [ph])
|
||||||
|
} else {
|
||||||
|
entry.push(ph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
export function haveCommonElements(arr1: string[], arr2: Set<string>): boolean {
|
export function haveCommonElements(arr1: string[], arr2: Set<string>): boolean {
|
||||||
return arr1.some(item => arr2.has(item))
|
return arr1.some(item => arr2.has(item))
|
||||||
}
|
}
|
||||||
@@ -258,3 +279,7 @@ export function haveEveryElements(arr1: string[], arr2: Set<string>): boolean {
|
|||||||
export function haveEveryElementsArr(arr1: string[], arr2: string[]): boolean {
|
export function haveEveryElementsArr(arr1: string[], arr2: string[]): boolean {
|
||||||
return haveEveryElements(arr1, new Set(arr2))
|
return haveEveryElements(arr1, new Set(arr2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeMaps<T, U>(map1: Map<T, U>, map2: Map<T, U>): Map<T, U> {
|
||||||
|
return new Map<T, U>([...map1, ...map2])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user