Merge pull request #50 from mikepenz/feature/dynamic_placeholders

New additional placeholders for `template` and `empty_template`
This commit is contained in:
Mike Penz
2020-10-19 18:13:13 +02:00
committed by GitHub
8 changed files with 168 additions and 47 deletions
+27 -20
View File
@@ -213,7 +213,7 @@ For advanced use cases additional settings can be provided to the action
### PR Template placeholders ### PR Template placeholders
Table of supported placeholders allowed to be used in the `template` configuration. Table of supported placeholders allowed to be used in the `pr_template` configuration.
| **Placeholder** | **Description** | | **Placeholder** | **Description** |
|------------------|-------------------------------------------------------------| |------------------|-------------------------------------------------------------|
@@ -230,33 +230,40 @@ Table of supported placeholders allowed to be used in the `template` configurati
### Template placeholders ### Template placeholders
Table of supported placeholders allowed to be used in the `pr_template` configuration. Table of supported placeholders allowed to be used in the `template` and `empty_template` (only supports placeholder marked for empty) configuration.
| **Placeholder** | **Description** | **Empty** |
|----------------------------|----------------------------------------------------------------------------------------------------|:---------:|
| `${{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration | |
| `${{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories | |
| `${{OWNER}}` | Describes the owner of the repository the changelog was generated for | x |
| `${{REPO}}` | The repository name of the repo the changelog was generated for | x |
| `${{FROM_TAG}}` | Defines the 'start' from where the changelog did consider merged pull requests | x |
| `${{TO_TAG}}` | Defines until which tag the changelog did consider merged pull requests | x |
| `${{CATEGORIZED_COUNT}}` | The count of PRs which were categorized | |
| `${{UNCATEGORIZED_COUNT}}` | The count of PRs and changes which were not categorized. No label overlapping with category labels | |
| **Placeholder** | **Description** |
|----------------------|-------------------------------------------------------------------------------------------------|
| `${{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration |
| `${{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories |
### Configuration Specification ### Configuration Specification
Table of descriptions for the `configuration.json` options. Table of descriptions for the `configuration.json` options.
| **Input** | **Description** | | **Input** | **Description** |
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| categories | An array of `category` specifications, offering a flexible way to group changes into categories | | categories | An array of `category` specifications, offering a flexible way to group changes into categories |
| category.title | The display name of a category in the changelog | | category.title | The display name of a category in the changelog |
| category.labels | An array of labels, to match pull request labels against. If any PR label, matches any category label, the pull request will show up under this category | | category.labels | An array of labels, to match pull request labels against. If any PR label, matches any category label, the pull request will show up under this category |
| sort | The sort order of pull requests. [ASC, DESC] | | sort | The sort order of pull requests. [ASC, DESC] |
| template | Specifies the global template to pick for creating the changelog. See [Template placeholders](#template-placeholders) for possible values | | template | Specifies the global template to pick for creating the changelog. See [Template placeholders](#template-placeholders) for possible values |
| pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values | | pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values |
| empty_template | Template to pick if no changes are detected. Does not support placeholders | | empty_template | Template to pick if no changes are detected. See [Template placeholders](#template-placeholders) for possible values |
| transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified | | transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified |
| transformer.pattern | A `regex` pattern, extracting values of the change message. | | transformer.pattern | A `regex` pattern, extracting values of the change message. |
| transformer.target | The result pattern, the regex groups will be filled into. Allows for full transformation of a pull request message. Including potentially specified texts | | transformer.target | The result pattern, the regex groups will be filled into. Allows for full transformation of a pull request message. Including potentially specified texts |
| max_tags_to_fetch | The maximum amount of tags to load from the API to find the previous tag. Loaded paginated with 100 per page | | max_tags_to_fetch | The maximum amount of tags to load from the API to find the previous tag. Loaded paginated with 100 per page |
| max_pull_requests | The maximum amount of pull requests to load from the API. Loaded paginated with 30 per page | | max_pull_requests | The maximum amount of pull requests to load from the API. Loaded paginated with 30 per page |
| max_back_track_time_days | Defines the max amount of days to go back in time per changelog | | max_back_track_time_days | Defines the max amount of days to go back in time per changelog |
| exclude_merge_branches | An array of branches to be ignored from processing as merge commits | | exclude_merge_branches | An array of branches to be ignored from processing as merge commits |
## Contribute 🧬 ## Contribute 🧬
+63 -13
View File
@@ -1,19 +1,6 @@
import { resolveConfiguration } from '../src/utils'; import { resolveConfiguration } from '../src/utils';
import { ReleaseNotesBuilder } from '../src/releaseNotesBuilder'; import { ReleaseNotesBuilder } from '../src/releaseNotesBuilder';
// shows how the runner will run a javascript action with env / stdout protocol
/*
test('test runs', () => {
jest.setTimeout(180000);
process.env['INPUT_CONFIGURATION'] = 'configuration.json'
const ip = path.join(__dirname, '..', 'lib', 'main.js')
const options: cp.ExecSyncOptions = {
env: process.env
}
console.log(cp.execSync(`node ${ip}`, options).toString())
})
*/
it('Should match generated changelog (unspecified fromTag)', async () => { it('Should match generated changelog (unspecified fromTag)', async () => {
jest.setTimeout(180000) jest.setTimeout(180000)
@@ -39,3 +26,66 @@ it('Should match generated changelog (unspecified fromTag)', async () => {
`) `)
}) })
it('Should use empty placeholder', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs/configuration.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
'v0.0.2',
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`- no changes`)
})
it('Should fill empty placeholders', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_empty_all_placeholders.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
'v0.0.2',
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`mikepenz\nrelease-changelog-builder-action\nv0.0.2\nv0.0.3`)
})
it('Should fill `template` placeholders', async () => {
jest.setTimeout(180000)
const configuration = resolveConfiguration('', 'configs_test/configuration_empty_all_placeholders.json')
const releaseNotesBuilder = new ReleaseNotesBuilder(
null,
'.',
'mikepenz',
'release-changelog-builder-action',
'v0.0.1',
'v0.0.3',
false,
false,
configuration
)
const changeLog = await releaseNotesBuilder.build()
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests\n\n- [CI] Specify Test Case\n - PR: #10\n\n\n\nmikepenz\nrelease-changelog-builder-action\nv0.0.1\nv0.0.3\n1\n0`)
})
@@ -0,0 +1,4 @@
{
"template": "${{CHANGELOG}}\n${{UNCATEGORIZED}}\n${{OWNER}}\n${{REPO}}\n${{FROM_TAG}}\n${{TO_TAG}}\n${{CATEGORIZED_COUNT}}\n${{UNCATEGORIZED_COUNT}}",
"empty_template": "${{OWNER}}\n${{REPO}}\n${{FROM_TAG}}\n${{TO_TAG}}"
}
Generated Vendored
+26 -7
View File
@@ -590,7 +590,7 @@ class ReleaseNotes {
return null; return null;
} }
core.startGroup('📦 Build changelog'); core.startGroup('📦 Build changelog');
const resultChangelog = transform_1.buildChangelog(mergedPullRequests, configuration); const resultChangelog = transform_1.buildChangelog(mergedPullRequests, configuration, this.options);
core.endGroup(); core.endGroup();
return resultChangelog; return resultChangelog;
}); });
@@ -708,6 +708,7 @@ const utils_1 = __webpack_require__(918);
const rest_1 = __webpack_require__(5375); const rest_1 = __webpack_require__(5375);
const tags_1 = __webpack_require__(7532); const tags_1 = __webpack_require__(7532);
const releaseNotes_1 = __webpack_require__(5882); const releaseNotes_1 = __webpack_require__(5882);
const transform_1 = __webpack_require__(1644);
class ReleaseNotesBuilder { class ReleaseNotesBuilder {
constructor(token, repositoryPath, owner, repo, fromTag, toTag, failOnError, ignorePreReleases, configuration) { constructor(token, repositoryPath, owner, repo, fromTag, toTag, failOnError, ignorePreReleases, configuration) {
this.token = token; this.token = token;
@@ -781,17 +782,18 @@ class ReleaseNotesBuilder {
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`); core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`);
core.endGroup(); core.endGroup();
} }
const releaseNotes = new releaseNotes_1.ReleaseNotes(octokit, { const options = {
owner: this.owner, owner: this.owner,
repo: this.repo, repo: this.repo,
fromTag: this.fromTag, fromTag: this.fromTag,
toTag: this.toTag, toTag: this.toTag,
failOnError: this.failOnError, failOnError: this.failOnError,
configuration: this.configuration configuration: this.configuration
}); };
const releaseNotes = new releaseNotes_1.ReleaseNotes(octokit, options);
return ((yield releaseNotes.pull()) || return ((yield releaseNotes.pull()) ||
this.configuration.empty_template || transform_1.fillAdditionalPlaceholders(this.configuration.empty_template ||
configuration_1.DefaultConfiguration.empty_template); configuration_1.DefaultConfiguration.empty_template, options));
}); });
} }
} }
@@ -976,11 +978,11 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.buildChangelog = void 0; exports.fillAdditionalPlaceholders = exports.buildChangelog = void 0;
const pullRequests_1 = __webpack_require__(4217); const pullRequests_1 = __webpack_require__(4217);
const core = __importStar(__webpack_require__(2186)); const core = __importStar(__webpack_require__(2186));
const configuration_1 = __webpack_require__(5527); const configuration_1 = __webpack_require__(5527);
function buildChangelog(prs, config) { function buildChangelog(prs, config, options) {
// sort to target order // sort to target order
const sort = config.sort || configuration_1.DefaultConfiguration.sort; const sort = config.sort || configuration_1.DefaultConfiguration.sort;
const sortAsc = sort.toUpperCase() === 'ASC'; const sortAsc = sort.toUpperCase() === 'ASC';
@@ -1000,6 +1002,7 @@ function buildChangelog(prs, config) {
for (const category of categories) { for (const category of categories) {
categorized.set(category, []); categorized.set(category, []);
} }
const categorizedPrs = [];
const uncategorized = []; const uncategorized = [];
// bring elements in order // bring elements in order
for (const [pr, body] of transformedMap) { for (const [pr, body] of transformedMap) {
@@ -1013,6 +1016,9 @@ function buildChangelog(prs, config) {
if (!matched) { if (!matched) {
uncategorized.push(body); uncategorized.push(body);
} }
else {
categorizedPrs.push(body);
}
} }
core.info(`️ Ordered all pull requests into ${categories.length} categories`); core.info(`️ Ordered all pull requests into ${categories.length} categories`);
// construct final changelog // construct final changelog
@@ -1037,10 +1043,23 @@ function buildChangelog(prs, config) {
let transformedChangelog = config.template || configuration_1.DefaultConfiguration.template; let transformedChangelog = config.template || configuration_1.DefaultConfiguration.template;
transformedChangelog = transformedChangelog.replace('${{CHANGELOG}}', changelog); transformedChangelog = transformedChangelog.replace('${{CHANGELOG}}', changelog);
transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED}}', changelogUncategorized); transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED}}', changelogUncategorized);
// fill other placeholders
transformedChangelog = transformedChangelog.replace('${{CATEGORIZED_COUNT}}', categorizedPrs.length.toString());
transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED_COUNT}}', uncategorized.length.toString());
transformedChangelog = fillAdditionalPlaceholders(transformedChangelog, options);
core.info(`️ Filled template`); core.info(`️ Filled template`);
return transformedChangelog; return transformedChangelog;
} }
exports.buildChangelog = buildChangelog; exports.buildChangelog = buildChangelog;
function fillAdditionalPlaceholders(text, options) {
let transformed = text;
transformed = transformed.replace('${{OWNER}}', options.owner);
transformed = transformed.replace('${{REPO}}', options.repo);
transformed = transformed.replace('${{FROM_TAG}}', options.fromTag);
transformed = transformed.replace('${{TO_TAG}}', options.toTag);
return transformed;
}
exports.fillAdditionalPlaceholders = fillAdditionalPlaceholders;
function haveCommonElements(arr1, arr2) { function haveCommonElements(arr1, arr2) {
return arr1.some(item => arr2.includes(item)); return arr1.some(item => arr2.includes(item));
} }
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -31,7 +31,11 @@ export class ReleaseNotes {
} }
core.startGroup('📦 Build changelog') core.startGroup('📦 Build changelog')
const resultChangelog = buildChangelog(mergedPullRequests, configuration) const resultChangelog = buildChangelog(
mergedPullRequests,
configuration,
this.options
)
core.endGroup() core.endGroup()
return resultChangelog return resultChangelog
} }
+9 -4
View File
@@ -6,6 +6,7 @@ import {failOrError} from './utils'
import {Octokit} from '@octokit/rest' import {Octokit} from '@octokit/rest'
import {Tags} from './tags' import {Tags} from './tags'
import {ReleaseNotes} from './releaseNotes' import {ReleaseNotes} from './releaseNotes'
import {fillAdditionalPlaceholders} from './transform'
export class ReleaseNotesBuilder { export class ReleaseNotesBuilder {
constructor( constructor(
@@ -96,19 +97,23 @@ export class ReleaseNotesBuilder {
core.endGroup() core.endGroup()
} }
const releaseNotes = new ReleaseNotes(octokit, { const options = {
owner: this.owner, owner: this.owner,
repo: this.repo, repo: this.repo,
fromTag: this.fromTag, fromTag: this.fromTag,
toTag: this.toTag, toTag: this.toTag,
failOnError: this.failOnError, failOnError: this.failOnError,
configuration: this.configuration configuration: this.configuration
}) }
const releaseNotes = new ReleaseNotes(octokit, options)
return ( return (
(await releaseNotes.pull()) || (await releaseNotes.pull()) ||
this.configuration.empty_template || fillAdditionalPlaceholders(
DefaultConfiguration.empty_template this.configuration.empty_template ||
DefaultConfiguration.empty_template,
options
)
) )
} }
} }
+33 -1
View File
@@ -1,5 +1,6 @@
import {PullRequestInfo, sortPullRequests} from './pullRequests' import {PullRequestInfo, sortPullRequests} from './pullRequests'
import * as core from '@actions/core' import * as core from '@actions/core'
import {ReleaseNotesOptions} from './releaseNotes'
import { import {
Category, Category,
Configuration, Configuration,
@@ -9,7 +10,8 @@ import {
export function buildChangelog( export function buildChangelog(
prs: PullRequestInfo[], prs: PullRequestInfo[],
config: Configuration config: Configuration,
options: ReleaseNotesOptions
): string { ): string {
// sort to target order // sort to target order
const sort = config.sort || DefaultConfiguration.sort const sort = config.sort || DefaultConfiguration.sort
@@ -43,6 +45,7 @@ export function buildChangelog(
for (const category of categories) { for (const category of categories) {
categorized.set(category, []) categorized.set(category, [])
} }
const categorizedPrs: string[] = []
const uncategorized: string[] = [] const uncategorized: string[] = []
// bring elements in order // bring elements in order
@@ -58,6 +61,8 @@ export function buildChangelog(
if (!matched) { if (!matched) {
uncategorized.push(body) uncategorized.push(body)
} else {
categorizedPrs.push(body)
} }
} }
core.info(`️ Ordered all pull requests into ${categories.length} categories`) core.info(`️ Ordered all pull requests into ${categories.length} categories`)
@@ -96,10 +101,37 @@ export function buildChangelog(
'${{UNCATEGORIZED}}', '${{UNCATEGORIZED}}',
changelogUncategorized changelogUncategorized
) )
// fill other placeholders
transformedChangelog = transformedChangelog.replace(
'${{CATEGORIZED_COUNT}}',
categorizedPrs.length.toString()
)
transformedChangelog = transformedChangelog.replace(
'${{UNCATEGORIZED_COUNT}}',
uncategorized.length.toString()
)
transformedChangelog = fillAdditionalPlaceholders(
transformedChangelog,
options
)
core.info(`️ Filled template`) core.info(`️ Filled template`)
return transformedChangelog return transformedChangelog
} }
export function fillAdditionalPlaceholders(
text: string,
options: ReleaseNotesOptions
): string {
let transformed = text
transformed = transformed.replace('${{OWNER}}', options.owner)
transformed = transformed.replace('${{REPO}}', options.repo)
transformed = transformed.replace('${{FROM_TAG}}', options.fromTag)
transformed = transformed.replace('${{TO_TAG}}', options.toTag)
return transformed
}
function haveCommonElements(arr1: string[], arr2: string[]): Boolean { function haveCommonElements(arr1: string[], arr2: string[]): Boolean {
return arr1.some(item => arr2.includes(item)) return arr1.some(item => arr2.includes(item))
} }