Merge pull request #1348 from mikepenz/develop

dev -> main
This commit is contained in:
Mike Penz
2024-07-26 09:35:54 +02:00
committed by GitHub
39 changed files with 7209 additions and 2815 deletions
+1
View File
@@ -177,6 +177,7 @@ jobs:
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: "configs/configuration_repo.json"
ignorePreReleases: ${{ !contains(github.ref, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+296 -185
View File
@@ -62,37 +62,6 @@ Specify the action as part of your GitHub actions workflow:
uses: mikepenz/release-changelog-builder-action@{latest-release}
```
### Action outputs
After action execution it will return the `changelog` and additional information as step output. You can use it in any follow-up step by referencing the output by referencing it via the id of the step. For example `build_changelog`.
```yml
# ${{steps.{CHANGELOG_STEP_ID}.outputs.changelog}}
${{steps.build_changelog.outputs.changelog}}
```
A full set list of possible output values for this action.
| **Output** | **Description** |
|-----------------------|---------------------------------------------------------------------------------------------------------------------------|
| `outputs.changelog` | The built release changelog built from the merged pull requests |
| `outputs.owner` | Specifies the owner of the repository processed |
| `outputs.repo` | Describes the repository name, which was processed |
| `outputs.fromTag` | Defines the `fromTag` which describes the lower bound to process pull requests for |
| `outputs.toTag` | Defines the `toTag` which describes the upper bound to process pull request for |
| `outputs.failed` | Defines if there was an issue with the action run, and the changelog may not have been generated correctly. [true, false] |
| `outputs.pull_requests` | Defines a `,` joined array with all PR IDs associated with the generated changelog. |
| `outputs.categorized_prs` | Count of PRs which were successfully categorized as part of the action. |
| `outputs.open_prs` | Count of open PRs. Only fetched if `includeOpen` is enabled. |
| `outputs.uncategorized_prs` | Count of PRs which were not categorized as part of the action. |
| `outputs.changed_files` | Count of changed files in this release. |
| `outputs.additions` | Count of code additions in this release (lines). |
| `outputs.deletions` | Count of code deletions in this release (lines). |
| `outputs.changes` | Total count of changes in this release (lines). |
| `outputs.commits` | Count of commits which have been added in this release. |
| `outputs.categorized` | The categorized pull requests used to build the changelog as serialized JSON. |
| `outputs.cache` | The file pointing to the cache for the current fetched data. Can be provided to another action step. |
## Full Sample 🖥️
Below is a complete example showcasing how to define a build, which is executed when tagging the project. It consists of:
@@ -120,7 +89,7 @@ jobs:
steps:
- name: Build Changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@v3
uses: mikepenz/release-changelog-builder-action@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -143,7 +112,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Build Changelog
uses: mikepenz/release-changelog-builder-action@v3
uses: mikepenz/release-changelog-builder-action@v4
with:
configurationJson: |
{
@@ -166,6 +135,137 @@ jobs:
</p>
</details>
<details><summary><b>Example Commit Mode w/ Configuration</b></summary>
<p>
```yml
jobs:
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Build Changelog
uses: mikepenz/release-changelog-builder-action@v4
with:
commitMode: true
configurationJson: |
{
"template": "#{{CHANGELOG}}",
"categories": [
{
"title": "## Feature",
"labels": ["feat", "feature"]
},
{
"title": "## Fix",
"labels": ["fix", "bug"]
},
{
"title": "## Other",
"labels": []
}
],
"label_extractor": [
{
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)",
"target": "$1"
}
],
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
This example defines a regex to extract the label from the commit message. Handling flags from the [Conventional Commit Standards](https://www.conventionalcommits.org/en/v1.0.0/).
</p>
</details>
## Action Inputs/Outputs
### Action inputs
Depending on the usecase additional settings can be provided to the action
```yml
- name: "Complex Configuration"
id: build_changelog
if: startsWith(github.ref, 'refs/tags/')
uses: mikepenz/release-changelog-builder-action@{latest-release}
with:
configuration: "configuration_complex.json"
owner: "mikepenz"
repo: "release-changelog-builder-action"
ignorePreReleases: "false"
fromTag: "0.3.0"
toTag: "0.5.0"
token: ${{ secrets.PAT }}
```
> [!NOTE]
> All input values are optional. It is only required to provide the `token` either via the input, or as `env` variable.
| **Input** | **Description** |
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `configurationJson` | Provide the configuration directly via the build `yml` file. Please note that `${{}}` has to be written as `#{{}}` within the `yml` file. |
| `configuration` | Relative path, to the `configuration.json` file, providing additional configurations |
| `outputFile` | Optional relative path to a file to store the resulting changelog in. |
| `owner` | The owner of the repository to generate the changelog for |
| `repo` | Name of the repository we want to process |
| `fromTag` | Defines the 'start' from where the changelog will consider merged pull requests (can be a tag or a valid git ref) |
| `toTag` | Defines until which tag the changelog will consider merged pull requests (can be a tag or a valid git ref) |
| `path` | Allows to specify an alternative sub directory, to use as base |
| `token` | Alternative config to specify token. You should prefer `env.GITHUB_TOKEN` instead though |
| `baseUrl` | Alternative config to specify base url for GitHub Enterprise authentication. Default value set to `https://api.github.com` |
| `includeOpen` | Enables to also fetch currently open PRs. Default: false |
| `ignorePreReleases` | Allows to ignore pre-releases for changelog generation (E.g. for 1.0.1... 1.0.0-rc02 <- ignore, 1.0.0 <- pick). Only used if `fromTag` was not specified. Default: false |
| `failOnError` | Defines if the action will result in a build failure if problems occurred. Default: false |
| `fetchViaCommits` | Enables PRs to get fetched via the commits identified between from->to tag. This will do 1 API request per commit -> Best for scenarios with squash merges | Or shorter from->to diffs (< 10 commits) | Also effective for shorters diffs for very old PRs. Default: false |
| `fetchReviewers` | Will enable fetching the users/reviewers who approved the PR. Default: false |
| `fetchReleaseInformation` | Will enable fetching additional release information from tags. Default: false |
| `fetchReviews` | Will enable fetching the reviews on of the PR. Default: false |
| `commitMode` | Special configuration for projects which work without PRs. Uses commit messages as changelog. This mode looses access to information only available for PRs. Default: false |
| `exportCache` | Will enable exporting the fetched PR information to a cache, which can be re-used by later runs. Default: false |
| `exportOnly` | When enabled, will result in only exporting the cache, without genearting a changelog. Default: false (Requires `exportCache` to be enabled) |
| `cache` | The file path to write/read the cache to/from. |
> [!WARNING]
> `${{ secrets.GITHUB_TOKEN }}` only grants rights to the current repository, for other repositories please use a PAT (Personal Access Token).
### Action outputs
After action execution it will return the `changelog` and additional information as step output. You can use it in any follow-up step by referencing the output by referencing it via the id of the step. For example `build_changelog`.
```yml
# ${{steps.{CHANGELOG_STEP_ID}.outputs.changelog}}
${{steps.build_changelog.outputs.changelog}}
```
A full set list of possible output values for this action.
| **Output** | **Description** |
|-----------------------------|---------------------------------------------------------------------------------------------------------------------------|
| `outputs.changelog` | The built release changelog built from the merged pull requests |
| `outputs.owner` | Specifies the owner of the repository processed |
| `outputs.repo` | Describes the repository name, which was processed |
| `outputs.fromTag` | Defines the `fromTag` which describes the lower bound to process pull requests for |
| `outputs.toTag` | Defines the `toTag` which describes the upper bound to process pull request for |
| `outputs.failed` | Defines if there was an issue with the action run, and the changelog may not have been generated correctly. [true, false] |
| `outputs.pull_requests` | Defines a `,` joined array with all PR IDs associated with the generated changelog. |
| `outputs.categorized_prs` | Count of PRs which were successfully categorized as part of the action. |
| `outputs.open_prs` | Count of open PRs. Only fetched if `includeOpen` is enabled. |
| `outputs.uncategorized_prs` | Count of PRs which were not categorized as part of the action. |
| `outputs.changed_files` | Count of changed files in this release. |
| `outputs.additions` | Count of code additions in this release (lines). |
| `outputs.deletions` | Count of code deletions in this release (lines). |
| `outputs.changes` | Total count of changes in this release (lines). |
| `outputs.commits` | Count of commits which have been added in this release. |
| `outputs.contributors` | The contributors of this release. Based on PR authors only. |
| `outputs.categorized` | The categorized pull requests used to build the changelog as serialized JSON. |
| `outputs.cache` | The file pointing to the cache for the current fetched data. Can be provided to another action step. |
## Customization 🖍️
### Note
@@ -186,27 +286,60 @@ jobs:
### Configuration
The action supports flexible configuration options to modify vast areas of its behavior. To do so, provide the configuration file to the workflow using the `configuration` setting.
The action supports flexible and extensive configuration options, to finetune it for the specific projects needs. To do so provide the configuration either directly to the step via `configurationJson` or as file via the `configuration`.
</p>
</details>
<details><summary><b>Configuration in .yml</b></summary>
<p>
```yml
- name: Build Changelog
uses: mikepenz/release-changelog-builder-action@v4
with:
configurationJson: |
{
"template": "#{{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n#{{UNCATEGORIZED}}\n</details>",
"categories": [
{
"title": "## 💬 Other",
"labels": ["other"]
}
]
}
```
</p>
</details>
</p>
</details>
<details><summary><b>Configuration as json file</b></summary>
<p>
```yml
- name: "Build Changelog"
uses: mikepenz/release-changelog-builder-action@{latest-release}
with:
configuration: "configuration.json"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
> [!NOTE]
> Defaults for the configuration can be found in the [configuration.ts](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/src/configuration.ts)
> [!WARNING]
> It is required to have a `checkout` step prior to the changelog step if `configuration` is used, to allow the action to discover the configuration file. Use `configurationJson` as alternative.
</p>
</details>
> [!NOTE]
> Defaults for the configuration can be found in the [configuration.ts](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/src/configuration.ts)
> [!NOTE]
> It is possible to provide the configuration as file and as json via the yml file. The order of config values used: `configurationJson` > `configuration` > `DefaultConfiguration`.
This configuration is a `JSON` in the following format. (The below showcases *example* configurations for all possible options. In most scenarios most of the settings will not be needed, and the defaults will be appropiate.)
The configuration is a `JSON` in the following format. (The below showcases *example* configurations for all possible options. In most scenarios most of the settings will not be needed, and the defaults will be appropiate.)
```json
{
@@ -306,54 +439,35 @@ Any section of the configuration can be omitted to have defaults apply.
Please see the [Configuration Specification](#configuration-specification) for detailed descriptions on the offered configuration options.
### Advanced workflow specification
For advanced use cases additional settings can be provided to the action
### Template placeholders
```yml
- name: "Complex Configuration"
id: build_changelog
if: startsWith(github.ref, 'refs/tags/')
uses: mikepenz/release-changelog-builder-action@{latest-release}
with:
configuration: "configuration_complex.json"
owner: "mikepenz"
repo: "release-changelog-builder-action"
ignorePreReleases: "false"
fromTag: "0.3.0"
toTag: "0.5.0"
token: ${{ secrets.PAT }}
```
Table of supported placeholders allowed to be used in the `template` and `empty_template` (only supports placeholder marked for empty) configuration, to give additional control on defining the contents of the release notes / changelog.
> [!NOTE]
> All input values are optional. It is only required to provide the `token` either via the input, or as `env` variable.
| **Input** | **Description** |
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `configurationJson` | Provide the configuration directly via the build `yml` file. Please note that `${{}}` has to be written as `#{{}}` within the `yml` file. |
| `configuration` | Relative path, to the `configuration.json` file, providing additional configurations |
| `outputFile` | Optional relative path to a file to store the resulting changelog in. |
| `owner` | The owner of the repository to generate the changelog for |
| `repo` | Name of the repository we want to process |
| `fromTag` | Defines the 'start' from where the changelog will consider merged pull requests (can be a tag or a valid git ref) |
| `toTag` | Defines until which tag the changelog will consider merged pull requests (can be a tag or a valid git ref) |
| `path` | Allows to specify an alternative sub directory, to use as base |
| `token` | Alternative config to specify token. You should prefer `env.GITHUB_TOKEN` instead though |
| `baseUrl` | Alternative config to specify base url for GitHub Enterprise authentication. Default value set to `https://api.github.com` |
| `includeOpen` | Enables to also fetch currently open PRs. Default: false |
| `ignorePreReleases` | Allows to ignore pre-releases for changelog generation (E.g. for 1.0.1... 1.0.0-rc02 <- ignore, 1.0.0 <- pick). Only used if `fromTag` was not specified. Default: false |
| `failOnError` | Defines if the action will result in a build failure if problems occurred. Default: false |
| `fetchViaCommits` | Enables PRs to get fetched via the commits identified between from->to tag. This will do 1 API request per commit -> Best for scenarios with squash merges | Or shorter from->to diffs (< 10 commits) | Also effective for shorters diffs for very old PRs. Default: false |
| `fetchReviewers` | Will enable fetching the users/reviewers who approved the PR. Default: false |
| `fetchReleaseInformation` | Will enable fetching additional release information from tags. Default: false |
| `fetchReviews` | Will enable fetching the reviews on of the PR. Default: false |
| `commitMode` | Special configuration for projects which work without PRs. Uses commit messages as changelog. This mode looses access to information only available for PRs. Default: false |
| `exportCache` | Will enable exporting the fetched PR information to a cache, which can be re-used by later runs. Default: false |
| `exportOnly` | When enabled, will result in only exporting the cache, without genearting a changelog. Default: false (Requires `exportCache` to be enabled) |
| `cache` | The file path to write/read the cache to/from. |
> [!WARNING]
> `${{ secrets.GITHUB_TOKEN }}` only grants rights to the current repository, for other repositories please use a PAT (Personal Access Token).
| **Placeholder** | **Description** | **Empty** |
|----------------------------|----------------------------------------------------------------------------------------------------|:---------:|
| `#{{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration | |
| `#{{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories | |
| `#{{OPEN}}` | All open pull requests. Will only be fetched if `includeOpen` is enabled. | |
| `#{{IGNORED}}` | All pull requests defining labels matching the `ignore_labels` configuration | |
| `#{{OWNER}}` | Describes the owner of the repository the changelog was generated for | x |
| `#{{REPO}}` | The repository name of the repo the changelog was generated for | x |
| `#{{FROM_TAG}}` | Defines the 'start' from where the changelog did consider merged pull requests | x |
| `#{{FROM_TAG_DATE}}` | Defines the date at which the 'start' tag was created. Requires `fetchReleaseInformation`. | x |
| `#{{TO_TAG}}` | Defines until which tag the changelog did consider merged pull requests | x |
| `#{{TO_TAG_DATE}}` | Defines the date at which the 'until' tag was created. Requires `fetchReleaseInformation`. | x |
| `#{{RELEASE_DIFF}}` | Introduces a link to the full diff between from tag and to tag releases | x |
| `#{{CHANGED_FILES}}` | The count of changed files. | |
| `#{{ADDITIONS}}` | The count of code additions (lines). | |
| `#{{DELETIONS}}` | The count of code deletions (lines). | |
| `#{{CHANGES}}` | The count of total changes (lines). | |
| `#{{COMMITS}}` | The count of commits in this release. | |
| `#{{CONTRIBUTORS}}` | The contributors of this release. Based on PR Authors only. | |
| `#{{CATEGORIZED_COUNT}}` | The count of PRs which were categorized | |
| `#{{UNCATEGORIZED_COUNT}}` | The count of PRs and changes which were not categorized. No label overlapping with category labels | |
| `#{{OPEN_COUNT}}` | The count of open PRs. Will only be fetched if `includeOpen` is configured. | |
| `#{{IGNORED_COUNT}}` | The count of PRs and changes which were specifically ignored from the changelog. | |
| `#{{DAYS_SINCE}}` | Days between the 2 releases. Requires `fetchReleaseInformation` to be enabled. | x |
### PR Template placeholders
@@ -393,7 +507,7 @@ When using `*` values are joined by `,`.
| `#{{REVIEWERS[*]}}` | GitHub Login names of specified reviewers. Requires `fetchReviewers` to be enabled. |
| `#{{APPROVERS[*]}}` | GitHub Login names of users who approved the PR. |
Additionally there are special array placeholders like `REVIEWS` which allows access to it's properties via
Additionally, there are special array placeholders like `REVIEWS` which allows access to it's properties via
`(KEY)[(*/index)].(property)`.
For example: `REVIEWS[*].author` or `REVIEWS[*].body`
@@ -417,35 +531,6 @@ Similar to `REVIEWS`, `REFERENCED` PRs also offer special placeholders.
</p>
</details>
### Template placeholders
Table of supported placeholders allowed to be used in the `template` and `empty_template` (only supports placeholder marked for empty) configuration, to give additional control on defining the contents of the release notes / changelog.
| **Placeholder** | **Description** | **Empty** |
|----------------------------|----------------------------------------------------------------------------------------------------|:---------:|
| `#{{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration | |
| `#{{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories | |
| `#{{OPEN}}` | All open pull requests. Will only be fetched if `includeOpen` is enabled. | |
| `#{{IGNORED}}` | All pull requests defining labels matching the `ignore_labels` configuration | |
| `#{{OWNER}}` | Describes the owner of the repository the changelog was generated for | x |
| `#{{REPO}}` | The repository name of the repo the changelog was generated for | x |
| `#{{FROM_TAG}}` | Defines the 'start' from where the changelog did consider merged pull requests | x |
| `#{{FROM_TAG_DATE}}` | Defines the date at which the 'start' tag was created. Requires `fetchReleaseInformation`. | x |
| `#{{TO_TAG}}` | Defines until which tag the changelog did consider merged pull requests | x |
| `#{{TO_TAG_DATE}}` | Defines the date at which the 'until' tag was created. Requires `fetchReleaseInformation`. | x |
| `#{{RELEASE_DIFF}}` | Introduces a link to the full diff between from tag and to tag releases | x |
| `#{{CHANGED_FILES}}` | The count of changed files. | |
| `#{{ADDITIONS}}` | The count of code additions (lines). | |
| `#{{DELETIONS}}` | The count of code deletions (lines). | |
| `#{{CHANGES}}` | The count of total changes (lines). | |
| `#{{COMMITS}}` | The count of commits in this release. | |
| `#{{CATEGORIZED_COUNT}}` | The count of PRs which were categorized | |
| `#{{UNCATEGORIZED_COUNT}}` | The count of PRs and changes which were not categorized. No label overlapping with category labels | |
| `#{{OPEN_COUNT}}` | The count of open PRs. Will only be fetched if `includeOpen` is configured. | |
| `#{{IGNORED_COUNT}}` | The count of PRs and changes which were specifically ignored from the changelog. | |
| `#{{DAYS_SINCE}}` | Days between the 2 releases. Requires `fetchReleaseInformation` to be enabled. | x |
### Configuration Specification
Table of descriptions for the `configuration.json` options to configure the resulting release notes / changelog.
@@ -453,17 +538,17 @@ Table of descriptions for the `configuration.json` options to configure the resu
| **Input** | **Description** |
|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| categories | An array of `category` specifications, offering a flexible way to group changes into categories. |
| category.key | Optional key used for the `categorized` json output. |
| category.key | Optional key used for the `categorized` json output. |
| category.title | The display name of a category in the changelog. |
| category.labels | An array of labels, to match pull request labels against. If any PR label matches any category label, the pull request will show up under this category. (See `exhaustive` to change this) |
| category.exclude_labels | Similar to `labels`, an array of labels to match PRs against, but if a match occurs the PR is excluded from this category. |
| category.exhaustive | Will require all labels defined within this category to be present on the matching PR. |
| category.exhaustive_rules | Will require all rules defined within this category to be valid on the matching PR. If not defined, defaults to the value of `exhaustive` |
| category.exhaustive_rules | Will require all rules defined within this category to be valid on the matching PR. If not defined, defaults to the value of `exhaustive` |
| category.empty_content | If the category has no matching PRs, this content will be used. When not set, the category will be skipped in the changelog. |
| category.rules | An array of `rules` used to match PRs against. Any match will include the PR. (See `exhaustive` to change this) |
| category.rules.pattern | A `regex` pattern to match the property value towards. Uses `RegExp.test("val")` |
| category.rules.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
| category.rules.on_property | The PR property to match against. [Possible values](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/src/configuration.ts#L33-L43). |
| category.rules.on_property | The PR property to match against. [Possible values](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/src/configuration.ts#L33-L43). |
| ignore_labels | An array of labels, to match pull request labels against. If any PR label overlaps, the pull request will be ignored from the changelog. This takes precedence over category labels |
| sort | A `sort` specification, offering the ability to define sort order and property. |
| sort.order | The sort order. Allowed values: `ASC`, `DESC` |
@@ -472,14 +557,10 @@ Table of descriptions for the `configuration.json` options to configure the resu
| pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values |
| empty_template | Template to pick if no changes are detected. See [Template placeholders](#template-placeholders) for possible values |
| label_extractor | An array of `Extractor` specifications, offering a flexible API to extract additinal labels from a PR (Default: `body`, Default in commit mode: `commit message`). |
| label_extractor.pattern | A `regex` pattern, extracting values of the change message. |
| label_extractor.target | The result pattern. The result text will be used as label. If empty, no label is created. (Unused for `match` method) |
| label_extractor.<REGEX> | Please see the documentation related to `Regex Configuration` for more details. |
| label_extractor.on_property | The property to retrieve the text from. This is optional. Defaults to: `body`. Alternative values: `title`, `author`, `milestone`. |
| label_extractor.method | The extraction method used. Defaults to: `replace`. Alternative value: `match`. The method specified references the JavaScript String method. |
| label_extractor.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
| label_extractor.on_empty | Defines the placeholder to be filled in, if the regex does not lead to a result. |
| duplicate_filter | Defines the `Extractor` to use for retrieving the identifier for a PR. In case of duplicates will keep the last matching pull request (depends on `sort`). See `label_extractor` for details on `Extractor` properties. |
| reference | Defines the `Extractor` to use for resolving the "PR-number" for a parent PR. In case of a match, the child PR will not be included in the release notes. See `label_extractor` for details on `Extractor` properties. |
| reference | Defines the `Extractor` to use for resolving the "PR-number" for a parent PR. In case of a match, the child PR will not be included in the release notes. See `label_extractor` for details on `Extractor` properties. |
| transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified |
| transformer.pattern | A `regex` pattern, extracting values of the change message. |
| transformer.target | The result pattern, the regex groups will be filled into. Allows for full transformation of a pull request message. Including potentially specified texts |
@@ -489,11 +570,70 @@ Table of descriptions for the `configuration.json` options to configure the resu
| exclude_merge_branches | An array of branches to be ignored from processing as merge commits |
| tag_resolver | Section to provide configuration for the tag resolving logic. Used if no `fromTag` is provided |
| tag_resolver.method | Defines the method to use. Current options are: `semver`, `sort`. Default: `semver` |
| tag_resolver.filter | Defines a regex which is used to filter out tags not matching. |
| tag_resolver.filter | Defines a regex object which is used to filter out tags not matching. |
| tag_resolver.transformer | Defines a regex transformer used to optionally transform the tag after the filter was applied. Allows to adjust the format to e.g. semver. |
| base_branches | The target branches for the merged PR, ingnores PRs with different target branch. Values can be a `regex`. Default: allow all base branches |
| trim_values | Defines if all values inserted in templates are `trimmed`. Default: false |
### Custom placeholders 🧪
### Regex Configuration
Since v5.x or newer, the regex configuration was unified to allow the same functionalities to be used for the various usecases.
This applies to all configurations outlined in `Configuration Specification` and `Custom placeholders` that allow a regex object.
| **Input** | **Description** |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <parent>.pattern | The `regex` pattern to use |
| <parent>.target | The result pattern. The result text will be used as label. If empty, no label is created. (Usage depends on the `method` used for the regex) |
| <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>.on_empty | Defines the placeholder to be filled in, if the regex does not lead to a result. |
<details><summary><b>Example regex configuration block</b></summary>
<p>
Sample extracts a ticket number from the title
Sample PR title input
```
[XYZ-1234] This is my PR title
```
Regex replace pattern
```
{
"name": "TICKET",
"source": "TITLE",
"transformer": {
"pattern": "\\s*\\[([A-Z].{2,4}-.{2,5})\\][\\S\\s]*",
"target": "- [$1](https://corp.ticket-system.com/browse/$1)"
}
}
```
Regexr style pattern (Use [regexr.com](https://regexr.com/) to test).
To test on regexr inverse the escaping of `\\` to `\`
```
{
"name": "TICKET",
"source": "TITLE",
"transformer": {
"pattern": "\\[([A-Z]{2,4}-.{2,5})\\]",
"method": "regexr",
"target": '- [$1](https://corp.ticket-system.com/browse/$1)'
}
}
```
</p>
</details>
> [!WARNING]
> Usages of `\` in the json have to be escaped. E.g. `\` becomes `\\`.
### Custom placeholders
Starting with v3.2.0 the action provides a feature of defining `CUSTOM_PLACEHOLDERS`.
@@ -523,12 +663,12 @@ Custom placeholders can be defined via the `configuration.json` as `custom_place
This example will look for JIRA tickets in the EPIC project, and extract all of these tickets. The exciting part for that case is, that the ticket is PR bound, but can be used in the global TEMPLATE, but equally also in the PR template. This is unique for CUSTOM PLACEHOLDERS as standard palceholders do not offer this functionality.
| **Input** | **Description** |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| custom_placeholders | An array of `Placeholder` specifications, offering a flexible API to extract custom placeholders from existing placeholders. |
| custom_placeholders.name | The name of the custom placeholder. Will be used within the template. |
| custom_placeholders.source | The source PLACEHOLDER, requires to be one of the existing Template or PR Template placeholders. |
| custom_placeholders.transformer | The transformer specification used to extract the value from the original source PLACEHOLDER. |
| **Input** | **Description** |
|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| custom_placeholders | An array of `Placeholder` specifications, offering a flexible API to extract custom placeholders from existing placeholders. |
| custom_placeholders.name | The name of the custom placeholder. Will be used within the template. |
| custom_placeholders.source | The source PLACEHOLDER, requires to be one of the existing Template or PR Template placeholders. |
| custom_placeholders.transformer.<REGEX> | The transformer specification used to extract the value from the original source PLACEHOLDER. |
A placeholder with the name as `CUSTOM_PLACEHOLDER` can be used as `#{{CUSTOM_PLACEHOLDER}}` in the target template.
By default the same restriction applies as for PR vs template placeholder. E.g. a global placeholder can only be used in the global template (and not in the PR template).
@@ -555,10 +695,9 @@ The API for gitea is equal to the one from GitHub, however it requires the `plat
uses: https://github.com/mikepenz/release-changelog-builder-action@v4.1.0
with:
platform: "gitea" # gitea or github, default is github
commitMode: true
configuration: "configuration.json"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Do not change this
token: ${{ secrets.GITEA_TOKEN }}
```
## Contribute 🧬
@@ -585,7 +724,7 @@ export GITHUB_TOKEN=your_personal_github_pat
## Local Testing 🧪
This GitHub action is fully developed in Typescript and can be run locally via npm or right from the browser using GitHub Codespace.
This GitHub action is fully developed in Typescript and can be run locally via `npm` or right from the browser using `GitHub Codespace`.
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/mikepenz/release-changelog-builder-action)
@@ -593,67 +732,41 @@ Doing so is a great way to test the action and/or your custom configurations loc
To run locally, or to access private repositories (GitHub Codespaces has automatic access to public repos with the default token), you will require to provide a valid `GITHUB_TOKEN` with read only permissions to access the repositories you want to run this action towards. (See more details in [Token Permission](#Token-Permission))
To test your own configuration and usecase, the project contains a [\_\_tests\_\_/demo/demo.test.ts](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/__tests__/demo/demo.test.ts) file, modify this one to your needs. (e.g. change repo, change token, change settings, ...), and then run it via:
```bash
npm test -- demo.test.ts
```
<details><summary><b>Debugging with Breakpoints</b></summary>
<p>
One major benefit of setting up a custom test is that it will allow you to use javascripts full debugging support, including the option of breakpoints via (for example) Visual Code.
From GitHub codespaces, open the terminal panel -> Click the small arrow down beside `+` and pick `JavaScript Debug Terminal` (make sure to export the token again). Now execute the test with this terminal. (This is very similar to local Visual Code environments).
</p>
</details>
<details><summary><b>Run common tests</b></summary>
<p>
To run the common tests of the action, you require to export a valid github token.
```
# Export the token in the CLI you use to execute.
export GITHUB_TOKEN=your_read_only_github_token
```
Afterwards it is possible to run the tests included in the project:
Afterwards it is possible to run any test included in the project:
```bash
npm test -- main.test.ts # modify the file name to run other testcases
```
To test your own configuration, it's adviced to create a new `__tests__/custom.test.ts` file, modify it to your needs (e.g. change repo, change token, change settings, ...), and then run it via `npm test -- custom.test.ts`
<details><summary><b>custom.test.ts</b></summary>
<p>
```typescript
import {mergeConfiguration, resolveConfiguration} from '../src/utils'
import {ReleaseNotesBuilder} from '../src/releaseNotesBuilder'
jest.setTimeout(180000)
it('Test custom changelog builder', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration(
'',
'configs_test/configuration_approvers.json'
))
const releaseNotesBuilder = new ReleaseNotesBuilder(
null, // baseUrl
null, // token
'.', // repoPath
'mikepenz', // user
'release-changelog-builder-action-playground', // repo
'1.5.0', // fromTag
'2.0.0', // toTag
false, // includeOpen
false, // failOnError
false, // ignorePrePrelease
false, // enable to fetch via commits
false, // enable to fetch reviewers
false, // enable to fetch release information
false, // enable to fetch reviews
false, // enable commitMode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
configuration // configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
})
```
</p>
</details>
One major benefit of setting up a custom test is that it will allow you to use javascripts full debugging support, including the option of breakpoints via (for example) Visual Code.
From GitHub codespaces, open the terminal panel -> Click the small arrow down beside `+` and pick `JavaScript Debug Terminal` (make sure to export the token again). Now execute the test with this terminal. (This is very similar to local Visual Code environments).
## Token Permission
Permissions depend on the specific usecase, however this action only requires `read-only` permissions as it will not make modifications to the repository.
@@ -687,8 +800,6 @@ For `Fine-grained personal access tokens` this means:
For Classic tokens you only have to create the token without special permissions.
## Developed By
* Mike Penz
+2
View File
@@ -1,6 +1,8 @@
import {clear} from '../src/transform'
import {mergeConfiguration, parseConfiguration, resolveConfiguration} from '../src/utils'
jest.setTimeout(180000)
clear()
it('Configurations are merged correctly', async () => {
const configurationJson = parseConfiguration(`{
+44
View File
@@ -0,0 +1,44 @@
import {mergeConfiguration, resolveConfiguration} from '../../src/utils'
import {ReleaseNotesBuilder} from '../../src/releaseNotesBuilder'
import {GithubRepository} from '../../src/repositories/GithubRepository'
jest.setTimeout(180000)
// Define the token to use. Either retrieved from the environment.
// Alternatively provide it as a string right here.
const token = process.env.GITHUB_TOKEN || ''
const githubRepository = new GithubRepository(token, undefined, '.')
it('Test custom changelog builder', async () => {
// define the configuration file to use.
// By default, it retrieves a configuration from a json file
// You can also quickly modify in code.
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs/configuration.json'))
// Demo to modify the configuration further in code
// configuration.pr_template = "#{{TITLE}}"
const releaseNotesBuilder = new ReleaseNotesBuilder(
null, // The base url used for the API requests (not needed for normal github)
githubRepository, // Repository implementation (allows tu test gitea). Keep default for GitHub
'.', // Root path to the checked out sources. Commonly keep as default
'mikepenz', // The owner of the repo to test
'release-changelog-builder-action-playground', // The repository name
'1.5.0', // `fromTag` The from tag name or the SHA1 of the from commit
'2.0.0', // `toTag` The to tag name or the SHA1 of the to commit
false, // `includeOpen` Define if you want to include open PRs into the changelog
false, // `failOnError` Define if the action should fail on errors
false, // `ignorePrePrerelease` used if no `fromTag` is defined to resolve the prior tag
false, // `fetchViaCommits` enable to fetch via commits
false, // `fetchReviewers` Enables fetching of reviewers for building the changelog (does additional API requests)
false, // `fetchReleaseInformation` Enable to fetch release information (does additional API requests)
false, // `fetchReviews` Enable to fetch reviews of the PRs (does additional API requests)
'PR', // `mode` Set the mode to use [PR, COMMIT, HYBRID]. PR -> builds changelog using PRs, COMMIT -> using commits, HYBRID -> Uses both
false, // `exportCache` Exports the fetched information to the cache. Not relevant for this test
false, // `exportOnly` Enables to only export the fetched information however not build a changelog
null, // `cache` Path to the cache. Not relevant for this test.
configuration // The configuration to use for building the changelog
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
})
@@ -1,8 +1,10 @@
import {mergeConfiguration, resolveConfiguration} from '../../src/utils'
import {ReleaseNotesBuilder} from '../../src/releaseNotesBuilder'
import {GiteaRepository} from '../../src/repositories/GiteaRepository'
import {clear} from '../../src/transform'
jest.setTimeout(180000)
clear()
/**
* Before starting testing, you should manually clone the repository
@@ -36,7 +38,7 @@ it('[Gitea] Verify reviewers who approved are fetched and also release informati
true, // enable to fetch reviewers
true, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -76,7 +78,7 @@ it('[Gitea] Should match generated changelog (unspecified fromTag)', async () =>
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -113,7 +115,7 @@ it('[Gitea] Should match generated changelog (unspecified tags)', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -149,7 +151,7 @@ it('[Gitea] Should use empty placeholder', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -200,7 +202,7 @@ it('[Gitea] Should fill empty placeholders', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -308,7 +310,7 @@ it('[Gitea] Should fill `template` placeholders', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -417,7 +419,7 @@ it('[Gitea] Should fill `template` placeholders, ignore', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -482,7 +484,7 @@ it('[Gitea] Uncategorized category', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -556,7 +558,7 @@ it('[Gitea] Verify commit based changelog', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
true, // enable commitMode
'COMMIT', // enable commitMode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -633,7 +635,7 @@ it('[Gitea] Verify commit based changelog', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
true, // enable commitMode
'COMMIT', // enable commitMode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -671,7 +673,7 @@ it('[Gitea] Verify default inclusion of open PRs', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -712,7 +714,7 @@ it('[Gitea] Verify custom categorisation of open PRs', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -744,7 +746,7 @@ it('[Gitea] Fetch release information', async () => {
false, // enable to fetch reviewers
true, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -778,7 +780,7 @@ it('[Gitea] Fetch release information for non existing tag / release', async ()
false, // enable to fetch reviewers
true, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -1,9 +1,12 @@
import {checkExportedData, mergeConfiguration, resolveConfiguration} from '../../src/utils'
import {buildChangelog} from '../../src/transform'
import {pullData} from '../../src/pr-collector/prCollector'
import {Options, pullData} from '../../src/pr-collector/prCollector'
import {GiteaRepository} from '../../src/repositories/GiteaRepository'
import {clear} from '../../src/transform'
import {ReleaseNotesOptions} from '../../src/releaseNotesBuilder'
jest.setTimeout(180000)
clear()
// load octokit instance
const enablePullData = false
@@ -45,17 +48,17 @@ it('[Gitea] Should have changelog (tags)', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: giteaRepository
}
let data: any
if (enablePullData) {
data = await pullData(giteaRepository, options)
data = await pullData(giteaRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/gitea_rcba_0.5.0-master_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features
@@ -83,17 +86,17 @@ it('[Gitea] Should match generated changelog (tags)', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: giteaRepository
}
let data: any
if (enablePullData) {
data = await pullData(giteaRepository, options)
data = await pullData(giteaRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/gitea_rcba_0.5.0-master_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features
@@ -122,17 +125,17 @@ it('[Gitea] Should match generated changelog (refs)', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: giteaRepository
}
let data: any
if (enablePullData) {
data = await pullData(giteaRepository, options)
data = await pullData(giteaRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/gitea_rcba_3e49adf-894a64_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 📦 Uncategorized
@@ -179,17 +182,17 @@ it('[Gitea] Should match generated changelog and replace all occurrences (refs)'
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: giteaRepository
}
let data: any
if (enablePullData) {
data = await pullData(giteaRepository, options)
data = await pullData(giteaRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/gitea_rcba_3e49adf-894a64_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 📦 Uncategorized
@@ -241,17 +244,17 @@ it('[Gitea] Should match ordered ASC', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: giteaRepository
}
let data: any
if (enablePullData) {
data = await pullData(giteaRepository, options)
data = await pullData(giteaRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/gitea_rcba_0.1.0-master_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features
@@ -298,17 +301,17 @@ it('[Gitea] Should match ordered DESC', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: giteaRepository
}
let data: any
if (enablePullData) {
data = await pullData(giteaRepository, options)
data = await pullData(giteaRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/gitea_rcba_0.1.0-master_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features
+2
View File
@@ -2,8 +2,10 @@ import * as path from 'path'
import * as process from 'process'
import * as cp from 'child_process'
import * as fs from 'fs'
import {clear} from '../src/transform'
jest.setTimeout(180000)
clear()
test('missing values should result in failure', () => {
expect.assertions(1)
+59
View File
@@ -0,0 +1,59 @@
import {transformStringToValue, validateRegex} from '../src/pr-collector/regexUtils'
import {Regex} from '../src/pr-collector/types'
import {clear} from '../src/transform'
jest.setTimeout(180000)
clear()
it('Replace into target', async () => {
const regex: Regex = {
pattern: '.*(\\[Feature\\]|\\[Issue\\]).*',
target: '$1'
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue('[Feature] TEST', validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Replace all into target', async () => {
const regex: Regex = {
pattern: '.*(\\[Feature\\]|\\[Issue\\]).*',
method: 'replaceAll',
target: '$1'
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue('[Feature] TEST', validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Match without target', async () => {
const regex: Regex = {
pattern: '\\[Feature\\]|\\[Issue\\]',
method: 'match'
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue('[Feature] TEST', validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Match into target', async () => {
const regex: Regex = {
pattern: '(?<label>\\[Feature\\]|\\[Issue\\])',
method: 'match',
target: '$1'
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue('[Feature] TEST', validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Match into named group', async () => {
const regex: Regex = {
pattern: '(?<label>\\[Feature\\]|\\[Issue\\])',
method: 'match',
target: 'label'
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue('[Feature] TEST', validatedRegex!!)).toStrictEqual(`[Feature]`)
})
+16 -14
View File
@@ -1,8 +1,10 @@
import {mergeConfiguration, resolveConfiguration} from '../src/utils'
import {ReleaseNotesBuilder} from '../src/releaseNotesBuilder'
import {GithubRepository} from '../src/repositories/GithubRepository'
import {clear} from '../src/transform'
jest.setTimeout(180000)
clear()
const token = process.env.GITHUB_TOKEN || ''
const githubRepository = new GithubRepository(token, undefined, '.')
@@ -23,7 +25,7 @@ it('[Github] Should match generated changelog (unspecified fromTag)', async () =
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -57,7 +59,7 @@ it('[Github] Should match generated changelog (unspecified tags)', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -86,7 +88,7 @@ it('[Github] Should use empty placeholder', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
'caches/rcba_0.0.2-0.0.3_cache.json', // path to the cache
@@ -115,7 +117,7 @@ it('[Github] Should fill empty placeholders', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
'caches/rcba_0.0.2-0.0.3_cache.json', // path to the cache
@@ -146,7 +148,7 @@ it('[Github] Should fill `template` placeholders', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
'caches/rcba_0.0.1-0.0.3_cache.json', // path to the cache
@@ -178,7 +180,7 @@ it('[Github] Should fill `template` placeholders, ignore', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
'caches/rcba_0.9.1-0.9.5_cache.json', // path to the cache
@@ -209,7 +211,7 @@ it('[Github] Uncategorized category', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
'caches/rcba_0.9.1-0.9.5_cache.json', // path to the cache
@@ -240,7 +242,7 @@ it('[Github] Verify commit based changelog', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
true, // enable commitMode
'COMMIT', // enable commitMode
false, // enable exportCache
false, // enable exportOnly
'caches/rcba_0.0.1-0.0.3_commit_cache.json', // path to the cache
@@ -271,7 +273,7 @@ it('[Github] Verify commit based changelog, with emoji categorisation', async ()
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
true, // enable commitMode
'COMMIT', // enable commitMode
false, // enable exportCache
false, // enable exportOnly
'caches/stackzy_bd3242-17a9e4_cache.json', // path to the cache
@@ -302,7 +304,7 @@ it('[Github] Verify default inclusion of open PRs', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -333,7 +335,7 @@ it('[Github] Verify custom categorisation of open PRs', async () => {
false, // enable to fetch reviewers
false, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -364,7 +366,7 @@ it('[Github] Verify reviewers who approved are fetched and also release informat
true, // enable to fetch reviewers
true, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -396,7 +398,7 @@ it('[Github] Fetch release information', async () => {
false, // enable to fetch reviewers
true, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
@@ -426,7 +428,7 @@ it('[Github] Fetch release information for non existing tag / release', async ()
false, // enable to fetch reviewers
true, // enable to fetch tag release information
false, // enable to fetch reviews
false, // enable commitMode
'PR', // mode
false, // enable exportCache
false, // enable exportOnly
null, // path to the cache
+108 -31
View File
@@ -1,9 +1,12 @@
import {checkExportedData, mergeConfiguration, resolveConfiguration} from '../src/utils'
import {buildChangelog} from '../src/transform'
import {pullData} from '../src/pr-collector/prCollector'
import {Options, pullData} from '../src/pr-collector/prCollector'
import {GithubRepository} from '../src/repositories/GithubRepository'
import {clear} from '../src/transform'
import {ReleaseNotesOptions} from '../src/releaseNotesBuilder'
jest.setTimeout(180000)
clear()
// load octokit instance
const enablePullData = false // if false -> use cache for data
@@ -24,17 +27,17 @@ it('Should have empty changelog (tags)', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_0.0.2-0.0.3_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual('- no changes')
})
@@ -52,17 +55,17 @@ it('Should match generated changelog (tags)', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_0.0.1-0.0.3_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
@@ -86,17 +89,17 @@ it('Should match generated changelog (refs)', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_5ec7a2-fa3788_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
@@ -127,17 +130,17 @@ it('Should match generated changelog and replace all occurrences (refs)', async
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_5ec7a2-fa3788_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
@@ -171,17 +174,17 @@ it('Should match ordered ASC', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_0.3.0-0.5.0_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n22\n24\n25\n26\n28\n\n## 🐛 Fixes\n\n23\n\n`)
})
@@ -200,17 +203,17 @@ it('Should match ordered DESC', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_0.3.0-0.5.0_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n28\n26\n25\n24\n22\n\n## 🐛 Fixes\n\n23\n\n`)
})
@@ -228,17 +231,17 @@ it('Should match ordered by title ASC', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_0.3.0-0.5.0_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(
`## 🚀 Features\n\nEnhanced action logs\nImprove README\nImproved configuration failure handling\nImproved defaults if no configuration is provided\nIntroduce additional placeholders [milestone, labels, assignees, reviewers]\n\n## 🐛 Fixes\n\nImproved handling for non existing tags\n\n`
@@ -258,17 +261,17 @@ it('Should match ordered by title DESC', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_0.3.0-0.5.0_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(
`## 🚀 Features\n\nIntroduce additional placeholders [milestone, labels, assignees, reviewers]\nImproved defaults if no configuration is provided\nImproved configuration failure handling\nImprove README\nEnhanced action logs\n\n## 🐛 Fixes\n\nImproved handling for non existing tags\n\n`
@@ -288,17 +291,17 @@ it('Should ignore PRs not merged into develop branch', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_1.3.1-1.4.0_base_develop_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`150\n\n`)
})
@@ -316,17 +319,91 @@ it('Should ignore PRs not merged into main branch', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
mode: 'PR',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options)
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/rcba_1.3.1-1.4.0_base_main_cache.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(`153\n\n`)
})
it('Default configuration with commit mode', async () => {
const configuration = Object.assign({}, mergeConfiguration(undefined, undefined, 'COMMIT'))
const options = {
owner: 'conventional-commits',
repo: 'conventionalcommits.org',
fromTag: {name: '56cdc85d01fd11aa164bd958bbf6114a51abfcf6'},
toTag: {name: '325b74fbc44bf34d9fa645951d076a450b4e26be'},
includeOpen: false,
failOnError: false,
fetchViaCommits: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
mode: 'COMMIT',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/github_conventional-commits-1e1c8e-325b74.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(
`## 🚀 Features\n\n- feat(lang): add Bengali\n- feat(lang): add uzbek translation (#558)\n\n## 🐛 Fixes\n\n- fix: Fix grammar and consistency in french translation (#546)\n- fix(ja): fix typo\n- fix(zh-hant): Distinguish translations of 'Release/Publish'\n- fix(ko): fix translation typo for message (#567)\n\n## 📦 Other\n\n- doc: new thi.ng links and descriptions\n- docs: add link to git-changelog-command-line docker image\n- docs: Add descriptions for commit types\n\n`
)
})
it('Default configuration with commit mode and custom placeholder', async () => {
const configuration = Object.assign({}, mergeConfiguration(undefined, undefined, 'COMMIT'))
configuration.pr_template = '- #{{TITLE_ONLY}}'
configuration.trim_values = true
configuration.custom_placeholders = [
{
name: 'TITLE_ONLY',
source: 'TITLE',
transformer: {
method: 'regexr',
pattern: '(\\w+(\\(.+\\))?: ?)?(.+)',
target: '$3'
}
}
]
const options = {
owner: 'conventional-commits',
repo: 'conventionalcommits.org',
fromTag: {name: '56cdc85d01fd11aa164bd958bbf6114a51abfcf6'},
toTag: {name: '325b74fbc44bf34d9fa645951d076a450b4e26be'},
includeOpen: false,
failOnError: false,
fetchViaCommits: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
mode: 'COMMIT',
configuration,
repositoryUtils: githubRepository
}
let data: any
if (enablePullData) {
data = await pullData(githubRepository, options as Options)
} else {
data = checkExportedData(false, 'caches/github_conventional-commits-1e1c8e-325b74.json')
}
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, options as ReleaseNotesOptions)
console.log(changeLog)
expect(changeLog).toStrictEqual(
`## 🚀 Features\n\n- add Bengali\n- add uzbek translation (#558)\n\n## 🐛 Fixes\n\n- Fix grammar and consistency in french translation (#546)\n- fix typo\n- Distinguish translations of 'Release/Publish'\n- fix translation typo for message (#567)\n\n## 📦 Other\n\n- new thi.ng links and descriptions\n- add link to git-changelog-command-line docker image\n- Add descriptions for commit types`
)
})
+17 -11
View File
@@ -1,7 +1,10 @@
import { validateTransformer } from '../src/pr-collector/regexUtils'
import {TagResolver} from '../src/configuration'
import {validateRegex} from '../src/pr-collector/regexUtils'
import {filterTags, prepareAndSortTags, TagInfo, transformTags} from '../src/pr-collector/tags'
import {clear} from '../src/transform'
jest.setTimeout(180000)
clear()
it('Should order tags correctly using semver', async () => {
const tags: TagInfo[] = [
@@ -100,14 +103,16 @@ it('Should filter tags correctly using the regex', async () => {
{name: '20.0.2', commit: ''}
]
const tagResolver = {
const tagResolver: TagResolver = {
method: 'non-existing-method',
filter: {
pattern: 'api-(.+)',
method: 'match',
flags: 'gu'
}
}
const filtered = filterTags(tags, tagResolver)
const filter = validateRegex(tagResolver.filter)
const filtered = filterTags(tags, filter)
.map(function (tag) {
return tag.name
})
@@ -131,14 +136,16 @@ it('Should filter tags correctly using the regex (inverse)', async () => {
{name: '20.0.2', commit: ''}
]
const tagResolver = {
const tagResolver: TagResolver = {
method: 'non-existing-method',
filter: {
pattern: '^(?!\\w+-)(.+)',
method: 'match',
flags: 'gu'
}
}
const filtered = filterTags(tags, tagResolver)
const filter = validateRegex(tagResolver.filter)
const filtered = filterTags(tags, filter)
.map(function (tag) {
return tag.name
})
@@ -147,7 +154,6 @@ it('Should filter tags correctly using the regex (inverse)', async () => {
expect(filtered).toStrictEqual(`0.1.0-b01,1.0.0,1.0.0-a01,2.0.0,10.1.0,20.0.2`)
})
it('Should transform tags correctly using the regex', async () => {
const tags: TagInfo[] = [
{name: 'api-0.0.1', commit: ''},
@@ -160,16 +166,16 @@ it('Should transform tags correctly using the regex', async () => {
{name: '20.0.2', commit: ''}
]
const tagResolver = {
const tagResolver: TagResolver = {
method: 'non-existing-method',
transformer: {
pattern: '(api\-)?(.+)',
target: "$2"
pattern: '(api-)?(.+)',
target: '$2'
}
}
const transformer = validateTransformer(tagResolver.transformer)
if(transformer != null) {
const transformer = validateRegex(tagResolver.transformer)
if (transformer != null) {
const transformed = transformTags(tags, transformer)
.map(function (tag) {
return tag.name
+24 -36
View File
@@ -4,8 +4,11 @@ import {Configuration, DefaultConfiguration} from '../src/configuration'
import {PullRequestInfo} from '../src/pr-collector/pullRequests'
import {DefaultDiffInfo} from '../src/pr-collector/commits'
import {GithubRepository} from '../src/repositories/GithubRepository'
import {clear} from '../src/transform'
import {buildChangelogTest} from './utils'
jest.setTimeout(180000)
clear()
const configuration = Object.assign({}, DefaultConfiguration)
configuration.categories = [
@@ -150,7 +153,7 @@ it('Extract label from title, combined regex', async () => {
on_property: 'title'
}
]
expect(buildChangelogTest(configuration, mergedPullRequests)).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\n`
)
})
@@ -166,7 +169,7 @@ it('Extract label from title and body, combined regex', async () => {
let prs = Array.from(mergedPullRequests)
prs.push(pullRequestWithLabelInBody)
expect(buildChangelogTest(configuration, prs)).toStrictEqual(
expect(buildChangelogTest(configuration, prs, 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- label in body\n - PR: #5\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n\n`
)
})
@@ -184,7 +187,7 @@ it('Extract label from title, split regex', async () => {
on_property: 'title'
}
]
expect(buildChangelogTest(configuration, mergedPullRequests)).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`
)
})
@@ -202,7 +205,7 @@ it('Extract label from title, match', async () => {
method: 'match'
}
]
expect(buildChangelogTest(configuration, mergedPullRequests)).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`
)
})
@@ -215,7 +218,7 @@ it('Extract label from title, match multiple', async () => {
method: 'match'
}
]
expect(buildChangelogTest(configuration, mergedPullRequests)).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`
)
})
@@ -229,7 +232,7 @@ it('Extract label from title, match multiple, custon non matching label', async
on_empty: '[Other]'
}
]
expect(buildChangelogTest(configuration, mergedPullRequests)).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## 🧪 Others\n\n- [AB-404] - not found label\n - PR: #4\n\n`
)
})
@@ -370,7 +373,7 @@ it('Match multiple labels exhaustive for category', async () => {
exhaustive: true
}
]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(
`## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
)
})
@@ -383,7 +386,7 @@ it('Deduplicate duplicated PRs', async () => {
on_property: 'title',
method: 'match'
}
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
)
})
@@ -397,7 +400,7 @@ it('Deduplicate duplicated PRs DESC', async () => {
on_property: 'title',
method: 'match'
}
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n\n`
)
})
@@ -417,7 +420,7 @@ it('Reference PRs', async () => {
method: 'replace',
target: '$1'
}
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(`1 -- 2\n4 -- \n3 -- \n\n`)
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(`1 -- 2\n4 -- \n3 -- \n\n`)
})
it('Use empty_content for empty category', async () => {
@@ -433,7 +436,7 @@ it('Use empty_content for empty category', async () => {
labels: ['Feature']
}
]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).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`
)
})
@@ -454,7 +457,7 @@ it('Commit SHA-1 in commitMode', async () => {
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: true,
mode: 'COMMIT',
configuration: customConfig,
repositoryUtils: repositoryUtils
})
@@ -476,7 +479,7 @@ it('Release Diff', async () => {
fetchReviewers: false,
fetchReleaseInformation: true,
fetchReviews: false,
commitMode: true,
mode: 'COMMIT',
configuration: customConfig,
repositoryUtils: repositoryUtils
})
@@ -504,7 +507,7 @@ it('Use exclude labels to not include a PR within a category.', async () => {
exclude_labels: ['Fix']
}
]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(
`## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🚀 Features and/or 🐛 Issues But No 🐛 Fixes\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n`
)
})
@@ -549,7 +552,7 @@ it('Extract custom placeholder from PR body and replace in global template', asy
'#{{CHANGELOG}}\n\n#{{C_PLACEHOLER_2[2]}}\n\n#{{C_PLACEHOLER_2[*]}}#{{C_PLACEHOLDER_1[7]}}#{{C_PLACEHOLER_2[1493]}}#{{C_PLACEHOLER_4[*]}}#{{C_PLACEHOLER_4[0]}}#{{C_PLACEHOLER_3[1]}}'
customConfig.pr_template = '#{{BODY}} ----> #{{C_PLACEHOLDER_1}}#{{C_PLACEHOLER_3}}'
expect(buildChangelogTest(customConfig, mergedPullRequests)).toStrictEqual(
expect(buildChangelogTest(customConfig, mergedPullRequests, repositoryUtils)).toStrictEqual(
`## 🚀 Features\n\nno magic body1 for this matter ----> - body1body1\nno magic body3 for this matter ----> - body3\n\n## 🐛 Fixes\n\nno magic body2 for this matter ----> - body2\nno magic body3 for this matter ----> - body3\n\n## 🧪 Others\n\nno magic body4 for this matter ----> - body4\n\n\n\n\n- ody3\n\n\n- ody1\n- ody2\n- ody3\n- ody4`
)
})
@@ -574,7 +577,7 @@ it('Use Rules to include a PR within a Category.', async () => {
exhaustive: true
}
]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual(
`## 🚀 Features But No 🐛 Fixes and only merged with a title containing \`[ABC-1234]\`\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n`
)
})
@@ -595,7 +598,9 @@ it('Use Rules to get all open PRs in a Category.', async () => {
]
}
]
expect(buildChangelogTest(customConfig, prs)).toStrictEqual(`## Open PRs only\n\n- Still pending open pull request\n - PR: #6\n\n`)
expect(buildChangelogTest(customConfig, prs, repositoryUtils)).toStrictEqual(
`## Open PRs only\n\n- Still pending open pull request\n - PR: #6\n\n`
)
})
it('Use Rules to get current open PR and merged categorised.', async () => {
@@ -633,7 +638,7 @@ it('Use Rules to get current open PR and merged categorised.', async () => {
}
]
expect(buildChangelogTest(customConfig, prs)).toStrictEqual(
expect(buildChangelogTest(customConfig, prs, repositoryUtils)).toStrictEqual(
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n- Still pending open pull request (Current)\n - PR: #6\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Issues\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
)
})
@@ -665,24 +670,7 @@ it('Use Rules to get all open PRs in one Category and merged categorised.', asyn
exhaustive: true
}
]
expect(buildChangelogTest(customConfig, prs)).toStrictEqual(
expect(buildChangelogTest(customConfig, prs, repositoryUtils)).toStrictEqual(
`## Open PRs only\n\n- Still pending open pull request\n - PR: #6\n\n## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
)
})
function buildChangelogTest(config: Configuration, prs: PullRequestInfo[]): string {
return buildChangelog(DefaultDiffInfo, prs, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration: config,
repositoryUtils: repositoryUtils
})
}
+87
View File
@@ -0,0 +1,87 @@
import moment from 'moment'
import {DefaultConfiguration} from '../src/configuration'
import {PullRequestInfo} from '../src/pr-collector/pullRequests'
import {GithubRepository} from '../src/repositories/GithubRepository'
import {clear} from '../src/transform'
import {buildChangelogTest, buildPullRequeset} from './utils'
jest.setTimeout(180000)
clear()
const repositoryUtils = new GithubRepository(process.env.GITEA_TOKEN || '', undefined, '.')
// test set of PRs with lables predefined
const pullRequestsWithLabels: PullRequestInfo[] = []
pullRequestsWithLabels.push(
buildPullRequeset(1, 'Core Feature Ticket', ['core', 'feature']),
buildPullRequeset(2, 'Core Bug Ticket', ['core', 'bug']),
buildPullRequeset(3, 'Mobile Feature Ticket', ['mobile', 'feature']),
buildPullRequeset(4, 'Mobile Bug Ticket', ['mobile', 'bug']),
buildPullRequeset(5, 'Mobile & Core Feature Ticket', ['core', 'mobile', 'feature']),
buildPullRequeset(6, 'Mobile & Core Bug Ticket', ['core', 'mobile', 'bug']),
buildPullRequeset(7, 'Mobile & Core Bug Bug Ticket', ['core', 'mobile', 'bug', 'fancy-bug']),
buildPullRequeset(8, 'Core Ticket', ['core'])
)
it('Match multiple labels exhaustive for category', async () => {
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.pr_template = '- #{{TITLE}}'
customConfig.categories = [
{
title: '## Core',
labels: ['core'],
consume: true,
categories: [
{
title: '### 🚀 Features',
labels: ['feature']
},
{
title: '### 🧪 Bug',
labels: ['bug'],
categories: [
{
title: '#### 🧪 Bug Bug',
labels: ['fancy-bug']
}
]
}
]
},
{
title: '## Mobile',
labels: ['mobile'],
consume: true,
categories: [
{
title: '### 🚀 Features',
labels: ['feature']
},
{
title: '### 🧪 Bug',
labels: ['bug']
}
]
},
{
title: '## Desktop',
labels: ['desktop'],
consume: true,
categories: [
{
title: '### 🚀 Features',
labels: ['feature']
},
{
title: '### 🧪 Bug',
labels: ['bug']
}
]
}
]
const built = buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)
expect(built).toStrictEqual(
`## Core\n\n- Core Ticket\n\n### 🚀 Features\n\n- Core Feature Ticket\n- Mobile & Core Feature Ticket\n\n### 🧪 Bug\n\n- Core Bug Ticket\n- Mobile & Core Bug Ticket\n\n#### 🧪 Bug Bug\n\n- Mobile & Core Bug Bug Ticket\n\n## Mobile\n\n\n### 🚀 Features\n\n- Mobile Feature Ticket\n\n### 🧪 Bug\n\n- Mobile Bug Ticket\n\n`
)
})
+44
View File
@@ -0,0 +1,44 @@
import {Configuration} from '../src/configuration'
import {DefaultDiffInfo} from '../src/pr-collector/commits'
import {PullRequestInfo} from '../src/pr-collector/pullRequests'
import {buildChangelog} from '../src/transform'
import {BaseRepository} from '../src/repositories/BaseRepository'
import moment from 'moment'
export const buildChangelogTest = (config: Configuration, prs: PullRequestInfo[], repositoryUtils: BaseRepository): string => {
return buildChangelog(DefaultDiffInfo, prs, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
mode: 'PR',
configuration: config,
repositoryUtils
})
}
export const buildPullRequeset = (number: number, title: string, labels: string[] = ['feature']): PullRequestInfo => {
return {
number,
title,
htmlURL: '',
baseBranch: '',
createdAt: moment(),
mergedAt: moment(),
mergeCommitSha: 'sha',
author: 'Author',
repoName: 'test-repo',
labels,
milestone: '',
body: '',
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'merged'
}
}
+3 -1
View File
@@ -40,8 +40,10 @@ inputs:
fetchReviews:
description: 'Will enable fetching the reviews (comments) attached to the PR'
default: "false"
mode:
description: 'Defines the mode used to retrieve the information. Available options: [`PR`, `COMMIT`, `HYBRID`]. Defaults to `PR`.'
commitMode:
description: 'Enables a `light` commit based mode. This mode generates changelogs based on the commits. Please note that this is not officially supported, and lacks a lot of features only possible with PRs.'
description: '[Deprecated] Enables the commit based mode. This mode generates changelogs based on the commits. Please note that this lacks a lot of features only possible with PRs.'
default: "false"
outputFile:
description: 'If defined, the changelog will get written to this file. (relative to the checkout dir)'
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -21,6 +21,7 @@
"labels": ["dependencies"]
}
],
"template": "${{CHANGELOG}}\n\nContributors:\n${{CONTRIBUTORS}}",
"max_pull_requests": 1000,
"max_back_track_time_days": 1000
}
Generated Vendored
+4176 -1127
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+40 -34
View File
@@ -304,6 +304,28 @@ THE SOFTWARE.
agent-base
MIT
(The MIT License)
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
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.
before-after-hook
Apache-2.0
@@ -593,25 +615,28 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
https-proxy-agent
MIT
(The MIT License)
lru-cache
ISC
The ISC License
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Copyright (c) Isaac Z. Schlueter and 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:
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
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.
moment
MIT
@@ -806,22 +831,3 @@ 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.
yallist
ISC
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+1769 -1092
View File
File diff suppressed because it is too large Load Diff
+18 -16
View File
@@ -1,6 +1,6 @@
{
"name": "release-changelog-builder-action",
"version": "v4.1.0",
"version": "v5.0.0",
"private": true,
"description": "A GitHub action that builds your release notes / changelog fast, easy and exactly the way you want.",
"main": "lib/main.js",
@@ -14,6 +14,7 @@
"test": "jest",
"test-github": "jest __tests__/*.test.ts",
"test-gitea": "jest __tests__/gitea/*.test.ts",
"test-demo": "jest __tests__/demo/*.test.ts",
"all": "npm run build && npm run format && npm run lint && npm run package && npm run test-github"
},
"repository": {
@@ -38,28 +39,29 @@
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@octokit/rest": "^20.0.2",
"gitea-js": "^1.20.1",
"https-proxy-agent": "^7.0.2",
"@octokit/rest": "^20.1.1",
"gitea-js": "^1.22.0",
"https-proxy-agent": "^7.0.5",
"moment": "^2.30.1",
"semver": "^7.6.0"
"semver": "^7.6.3"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.11.17",
"@types/semver": "^7.5.6",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@types/node": "^20.14.12",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.56.0",
"eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-prettier": "^5.1.3",
"eslint": "^8.57.0",
"eslint-plugin-github": "^5.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"js-yaml": "^4.1.0",
"prettier": "3.2.5",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
"prettier": "3.3.3",
"ts-jest": "^29.2.3",
"typescript": "^5.5.4"
}
}
+50 -6
View File
@@ -1,4 +1,4 @@
import {Extractor, PullConfiguration, Regex, Rule, Sort, Transformer} from './pr-collector/types'
import {Extractor, PullConfiguration, Regex, Rule, Sort} from './pr-collector/types'
export interface Configuration extends PullConfiguration {
max_tags_to_fetch: number
@@ -14,7 +14,7 @@ export interface Configuration extends PullConfiguration {
label_extractor: Extractor[]
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.
transformers: Transformer[]
transformers: Regex[]
tag_resolver: TagResolver
base_branches: string[]
custom_placeholders?: Placeholder[]
@@ -30,6 +30,9 @@ export interface Category {
exhaustive?: boolean // requires all labels to be present in the PR
exhaustive_rules?: boolean // requires all rules to be present in the PR (if not set, defaults to exhaustive value)
empty_content?: string // if the category has no matching PRs, this content will be used. If not set, the category will be skipped in the changelog.
categories?: Category[] // allows for nested categories, items matched for a child category won't show up in the parent
consume?: boolean // defines if the matched PR will be consumed by this category. Consumed PRs won't show up in any category *after*
entries?: string[] // array of single changelog entries, used to construct the changelog. (this is filled during the build)
}
/**
@@ -51,13 +54,13 @@ export type Property =
export interface TagResolver {
method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting
transformer?: Transformer // transforms the tag name using the regex, run after the filter
transformer?: Regex // transforms the tag name using the regex, run after the filter
}
export interface Placeholder {
name: string // the name of the new placeholder
source: string // the src placeholder which will be used to apply the transformer on
transformer: Transformer // the transformer to use to transform the original placeholder into the custom placheolder
transformer: Regex // the transformer to use to transform the original placeholder into the custom placeheolder
}
export const DefaultConfiguration: Configuration = {
@@ -91,13 +94,13 @@ export const DefaultConfiguration: Configuration = {
labels: []
}
], // the categories to support for the ordering
ignore_labels: ['ignore'], // list of lables being ignored from the changelog
ignore_labels: ['ignore'], // list of labels being ignored from the changelog
label_extractor: [], // extracts additional labels from the commit message given a regex
duplicate_filter: undefined, // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`)
transformers: [], // transformers to apply on the PR description according to the `pr_template`
tag_resolver: {
// defines the logic on how to resolve the previous tag, only relevant if `fromTag` is not specified
method: 'semver', // defines which method to use, by default it will use `semver` (dropping all non matching tags). Alternative `sort` is also available.
method: 'semver', // defines which method to use, by default it will use `semver` (dropping all non-matching tags). Alternative `sort` is also available.
filter: undefined, // filter out all tags not matching the regex
transformer: undefined // transforms the tag name using the regex, run after the filter
},
@@ -105,3 +108,44 @@ export const DefaultConfiguration: Configuration = {
custom_placeholders: [],
trim_values: false // defines if values are being trimmed prior to inserting
}
export const DefaultCommitConfiguration: Configuration = {
max_tags_to_fetch: DefaultConfiguration.max_tags_to_fetch,
max_pull_requests: DefaultConfiguration.max_pull_requests,
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: [
{
title: '## 🚀 Features',
labels: ['feature', 'feat']
},
{
title: '## 🐛 Fixes',
labels: ['fix', 'bug']
},
{
title: '## 🧪 Tests',
labels: ['test']
},
{
title: '## 📦 Other',
labels: []
}
], // the categories to support for the ordering
ignore_labels: DefaultConfiguration.ignore_labels,
label_extractor: [
{
pattern: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)',
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
}
+10 -6
View File
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import {mergeConfiguration, parseConfiguration, resolveConfiguration, retrieveRepositoryPath, writeOutput} from './utils'
import {mergeConfiguration, parseConfiguration, resolveConfiguration, resolveMode, retrieveRepositoryPath, writeOutput} from './utils'
import {ReleaseNotesBuilder} from './releaseNotesBuilder'
import {Configuration} from './configuration'
import {GithubRepository} from './repositories/GithubRepository'
@@ -37,22 +37,26 @@ async function run(): Promise<void> {
if (configurationJson) {
configJson = parseConfiguration(configurationJson)
if (configJson) {
core.info(`️ Retreived configuration via 'configurationJson'.`)
core.info(`️ Retrieved configuration via 'configurationJson'.`)
}
}
// read in the configuration from the file if possible
const configurationFile: string = core.getInput('configuration')
const configFile = resolveConfiguration(repositoryPath, configurationFile)
if (configFile) {
core.info(`️ Retreived configuration via 'configuration' (via file).`)
core.info(`️ Retrieved configuration via 'configuration' (via file).`)
}
if (!configJson && !configFile) {
core.info(`️ No configuration provided. Using Defaults.`)
}
// mode of the action (PR, COMMIT, HYBRID)
const mode = resolveMode(core.getInput('mode'), core.getInput('commitMode') === 'true')
core.info(`️ Running in ${mode} mode.`)
// merge configs, use default values from DefaultConfig on missing definition
const configuration = mergeConfiguration(configJson, configFile)
const configuration = mergeConfiguration(configJson, configFile, mode)
// read in repository inputs
const baseUrl = core.getInput('baseUrl')
@@ -70,7 +74,6 @@ async function run(): Promise<void> {
const fetchReviewers = core.getInput('fetchReviewers') === 'true'
const fetchReleaseInformation = core.getInput('fetchReleaseInformation') === 'true'
const fetchReviews = core.getInput('fetchReviews') === 'true'
const commitMode = core.getInput('commitMode') === 'true'
const exportCache = core.getInput('exportCache') === 'true'
const exportOnly = core.getInput('exportOnly') === 'true'
const cache = core.getInput('cache')
@@ -91,7 +94,7 @@ async function run(): Promise<void> {
fetchReviewers,
fetchReleaseInformation,
fetchReviews,
commitMode,
mode,
exportCache,
exportOnly,
cache,
@@ -108,6 +111,7 @@ async function run(): Promise<void> {
}
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.setFailed(error.message)
core.error(`🔥 Failed to generate changelog due to ${JSON.stringify(error)}`)
}
}
+35 -32
View File
@@ -91,42 +91,45 @@ export class Commits {
}
async generateCommitPRs(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, configuration} = options
const diffInfo = await this.getCommitHistory(options)
const commits = diffInfo.commitInfo
if (commits.length === 0) {
return [diffInfo, []]
}
const prCommits = filterCommits(commits, configuration.exclude_merge_branches)
core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`)
const prs = prCommits.map(function (commit): PullRequestInfo {
return {
number: 0,
title: commit.summary,
htmlURL: '',
baseBranch: '',
createdAt: commit.commitDate,
mergedAt: commit.commitDate,
mergeCommitSha: commit.sha,
author: commit.author || '',
repoName: '',
labels: [],
milestone: '',
body: commit.message || '',
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'merged'
}
})
return [diffInfo, prs]
return convertCommitsToPrs(options, diffInfo)
}
}
export function convertCommitsToPrs(options: Options, diffInfo: DiffInfo): [DiffInfo, PullRequestInfo[]] {
const {owner, repo, configuration} = options
const commits = diffInfo.commitInfo
if (commits.length === 0) {
return [diffInfo, []]
}
const prCommits = filterCommits(commits, configuration.exclude_merge_branches)
core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`)
const prs = prCommits.map(function (commit): PullRequestInfo {
return {
number: 0,
title: commit.summary,
htmlURL: '',
baseBranch: '',
createdAt: commit.commitDate,
mergedAt: commit.commitDate,
mergeCommitSha: commit.sha,
author: commit.author || '',
repoName: '',
labels: [],
milestone: '',
body: commit.message || '',
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'merged'
}
})
return [diffInfo, prs]
}
/**
* Filters out all commits which match the exclude pattern
*/
+22 -14
View File
@@ -3,7 +3,7 @@ import {PullConfiguration} from './types'
import {TagInfo, Tags} from './tags'
import {failOrError} from './utils'
import {PullRequestInfo, PullRequests} from './pullRequests'
import {Commits, DiffInfo} from './commits'
import {Commits, DefaultDiffInfo, DiffInfo, convertCommitsToPrs} from './commits'
import {BaseRepository} from '../repositories/BaseRepository'
export interface Options {
@@ -17,7 +17,7 @@ export interface Options {
fetchReviewers: boolean // defines if the action should fetch the reviewers for PRs - approved reviewers are not included in the default PR listing
fetchReleaseInformation: boolean // defines if the action should fetch the release information for the from and to tag - e.g. the creation date for the associated release
fetchReviews: boolean // defines if the action should fetch the reviews for the PR.
commitMode: boolean // defines if we use the alternative commit based mode. note: this is only partially supported
mode: 'PR' | 'COMMIT' | 'HYBRID' // defines the mode used. note: the commit or hybrid modes are not fully supported
configuration: PullConfiguration // the configuration as defined in `configuration.ts`
}
@@ -44,7 +44,7 @@ export class PullRequestCollector {
private fetchReviewers = false,
private fetchReleaseInformation = false,
private fetchReviews = false,
private commitMode = false,
private mode: 'PR' | 'COMMIT' | 'HYBRID' = 'PR',
private configuration: PullConfiguration
) {}
@@ -102,29 +102,37 @@ export class PullRequestCollector {
fetchReviewers: this.fetchReviewers,
fetchReleaseInformation: this.fetchReleaseInformation,
fetchReviews: this.fetchReviews,
commitMode: this.commitMode,
mode: this.mode,
configuration: this.configuration
})
}
}
export async function pullData(repositoryUtils: BaseRepository, options: Options): Promise<Data | null> {
let mergedPullRequests: PullRequestInfo[]
let diffInfo: DiffInfo
let mergedPullRequests: PullRequestInfo[] = []
let diffInfo: DiffInfo = Object.assign({}, DefaultDiffInfo)
const commitsApi = new Commits(repositoryUtils)
if (!options.commitMode) {
core.startGroup(`🚀 Load pull requests`)
core.startGroup(`🚀 Load data`)
if (options.mode === 'COMMIT') {
core.info(`🚀 Load commit history (⚠️ Executing experimental commit mode)`)
const [info, prs] = await commitsApi.generateCommitPRs(options)
mergedPullRequests = mergedPullRequests.concat(prs)
diffInfo = info
} else {
// PR mode, HYBRID mode
core.info(`🚀 Load pull requests`)
const pullRequestsApi = new PullRequests(repositoryUtils, commitsApi)
const [info, prs] = await pullRequestsApi.getMergedPullRequests(options)
mergedPullRequests = prs
diffInfo = info
} else {
core.startGroup(`🚀 Load commit history`)
core.info(`⚠️ Executing experimental commit mode`)
const [info, prs] = await commitsApi.generateCommitPRs(options)
mergedPullRequests = prs
diffInfo = info
if (options.mode === 'HYBRID') {
core.info(`🚀 Converting commits to pull requests`)
const [, fakeCommitPrs] = convertCommitsToPrs(options, info)
mergedPullRequests = mergedPullRequests.concat(fakeCommitPrs)
}
}
core.endGroup()
+1 -1
View File
@@ -205,7 +205,7 @@ export class PullRequests {
if (reviews && (reviews?.length || 0) > 0) {
core.info(`️ Retrieved ${reviews.length || 0} review(s) for PR ${owner}/${repo}/#${pr.number}`)
// backwards compatiblity
// backwards compatibility
pr.approvedReviewers = reviews.filter(r => r.state === 'APPROVED').map(r => r.author)
} else {
core.debug(`No reviewer(s) for PR ${owner}/${repo}/#${pr.number}`)
+91 -19
View File
@@ -1,25 +1,17 @@
import * as core from '@actions/core'
import {Extractor, Property, Regex, RegexTransformer, Transformer} from './types'
import {Extractor, Property, Regex, RegexTransformer} from './types'
export function validateTransformer(transformer?: Regex): RegexTransformer | null {
if (transformer === undefined) {
export function validateRegex(regex?: Regex): RegexTransformer | null {
if (regex === undefined) {
return null
}
try {
let target = undefined
if (transformer.hasOwnProperty('target')) {
target = (transformer as Transformer).target
}
const target = regex.target
const method = regex.method
const onEmpty = regex.on_empty
let onProperty = undefined
let method = undefined
let onEmpty = undefined
if (transformer.hasOwnProperty('method')) {
method = (transformer as Extractor).method
onEmpty = (transformer as Extractor).on_empty
onProperty = (transformer as Extractor).on_property
} else if (transformer.hasOwnProperty('on_property')) {
onProperty = (transformer as Extractor).on_property
if (regex.hasOwnProperty('on_property')) {
onProperty = (regex as Extractor).on_property
}
// legacy handling, transform single value input to array
if (!Array.isArray(onProperty)) {
@@ -28,9 +20,9 @@ export function validateTransformer(transformer?: Regex): RegexTransformer | nul
}
}
return buildRegex(transformer, target, onProperty, method, onEmpty)
return buildRegex(regex, target, onProperty, method, onEmpty)
} catch (e) {
core.warning(`⚠️ Failed to validate transformer: ${transformer.pattern}`)
core.warning(`⚠️ Failed to validate transformer: ${regex.pattern}`)
return null
}
}
@@ -42,7 +34,7 @@ export function buildRegex(
regex: Regex,
target: string | undefined,
onProperty?: Property[] | undefined,
method?: 'replace' | 'match' | undefined,
method?: 'replace' | 'replaceAll' | 'match' | 'regexr' | undefined,
onEmpty?: string | undefined
): RegexTransformer | null {
try {
@@ -58,3 +50,83 @@ export function buildRegex(
return null
}
}
export function transformStringToValues(value: string, extractor: RegexTransformer): string[] | null {
if (extractor.pattern == null) {
return null
}
if (extractor.method === 'regexr') {
const matches = transformRegexr(extractor.pattern, value, extractor.target)
if (matches !== null && matches.size > 0) {
return [...matches]
}
} else if (extractor.method === 'match') {
const matches = value.match(extractor.pattern)
if (matches !== null && matches.length > 0) {
return matches.map(match => match || '')
}
} else if (extractor.method === 'replaceAll') {
const match = value.replaceAll(extractor.pattern, extractor.target)
if (match !== '') {
return [match]
}
} else {
const match = value.replace(extractor.pattern, extractor.target)
if (match !== '') {
return [match]
}
}
if (extractor.onEmpty !== undefined) {
return [extractor.onEmpty]
}
return null
}
export function transformStringToOptionalValue(value: string, extractor: RegexTransformer): string | null {
const result = transformStringToValues(value, extractor)
if (result != null && result.length > 0) {
return result[0]
} else {
return null
}
}
export function transformStringToValue(value: string, extractor: RegexTransformer): string {
return transformStringToOptionalValue(value, extractor) || ''
}
function transformRegexr(regex: RegExp, source: string, target: string): Set<string> | null {
/**
* Util function extracted from regexr and is licensed under:
*
* RegExr: Learn, Build, & Test RegEx
* Copyright (C) 2017 gskinner.com, inc.
* https://github.com/gskinner/regexr/blob/master/dev/src/helpers/BrowserSolver.js#L111-L136
*/
let repl
let ref
if (target.search(/\$[&1-9`']/) === -1) {
target = `$&${target}`
}
const firstOnly = true // for now we don't support multi matches for PRs, future improvement
const adaptedRegex = new RegExp(regex.source, regex.flags.replace('g', ''))
const result = new Set<string>()
do {
ref = source.replace(adaptedRegex, '\b') // bell char - just a placeholder to find
const index = ref.indexOf('\b')
const empty = ref.length > source.length
if (index === -1) {
break
}
repl = source.replace(adaptedRegex, target)
result.add(repl.substr(index, repl.length - ref.length + 1))
source = ref.substr(index + (empty ? 2 : 1))
if (firstOnly) {
break
}
} while (source.length)
return result
}
+11 -11
View File
@@ -2,10 +2,10 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import * as semver from 'semver'
import {SemVer} from 'semver'
import {RegexTransformer, TagResolver, Transformer} from './types'
import {Regex, RegexTransformer, TagResolver} from './types'
import {createCommandManager} from './gitHelper'
import moment from 'moment'
import {validateTransformer} from './regexUtils'
import {transformStringToOptionalValue, transformStringToValue, validateRegex} from './regexUtils'
import {BaseRepository} from '../repositories/BaseRepository'
export interface TagResult {
@@ -88,15 +88,17 @@ export class Tags {
let tags: TagInfo[] = []
if (!toTag || !fromTag) {
const filterRegex = validateRegex(tagResolver.filter)
// filter out tags not matching the specified filter
const filteredTags = filterTags(
// retrieve the tags from the API
await this.getTags(owner, repo, maxTagsToFetch),
tagResolver
filterRegex
)
// check if a transformer, legacy handling, transform single value input to array
let tagTransfomers: Transformer[] | undefined = undefined
let tagTransfomers: Regex[] | undefined = undefined
if (tagResolver.transformer !== undefined) {
if (!Array.isArray(tagResolver.transformer)) {
tagTransfomers = [tagResolver.transformer]
@@ -109,7 +111,7 @@ export class Tags {
let transformedTags: TagInfo[] = filteredTags
if (tagTransfomers !== undefined && tagTransfomers.length > 0) {
for (const transformer of tagTransfomers) {
const tagTransformer = validateTransformer(transformer)
const tagTransformer = validateRegex(transformer)
if (tagTransformer != null) {
core.debug(`️ Using configured tagTransformer (${transformer.pattern})`)
transformedTags = transformTags(transformedTags, tagTransformer)
@@ -196,11 +198,9 @@ export class Tags {
* Uses the provided filter (if available) to filter out any tags not currently relevant.
* https://github.com/mikepenz/release-changelog-builder-action/issues/566
*/
export function filterTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[] {
const filter = tagResolver.filter
if (filter !== undefined) {
const regex = new RegExp(filter.pattern.replace('\\\\', '\\'), filter.flags ?? 'gu')
const filteredTags = tags.filter(tag => tag.name.match(regex) !== null)
export function filterTags(tags: TagInfo[], filterRegex: RegexTransformer | null): TagInfo[] {
if (filterRegex !== null) {
const filteredTags = tags.filter(tag => transformStringToOptionalValue(tag.name, filterRegex) !== null)
core.debug(`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`)
return filteredTags
} else {
@@ -214,7 +214,7 @@ export function filterTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[]
export function transformTags(tags: TagInfo[], transformer: RegexTransformer): TagInfo[] {
return tags.map(function (tag) {
if (transformer.pattern) {
const transformedName = tag.name.replace(transformer.pattern, transformer.target)
const transformedName = transformStringToValue(tag.name, transformer)
core.debug(`️ Transformed ${tag.name} to ${transformedName}`)
return {
tmp: tag.name, // remember the original name
+5 -8
View File
@@ -36,28 +36,25 @@ export interface Sort {
export interface TagResolver {
method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting
transformer?: Transformer | Transformer[] // transforms the tag name using the regex, run after the filter
transformer?: Regex | Regex[] // transforms the tag name using the regex, run after the filter
}
export interface Regex {
pattern: string // the regex pattern to match
flags?: string // the regex flag to use for RegExp
}
export interface Transformer extends Regex {
target?: string // the target string to transform the source string using the regex to
method?: 'replace' | 'replaceAll' | 'match' | 'regexr' | undefined // the method to use to extract the value, `match` will not use the `target` property
on_empty?: string | undefined // in case the regex results in an empty string, this value is gonna be used instead (only for label_extractor currently)
}
export interface Extractor extends Transformer {
export interface Extractor extends Regex {
on_property?: Property[] | Property | undefined // retrieve the property to extract the value from
method?: 'replace' | 'match' | undefined // the method to use to extract the value, `match` will not use the `target` property
on_empty?: string | undefined // in case the regex results in an empty string, this value is gonna be used instead (only for label_extractor currently)
}
export interface RegexTransformer {
pattern: RegExp | null
target: string
onProperty?: Property[]
method?: 'replace' | 'match'
method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
onEmpty?: string
}
+2 -2
View File
@@ -1,13 +1,13 @@
import * as core from '@actions/core'
import {RegexTransformer, Rule} from './pr-collector/types'
import {PullRequestInfo, retrieveProperty} from './pr-collector/pullRequests'
import {validateTransformer} from './pr-collector/regexUtils'
import {validateRegex} from './pr-collector/regexUtils'
/**
* Checks if any of the rules match the given PR
*/
export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: Boolean): boolean {
const transformers: RegexTransformer[] = rules.map(rule => validateTransformer(rule)).filter(t => t !== null) as RegexTransformer[]
const transformers: RegexTransformer[] = rules.map(rule => validateRegex(rule)).filter(t => t !== null) as RegexTransformer[]
if (exhaustive) {
return transformers.every(transformer => {
return matches(pr, transformer, 'rule')
+5 -5
View File
@@ -19,7 +19,7 @@ export interface ReleaseNotesOptions {
fetchReviewers: boolean // defines if the action should fetch the reviewers for PRs - approved reviewers are not included in the default PR listing
fetchReleaseInformation: boolean // defines if the action should fetch the release information for the from and to tag - e.g. the creation date for the associated release
fetchReviews: boolean // defines if the action should fetch the reviews for the PR.
commitMode: boolean // defines if we use the alternative commit based mode. note: this is only partially supported
mode: 'PR' | 'COMMIT' | 'HYBRID' // defines the mode used. note: the commit or hybrid modes are not fully supported
configuration: Configuration // the configuration as defined in `configuration.ts`
repositoryUtils: BaseRepository // the repository implementation used to generate the changelog
}
@@ -46,7 +46,7 @@ export class ReleaseNotesBuilder {
private fetchReviewers = false,
private fetchReleaseInformation = false,
private fetchReviews = false,
private commitMode = false,
private mode: 'PR' | 'COMMIT' | 'HYBRID' = 'PR',
private exportCache = false,
private exportOnly = false,
private cache: string | null = null,
@@ -92,7 +92,7 @@ export class ReleaseNotesBuilder {
this.fetchReviewers,
this.fetchReleaseInformation,
this.fetchReviews,
this.commitMode,
this.mode,
this.configuration
).build()
@@ -110,7 +110,7 @@ export class ReleaseNotesBuilder {
fetchReviewers: this.fetchReviewers,
fetchReleaseInformation: this.fetchReleaseInformation,
fetchReviews: this.fetchReviews,
commitMode: this.commitMode,
mode: this.mode,
configuration: this.configuration,
repositoryUtils: this.repositoryUtils
}
@@ -164,7 +164,7 @@ export class ReleaseNotesBuilder {
fetchReviewers: this.fetchReviewers || orgOptions.fetchReviewers,
fetchReleaseInformation: this.fetchReleaseInformation || orgOptions.fetchReleaseInformation,
fetchReviews: this.fetchReviews || orgOptions.fetchReviews,
commitMode: this.commitMode || orgOptions.commitMode,
mode: this.mode || orgOptions.mode,
configuration: this.configuration || orgOptions.configuration,
repositoryUtils: this.repositoryUtils || orgOptions.repositoryUtils
}
+1 -1
View File
@@ -74,7 +74,7 @@ export class GiteaRepository extends BaseRepository {
mergeCommitSha: pr.merge_commit_sha || '',
author: pr.user?.login || '',
repoName: pr.base?.repo?.full_name || '',
labels: pr.labels?.map(label => label.name) as string[],
labels: pr.labels?.map(label => label.name?.toLowerCase()) as string[],
milestone: pr.milestone?.title || '',
body: pr.body || '',
assignees: pr.assignees?.map(user => user.full_name) as string[],
+2 -2
View File
@@ -59,7 +59,7 @@ export class GithubRepository extends BaseRepository {
sha: commit.sha || '',
summary: commit.commit.message.split('\n')[0],
message: commit.commit.message,
author: commit.author?.login || '',
author: commit.author?.login || commit.commit.author?.name || '',
authorDate: moment(commit.commit.author?.date),
committer: commit.committer?.login || '',
commitDate: moment(commit.commit.committer?.date),
@@ -273,7 +273,7 @@ export class GithubRepository extends BaseRepository {
labels: this.attachSpecialLabels(status, pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || []),
milestone: pr.milestone?.title || '',
body: pr.body || '',
assignees: pr.assignees?.map(asignee => asignee?.login || '') || [],
assignees: pr.assignees?.map(assignee => assignee?.login || '') || [],
requestedReviewers: pr.requested_reviewers?.map(reviewer => reviewer?.login || '') || [],
approvedReviewers: [],
reviews: undefined,
+179 -106
View File
@@ -10,12 +10,17 @@ import {
sortPullRequests
} from './pr-collector/pullRequests'
import {DiffInfo} from './pr-collector/commits'
import {validateTransformer} from './pr-collector/regexUtils'
import {RegexTransformer, Transformer} from './pr-collector/types'
import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils'
import {Regex, RegexTransformer} from './pr-collector/types'
import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {matchesRules} from './regexUtils'
const EMPTY_MAP = new Map<string, string>()
let CLEAR = false
export function clear(): void {
CLEAR = true
}
export interface PullRequestData extends PullRequestInfo {
childPrs?: PullRequestInfo[]
@@ -40,7 +45,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
// establish parent child PR relations
if (config.reference !== undefined) {
const reference = validateTransformer(config.reference)
const reference = validateRegex(config.reference)
if (reference !== null) {
core.info(`️ Identifying PR references using \`reference\``)
@@ -77,14 +82,14 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
// drop duplicate pull requests
if (config.duplicate_filter !== undefined) {
const extractor = validateTransformer(config.duplicate_filter)
const extractor = validateRegex(config.duplicate_filter)
if (extractor !== null) {
core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``)
const deduplicatedMap = new Map<string, PullRequestInfo>()
const unmatched: PullRequestInfo[] = []
for (const pr of prs) {
const extracted = extractValues(pr, extractor, 'dupliate_filter')
const extracted = extractValues(pr, extractor, 'duplicate_filter')
if (extracted !== null && extracted.length > 0) {
deduplicatedMap.set(extracted[0], pr)
} else {
@@ -111,7 +116,6 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
for (const label of extracted) {
pr.labels.push(label)
}
if (core.isDebug()) {
core.debug(` Extracted the following labels (${JSON.stringify(extracted)}) for PR ${pr.number}`)
}
@@ -136,21 +140,24 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
core.info(`✒️ Wrote messages for ${prs.length} pull requests`)
// bring PRs into the order of categories
const categorized = new Map<Category, string[]>()
const categories = config.categories
const ignoredLabels = config.ignore_labels
for (const category of categories) {
categorized.set(category, [])
}
const flatCategories = flatten(config.categories)
const categorizedPrs: string[] = []
const ignoredPrs: string[] = []
const openPrs: string[] = []
const uncategorizedPrs: string[] = []
// set-up the category object
for (const category of flatCategories) {
if (CLEAR || !category.entries) {
category.entries = []
}
}
// bring elements in order
for (const [pr, body] of transformedMap) {
prLoop: for (const [pr, body] of transformedMap) {
if (
haveCommonElementsArr(
ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')),
@@ -166,67 +173,18 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
}
let matchedOnce = false // in case we matched once at least, the PR can't be uncategorized
for (const [category, pullRequests] of categorized) {
let matched = false // check if we matched within the given category
// check if any exclude label matches
if (category.exclude_labels !== undefined) {
if (
haveCommonElementsArr(
category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels
)
) {
if (core.isDebug()) {
const excludeLabels = JSON.stringify(category.exclude_labels)
core.debug(` PR ${pr.number} with labels: ${pr.labels} excluded from category via exclude label: ${excludeLabels}`)
}
continue // one of the exclude labels matched, skip the PR for this category
}
}
// in case we have exhaustive matching enabled, and have labels and/or rules
// validate for an exhaustive match (e.g. every provided rule applies)
if (category.exhaustive === true && (category.labels !== undefined || category.rules !== undefined)) {
if (category.labels !== undefined) {
matched = haveEveryElementsArr(
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels
)
}
let exhaustive_rules = true
if (category.exhaustive_rules !== undefined) {
exhaustive_rules = category.exhaustive_rules
}
if ((matched || category.labels === undefined) && category.rules !== undefined) {
matched = matchesRules(category.rules, pr, exhaustive_rules)
}
} else {
// if not exhaustive, do individual matches
if (category.labels !== undefined) {
// check if either any of the labels applies
matched = haveCommonElementsArr(
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels
)
}
let exhaustive_rules = false
if (category.exhaustive_rules !== undefined) {
exhaustive_rules = category.exhaustive_rules
}
if (!matched && category.rules !== undefined) {
// if no label did apply, check if any rule applies
matched = matchesRules(category.rules, pr, exhaustive_rules)
}
}
if (matched) {
pullRequests.push(body) // if matched add the PR to the list
for (const category of categories) {
const [matched, consumed] = recursiveCategorizePr(category, pr, body)
if (consumed) {
continue prLoop
}
matchedOnce = matchedOnce || matched
}
if (!matchedOnce) {
// we allow to have pull requests included in an "uncategorized" category
for (const [category, pullRequests] of categorized) {
for (const category of flatCategories) {
const pullRequests = category.entries || []
if ((category.labels === undefined || category.labels.length === 0) && category.rules === undefined) {
// check if any exclude label matches for the "uncategorized" category
if (category.exclude_labels !== undefined) {
@@ -260,30 +218,17 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
core.info(`️ Ordered all pull requests into ${categories.length} categories`)
// serialize and provide the categorized content as json
const transformedCategorized = Array.from(categorized).reduce(
(obj, [key, value]) => Object.assign(obj, {[key.key || key.title]: value}),
{}
)
const transformedCategorized = {}
for (const category of flatCategories) {
Object.assign(transformedCategorized, {[category.key || category.title]: category.entries})
}
core.setOutput('categorized', JSON.stringify(transformedCategorized))
// construct final changelog
let changelog = ''
for (const [category, pullRequests] of categorized) {
if (pullRequests.length > 0) {
if (category.title) {
changelog = `${changelog + category.title}\n\n`
}
for (const pr of pullRequests) {
changelog = `${changelog + pr}\n`
}
changelog = `${changelog}\n` // add space between sections
} else if (category.empty_content !== undefined) {
if (category.title) {
changelog = `${changelog + category.title}\n\n`
}
changelog = `${changelog + category.empty_content}\n\n`
}
for (const category of flatCategories) {
const pullRequests = category.entries || []
changelog = attachCategoryChangelog(changelog, category, pullRequests)
}
core.info(`✒️ Wrote ${categorizedPrs.length} categorized pull requests down`)
if (core.isDebug()) {
@@ -330,12 +275,25 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
}
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`)
// collect all contributors
const contributorsSet: Set<String> = new Set()
for (const pr of prs) {
contributorsSet.add(`@${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))
// fill template
const placeholderMap = new Map<string, string>()
placeholderMap.set('CHANGELOG', changelog)
placeholderMap.set('UNCATEGORIZED', changelogUncategorized)
placeholderMap.set('OPEN', changelogOpen)
placeholderMap.set('IGNORED', changelogIgnored)
// fill special collected contributors
placeholderMap.set('CONTRIBUTORS', contributorsString)
placeholderMap.set('EXTERNAL_CONTRIBUTORS', externalContributorString)
// fill other placeholders
placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString())
placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString())
@@ -359,6 +317,109 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
return transformedChangelog
}
function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: string): boolean[] {
let matched = false
let consumed = false
const matchesParent = categorizePr(category, pr)
// only do children if parent also matches
if (category.categories && matchesParent) {
for (const childCategory of category.categories) {
const [childMatched, childConsumed] = recursiveCategorizePr(childCategory, pr, body)
matched = matched || childMatched // at least one time it matched
consumed = childConsumed
}
}
// if consumed we don't handle it anymore, as it was matched in a child, don't handle anymore
if (!consumed && !matched) {
const pullRequests = category.entries || []
matched = matchesParent
if (matched) {
pullRequests.push(body) // if matched add the PR to the list
}
}
if (matched && category.consume) {
consumed = true
}
return [matched, consumed]
}
function categorizePr(category: Category, pr: PullRequestInfo): boolean {
let matched = false // check if we matched within the given category
// check if any exclude label matches
if (category.exclude_labels !== undefined) {
if (
haveCommonElementsArr(
category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels
)
) {
if (core.isDebug()) {
const excludeLabels = JSON.stringify(category.exclude_labels)
core.debug(` PR ${pr.number} with labels: ${pr.labels} excluded from category via exclude label: ${excludeLabels}`)
}
return false // one of the exclude labels matched, skip the PR for this category
}
}
// in case we have exhaustive matching enabled, and have labels and/or rules
// validate for an exhaustive match (e.g. every provided rule applies)
if (category.exhaustive === true && (category.labels !== undefined || category.rules !== undefined)) {
if (category.labels !== undefined) {
matched = haveEveryElementsArr(
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels
)
}
let exhaustive_rules = true
if (category.exhaustive_rules !== undefined) {
exhaustive_rules = category.exhaustive_rules
}
if ((matched || category.labels === undefined) && category.rules !== undefined) {
matched = matchesRules(category.rules, pr, exhaustive_rules)
}
} else {
// if not exhaustive, do individual matches
if (category.labels !== undefined) {
// check if either any of the labels applies
matched = haveCommonElementsArr(
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels
)
}
let exhaustive_rules = false
if (category.exhaustive_rules !== undefined) {
exhaustive_rules = category.exhaustive_rules
}
if (!matched && category.rules !== undefined) {
// if no label did apply, check if any rule applies
matched = matchesRules(category.rules, pr, exhaustive_rules)
}
}
return matched
}
function attachCategoryChangelog(changelog: string, category: Category, pullRequests: string[]): string {
if (pullRequests.length > 0 || hasChildWithEntries(category)) {
if (category.title) {
changelog = `${changelog + category.title}\n\n`
}
for (const pr of pullRequests) {
changelog = `${changelog + pr}\n`
}
changelog = `${changelog}\n` // add space between sections
} else if (category.empty_content !== undefined) {
if (category.title) {
changelog = `${changelog + category.title}\n\n`
}
changelog = `${changelog + category.empty_content}\n\n`
}
return changelog
}
export function replaceEmptyTemplate(template: string, options: ReleaseNotesOptions): string {
const placeholders = new Map<string, Placeholder[]>()
for (const ph of options.configuration.custom_placeholders || []) {
@@ -461,11 +522,12 @@ function handlePlaceholder(
const phs = placeholders.get(key)
if (phs) {
for (const placeholder of phs) {
const transformer = validateTransformer(placeholder.transformer)
const transformer = validateRegex(placeholder.transformer)
if (transformer?.pattern) {
const extractedValue = value.replace(transformer.pattern, transformer.target)
const extractedValue = transformStringToOptionalValue(value, transformer)
// note: `.replace` will return the full string again if there was no match
if (extractedValue && (extractedValue !== value || (extractedValue === value && value.match(transformer.pattern)))) {
// note: This is mostly backwards compatibility
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
if (placeholderPrMap) {
createOrSet(placeholderPrMap, placeholder.name, extractedValue)
}
@@ -475,7 +537,7 @@ function handlePlaceholder(
)
if (core.isDebug()) {
core.debug(` Custom Placeholder successfully matched data - ${extractValues} (${placeholder.name})`)
core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`)
}
} else if (core.isDebug() && extractedValue === value) {
core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`)
@@ -580,11 +642,11 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
return transformed
}
function validateTransformers(specifiedTransformers: Transformer[]): RegexTransformer[] {
function validateTransformers(specifiedTransformers: Regex[]): RegexTransformer[] {
const transformers = specifiedTransformers
return transformers
.map(transformer => {
return validateTransformer(transformer)
return validateRegex(transformer)
})
.filter(transformer => transformer?.pattern != null)
.map(transformer => {
@@ -619,20 +681,31 @@ function extractValuesFromString(value: string, extractor: RegexTransformer): st
if (extractor.pattern == null) {
return null
}
if (extractor.method === 'match') {
const lables = value.match(extractor.pattern)
if (lables !== null && lables.length > 0) {
return lables.map(label => label?.toLocaleLowerCase('en') || '')
}
const transformed = transformStringToValues(value, extractor)
if (transformed) {
return transformed.map(val => val?.toLocaleLowerCase('en') || '')
} else {
const label = value.replace(extractor.pattern, extractor.target)
if (label !== '') {
return [label.toLocaleLowerCase('en')]
}
return null
}
if (extractor.onEmpty !== undefined) {
return [extractor.onEmpty.toLocaleLowerCase('en')]
}
function flatten(categories?: Category[]): Category[] {
if (!categories) {
return []
}
return null
return categories.reduce(function (r: Category[], i) {
return r.concat([i]).concat(flatten(i.categories))
}, [])
}
function hasChildWithEntries(category: Category): boolean {
const categories = category.categories
if (!categories || categories.length === 0) {
return (category.entries?.length || 0) > 0
}
let hasEntries = false
for (const cat of categories) {
hasEntries = hasEntries || hasChildWithEntries(cat)
}
return hasEntries
}
+45 -20
View File
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as path from 'path'
import {Configuration, DefaultConfiguration} from './configuration'
import {Configuration, DefaultCommitConfiguration, DefaultConfiguration} from './configuration'
import moment from 'moment'
import {DiffInfo} from './pr-collector/commits'
import {PullRequestInfo} from './pr-collector/pullRequests'
@@ -117,6 +117,24 @@ export function checkExportedData(exportCache: boolean, cacheInput: string | nul
}
}
export function resolveMode(mode: string | undefined, commitMode: boolean): 'PR' | 'COMMIT' | 'HYBRID' {
if (commitMode === false || mode === undefined) {
if (commitMode === true) {
return 'COMMIT'
} else {
return 'PR'
}
} else {
const upperCaseMode = mode.toUpperCase()
if (upperCaseMode === 'COMMIT') {
return 'COMMIT'
} else if (upperCaseMode === 'HYBRID') {
return 'HYBRID'
}
}
return 'PR'
}
/**
* Retrieves the configuration given the file path, if not found it will fallback to the `DefaultConfiguration`
*/
@@ -161,7 +179,7 @@ function readConfiguration(filename: string): Configuration | undefined {
*/
export function parseConfiguration(config: string): Configuration | undefined {
try {
// for compatiblity with the `yml` file we require to use `#{{}}` instead of `${{}}` - replace it here.
// for compatibility with the `yml` file we require to use `#{{}}` instead of `${{}}` - replace it here.
const configurationJSON: Configuration = JSON.parse(config.replace(/\${{/g, '#{{'))
return configurationJSON
} catch (error) {
@@ -173,25 +191,32 @@ export function parseConfiguration(config: string): Configuration | undefined {
/**
* Merges the configurations, will fallback to the DefaultConfiguration value
*/
export function mergeConfiguration(jc?: Configuration, fc?: Configuration): Configuration {
export function mergeConfiguration(jc?: Configuration, fc?: Configuration, mode?: 'PR' | 'COMMIT' | 'HYBRID'): Configuration {
let def: Configuration
if (mode === 'COMMIT') {
def = DefaultCommitConfiguration
} else {
def = DefaultConfiguration
}
return {
max_tags_to_fetch: jc?.max_tags_to_fetch || fc?.max_tags_to_fetch || DefaultConfiguration.max_tags_to_fetch,
max_pull_requests: jc?.max_pull_requests || fc?.max_pull_requests || DefaultConfiguration.max_pull_requests,
max_back_track_time_days: jc?.max_back_track_time_days || fc?.max_back_track_time_days || DefaultConfiguration.max_back_track_time_days,
exclude_merge_branches: jc?.exclude_merge_branches || fc?.exclude_merge_branches || DefaultConfiguration.exclude_merge_branches,
sort: jc?.sort || fc?.sort || DefaultConfiguration.sort,
template: jc?.template || fc?.template || DefaultConfiguration.template,
pr_template: jc?.pr_template || fc?.pr_template || DefaultConfiguration.pr_template,
empty_template: jc?.empty_template || fc?.empty_template || DefaultConfiguration.empty_template,
categories: jc?.categories || fc?.categories || DefaultConfiguration.categories,
ignore_labels: jc?.ignore_labels || fc?.ignore_labels || DefaultConfiguration.ignore_labels,
label_extractor: jc?.label_extractor || fc?.label_extractor || DefaultConfiguration.label_extractor,
duplicate_filter: jc?.duplicate_filter || fc?.duplicate_filter || DefaultConfiguration.duplicate_filter,
transformers: jc?.transformers || fc?.transformers || DefaultConfiguration.transformers,
tag_resolver: jc?.tag_resolver || fc?.tag_resolver || DefaultConfiguration.tag_resolver,
base_branches: jc?.base_branches || fc?.base_branches || DefaultConfiguration.base_branches,
custom_placeholders: jc?.custom_placeholders || fc?.custom_placeholders || DefaultConfiguration.custom_placeholders,
trim_values: jc?.trim_values || fc?.trim_values || DefaultConfiguration.trim_values
max_tags_to_fetch: jc?.max_tags_to_fetch || fc?.max_tags_to_fetch || def.max_tags_to_fetch,
max_pull_requests: jc?.max_pull_requests || fc?.max_pull_requests || def.max_pull_requests,
max_back_track_time_days: jc?.max_back_track_time_days || fc?.max_back_track_time_days || def.max_back_track_time_days,
exclude_merge_branches: jc?.exclude_merge_branches || fc?.exclude_merge_branches || def.exclude_merge_branches,
sort: jc?.sort || fc?.sort || def.sort,
template: jc?.template || fc?.template || def.template,
pr_template: jc?.pr_template || fc?.pr_template || def.pr_template,
empty_template: jc?.empty_template || fc?.empty_template || def.empty_template,
categories: jc?.categories || fc?.categories || def.categories,
ignore_labels: jc?.ignore_labels || fc?.ignore_labels || def.ignore_labels,
label_extractor: jc?.label_extractor || fc?.label_extractor || def.label_extractor,
duplicate_filter: jc?.duplicate_filter || fc?.duplicate_filter || def.duplicate_filter,
transformers: jc?.transformers || fc?.transformers || def.transformers,
tag_resolver: jc?.tag_resolver || fc?.tag_resolver || def.tag_resolver,
base_branches: jc?.base_branches || fc?.base_branches || def.base_branches,
custom_placeholders: jc?.custom_placeholders || fc?.custom_placeholders || def.custom_placeholders,
trim_values: jc?.trim_values || fc?.trim_values || def.trim_values
}
}
+1 -1
View File
@@ -9,5 +9,5 @@
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"lib": [ "ES2021.String", "dom"] /* Enable custom `ES2021.String` extension in typescript for `replaceAll` */
},
"exclude": ["node_modules", "**/*.test.ts", "**/**/*.test.ts", "src/pr-collector"],
"exclude": ["node_modules", "__tests__/*.ts", "**/*.test.ts", "**/**/*.test.ts"],
}