Merge pull request #1390 from mikepenz/develop

dev -> main
This commit is contained in:
Mike Penz
2024-11-02 10:37:41 +01:00
committed by GitHub
20 changed files with 2436 additions and 2856 deletions
+7 -8
View File
@@ -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
View File
@@ -99,4 +99,5 @@ __tests__/runner/*
lib/**/* lib/**/*
lib lib
src/pr-collector/dist src/pr-collector/dist
.idea
+9 -1
View File
@@ -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
}
}
]
} }
+6 -6
View File
@@ -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>
+1 -1
View File
@@ -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)
} }
}) })
+2 -2
View File
@@ -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,
+3 -1
View File
@@ -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`
) )
}) })
Generated Vendored
+1425 -1840
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -14
View File
@@ -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
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+542 -742
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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
View File
@@ -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
} }
+8 -4
View File
@@ -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'
+17
View File
@@ -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
View File
@@ -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 => {
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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])
}