Merge pull request #837 from mikepenz/feature/custom_placeholders
Introduce experimental support for CUSTOM PLACEHOLDERS
This commit is contained in:
@@ -366,7 +366,7 @@ Table of supported placeholders allowed to be used in the `template` and `empty_
|
||||
| `${{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. | * |
|
||||
| `${{DAYS_SINCE}}` | Days between the 2 releases. Requires `fetchReleaseInformation` to be enabled. | x |
|
||||
|
||||
### Configuration Specification
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {resolveConfiguration} from '../src/utils'
|
||||
import {ReleaseNotesBuilder} from '../src/releaseNotesBuilder'
|
||||
import { TagInfo, sortTags, filterTags } from '../src/tags';
|
||||
|
||||
jest.setTimeout(180000)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {buildChangelog} from '../src/transform'
|
||||
import {PullRequestInfo} from '../src/pullRequests'
|
||||
import moment from 'moment'
|
||||
import { DefaultConfiguration, Configuration } from '../src/configuration';
|
||||
import { Configuration, DefaultConfiguration } from '../src/configuration';
|
||||
import { DefaultDiffInfo } from '../src/commits';
|
||||
|
||||
jest.setTimeout(180000)
|
||||
@@ -41,7 +41,7 @@ mergedPullRequests.push(
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
body: 'no magic body1 for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: [],
|
||||
approvedReviewers: [],
|
||||
@@ -59,7 +59,7 @@ mergedPullRequests.push(
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
body: 'no magic body2 for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: [],
|
||||
approvedReviewers: [],
|
||||
@@ -77,7 +77,7 @@ mergedPullRequests.push(
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
body: 'no magic body3 for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: [],
|
||||
approvedReviewers: [],
|
||||
@@ -95,7 +95,7 @@ mergedPullRequests.push(
|
||||
repoName: 'test-repo',
|
||||
labels: new Set<string>(),
|
||||
milestone: '',
|
||||
body: 'no magic body for this matter',
|
||||
body: 'no magic body4 for this matter',
|
||||
assignees: [],
|
||||
requestedReviewers: [],
|
||||
approvedReviewers: [],
|
||||
@@ -582,4 +582,44 @@ it('Use exclude labels to not include a PR within a category.', async () => {
|
||||
expect(resultChangelog).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`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
it('Extract custom placeholder from PR body and replace in global template', async () => {
|
||||
const customConfig = Object.assign({}, configuration)
|
||||
customConfig.custom_placeholders = [
|
||||
{
|
||||
name: "C_PLACEHOLDER_1",
|
||||
source: "BODY",
|
||||
transformer: {
|
||||
pattern: '.+ (b....).+',
|
||||
target: '- $1'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "C_PLACEHOLER_2",
|
||||
source: "BODY",
|
||||
transformer: {
|
||||
pattern: '.+ b(....).+',
|
||||
target: '\n- $1'
|
||||
}
|
||||
}
|
||||
]
|
||||
customConfig.template = "${{CHANGELOG}}\n\n${{C_PLACEHOLER_2[2]}}\n\n${{C_PLACEHOLER_2[*]}}${{C_PLACEHOLDER_1[7]}}${{C_PLACEHOLER_2[1493]}}"
|
||||
customConfig.pr_template = "${{BODY}} ----> ${{C_PLACEHOLDER_1}}"
|
||||
|
||||
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, {
|
||||
owner: 'mikepenz',
|
||||
repo: 'test-repo',
|
||||
fromTag: { name: '1.0.0' },
|
||||
toTag: { name: '2.0.0' },
|
||||
includeOpen: false,
|
||||
failOnError: false,
|
||||
fetchReviewers: false,
|
||||
fetchReleaseInformation: false,
|
||||
commitMode: false,
|
||||
configuration: customConfig
|
||||
})
|
||||
|
||||
expect(resultChangelog).toStrictEqual(`## 🚀 Features\n\nno magic body1 for this matter ----> - body1\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`)
|
||||
})
|
||||
|
||||
+132
-59
@@ -217,7 +217,8 @@ exports.DefaultConfiguration = {
|
||||
filter: undefined,
|
||||
transformer: undefined // transforms the tag name using the regex, run after the filter
|
||||
},
|
||||
base_branches: [] // target branches for the merged PR ignoring PRs with different target branch, by default it will get all PRs
|
||||
base_branches: [],
|
||||
custom_placeholders: []
|
||||
};
|
||||
|
||||
|
||||
@@ -782,7 +783,7 @@ class ReleaseNotes {
|
||||
core.setOutput('commits', diffInfo.commits);
|
||||
if (mergedPullRequests.length === 0) {
|
||||
core.warning(`⚠️ No pull requests found`);
|
||||
return (0, transform_1.fillAdditionalPlaceholders)(this.options.configuration.empty_template || configuration_1.DefaultConfiguration.empty_template, this.options);
|
||||
return (0, transform_1.replaceEmptyTemplate)(this.options.configuration.empty_template || configuration_1.DefaultConfiguration.empty_template, this.options);
|
||||
}
|
||||
core.startGroup('📦 Build changelog');
|
||||
const resultChangelog = (0, transform_1.buildChangelog)(diffInfo, mergedPullRequests, this.options);
|
||||
@@ -1444,10 +1445,11 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.validateTransformer = exports.fillAdditionalPlaceholders = exports.buildChangelog = void 0;
|
||||
exports.validateTransformer = exports.replaceEmptyTemplate = exports.buildChangelog = void 0;
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
const configuration_1 = __nccwpck_require__(5527);
|
||||
const pullRequests_1 = __nccwpck_require__(4217);
|
||||
const utils_1 = __nccwpck_require__(918);
|
||||
function buildChangelog(diffInfo, prs, options) {
|
||||
// sort to target order
|
||||
const config = options.configuration;
|
||||
@@ -1493,11 +1495,17 @@ function buildChangelog(diffInfo, prs, options) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// keep reference for the placeholder values
|
||||
const placeholders = new Map();
|
||||
for (const ph of config.custom_placeholders || []) {
|
||||
(0, utils_1.createOrSet)(placeholders, ph.source, ph);
|
||||
}
|
||||
const placeholderPrMap = new Map();
|
||||
const validatedTransformers = validateTransformers(config.transformers);
|
||||
const transformedMap = new Map();
|
||||
// convert PRs to their text representation
|
||||
for (const pr of prs) {
|
||||
transformedMap.set(pr, transform(fillTemplate(pr, config.pr_template || configuration_1.DefaultConfiguration.pr_template), validatedTransformers));
|
||||
transformedMap.set(pr, transform(fillPrTemplate(pr, config.pr_template || configuration_1.DefaultConfiguration.pr_template, placeholders, placeholderPrMap), validatedTransformers));
|
||||
}
|
||||
core.info(`ℹ️ Used ${validatedTransformers.length} transformers to adjust message`);
|
||||
core.info(`✒️ Wrote messages for ${prs.length} pull requests`);
|
||||
@@ -1514,7 +1522,7 @@ function buildChangelog(diffInfo, prs, options) {
|
||||
const uncategorizedPrs = [];
|
||||
// bring elements in order
|
||||
for (const [pr, body] of transformedMap) {
|
||||
if (haveCommonElements(ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
if ((0, utils_1.haveCommonElements)(ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
ignoredPrs.push(body);
|
||||
continue;
|
||||
}
|
||||
@@ -1525,7 +1533,7 @@ function buildChangelog(diffInfo, prs, options) {
|
||||
for (const [category, pullRequests] of categorized) {
|
||||
// check if any exclude label matches
|
||||
if (category.exclude_labels !== undefined) {
|
||||
if (haveCommonElements(category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
if ((0, utils_1.haveCommonElements)(category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
if (core.isDebug()) {
|
||||
const prNum = pr.number;
|
||||
const prLabels = pr.labels;
|
||||
@@ -1536,13 +1544,13 @@ function buildChangelog(diffInfo, prs, options) {
|
||||
}
|
||||
}
|
||||
if (category.exhaustive === true) {
|
||||
if (haveEveryElements(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
if ((0, utils_1.haveEveryElements)(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
pullRequests.push(body);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (haveCommonElements(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
if ((0, utils_1.haveCommonElements)(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
|
||||
pullRequests.push(body);
|
||||
matched = true;
|
||||
}
|
||||
@@ -1620,72 +1628,119 @@ function buildChangelog(diffInfo, prs, options) {
|
||||
}
|
||||
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`);
|
||||
// fill template
|
||||
let transformedChangelog = config.template || configuration_1.DefaultConfiguration.template;
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CHANGELOG}}/g, changelog);
|
||||
transformedChangelog = transformedChangelog.replace(/\${{UNCATEGORIZED}}/g, changelogUncategorized);
|
||||
transformedChangelog = transformedChangelog.replace(/\${{OPEN}}/g, changelogOpen);
|
||||
transformedChangelog = transformedChangelog.replace(/\${{IGNORED}}/g, changelogIgnored);
|
||||
const placeholderMap = new Map();
|
||||
placeholderMap.set('CHANGELOG', changelog);
|
||||
placeholderMap.set('UNCATEGORIZED', changelogUncategorized);
|
||||
placeholderMap.set('OPEN', changelogOpen);
|
||||
placeholderMap.set('IGNORED', changelogIgnored);
|
||||
// fill other placeholders
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CATEGORIZED_COUNT}}/g, categorizedPrs.length.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{UNCATEGORIZED_COUNT}}/g, uncategorizedPrs.length.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{OPEN_COUNT}}/g, openPrs.length.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{IGNORED_COUNT}}/g, ignoredPrs.length.toString());
|
||||
placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString());
|
||||
placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString());
|
||||
placeholderMap.set('OPEN_COUNT', openPrs.length.toString());
|
||||
placeholderMap.set('IGNORED_COUNT', ignoredPrs.length.toString());
|
||||
// code change placeholders
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CHANGED_FILES}}/g, diffInfo.changedFiles.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{ADDITIONS}}/g, diffInfo.additions.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{DELETIONS}}/g, diffInfo.deletions.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CHANGES}}/g, diffInfo.changes.toString());
|
||||
transformedChangelog = transformedChangelog.replace(/\${{COMMITS}}/g, diffInfo.commits.toString());
|
||||
transformedChangelog = fillAdditionalPlaceholders(transformedChangelog, options);
|
||||
placeholderMap.set('CHANGED_FILES', diffInfo.changedFiles.toString());
|
||||
placeholderMap.set('ADDITIONS', diffInfo.additions.toString());
|
||||
placeholderMap.set('DELETIONS', diffInfo.deletions.toString());
|
||||
placeholderMap.set('CHANGES', diffInfo.changes.toString());
|
||||
placeholderMap.set('COMMITS', diffInfo.commits.toString());
|
||||
fillAdditionalPlaceholders(options, placeholderMap);
|
||||
let transformedChangelog = config.template || configuration_1.DefaultConfiguration.template;
|
||||
transformedChangelog = replacePlaceholders(transformedChangelog, placeholderMap, placeholders, placeholderPrMap);
|
||||
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap);
|
||||
transformedChangelog = cleanupPrPlaceHolders(transformedChangelog, placeholders);
|
||||
core.info(`ℹ️ Filled template`);
|
||||
return transformedChangelog;
|
||||
}
|
||||
exports.buildChangelog = buildChangelog;
|
||||
function fillAdditionalPlaceholders(text, options) {
|
||||
function replaceEmptyTemplate(template, options) {
|
||||
const placeholders = new Map();
|
||||
for (const ph of options.configuration.custom_placeholders || []) {
|
||||
(0, utils_1.createOrSet)(placeholders, ph.source, ph);
|
||||
}
|
||||
const placeholderMap = new Map();
|
||||
fillAdditionalPlaceholders(options, placeholderMap);
|
||||
return replacePlaceholders(template, placeholderMap, placeholders);
|
||||
}
|
||||
exports.replaceEmptyTemplate = replaceEmptyTemplate;
|
||||
function fillAdditionalPlaceholders(options, placeholderMap /* placeholderKey and original value */) {
|
||||
var _a, _b;
|
||||
let transformed = text;
|
||||
// repository placeholders
|
||||
transformed = transformed.replace(/\${{OWNER}}/g, options.owner);
|
||||
transformed = transformed.replace(/\${{REPO}}/g, options.repo);
|
||||
transformed = transformed.replace(/\${{FROM_TAG}}/g, options.fromTag.name);
|
||||
transformed = transformed.replace(/\${{FROM_TAG_DATE}}/g, ((_a = options.fromTag.date) === null || _a === void 0 ? void 0 : _a.toISOString()) || '');
|
||||
transformed = transformed.replace(/\${{TO_TAG}}/g, options.toTag.name);
|
||||
transformed = transformed.replace(/\${{TO_TAG_DATE}}/g, ((_b = options.toTag.date) === null || _b === void 0 ? void 0 : _b.toISOString()) || '');
|
||||
placeholderMap.set('OWNER', options.owner);
|
||||
placeholderMap.set('REPO', options.repo);
|
||||
placeholderMap.set('FROM_TAG', options.fromTag.name);
|
||||
placeholderMap.set('FROM_TAG_DATE', ((_a = options.fromTag.date) === null || _a === void 0 ? void 0 : _a.toISOString()) || '');
|
||||
placeholderMap.set('TO_TAG', options.toTag.name);
|
||||
placeholderMap.set('TO_TAG_DATE', ((_b = options.toTag.date) === null || _b === void 0 ? void 0 : _b.toISOString()) || '');
|
||||
const fromDate = options.fromTag.date;
|
||||
const toDate = options.toTag.date;
|
||||
if (fromDate !== undefined && toDate !== undefined) {
|
||||
transformed = transformed.replace(/\${{DAYS_SINCE}}/g, toDate.diff(fromDate, 'days').toString() || '');
|
||||
placeholderMap.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '');
|
||||
}
|
||||
else {
|
||||
transformed = transformed.replace(/\${{DAYS_SINCE}}/g, '');
|
||||
placeholderMap.set('DAYS_SINCE', '');
|
||||
}
|
||||
placeholderMap.set('RELEASE_DIFF', `https://github.com/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}`);
|
||||
}
|
||||
function fillPrTemplate(pr, template, placeholders /* placeholders to apply */, placeholderPrMap /* map to keep replaced placeholder values with their key */) {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
const placeholderMap = new Map();
|
||||
placeholderMap.set('NUMBER', pr.number.toString());
|
||||
placeholderMap.set('TITLE', pr.title);
|
||||
placeholderMap.set('URL', pr.htmlURL);
|
||||
placeholderMap.set('STATUS', pr.status);
|
||||
placeholderMap.set('CREATED_AT', pr.createdAt.toISOString());
|
||||
placeholderMap.set('MERGED_AT', ((_a = pr.mergedAt) === null || _a === void 0 ? void 0 : _a.toISOString()) || '');
|
||||
placeholderMap.set('MERGE_SHA', pr.mergeCommitSha);
|
||||
placeholderMap.set('AUTHOR', pr.author);
|
||||
placeholderMap.set('LABELS', ((_c = (_b = [...pr.labels]) === null || _b === void 0 ? void 0 : _b.filter(l => !l.startsWith('--rcba-'))) === null || _c === void 0 ? void 0 : _c.join(', ')) || '');
|
||||
placeholderMap.set('MILESTONE', pr.milestone || '');
|
||||
placeholderMap.set('BODY', pr.body);
|
||||
placeholderMap.set('ASSIGNEES', ((_d = pr.assignees) === null || _d === void 0 ? void 0 : _d.join(', ')) || '');
|
||||
placeholderMap.set('REVIEWERS', ((_e = pr.requestedReviewers) === null || _e === void 0 ? void 0 : _e.join(', ')) || '');
|
||||
placeholderMap.set('APPROVERS', ((_f = pr.approvedReviewers) === null || _f === void 0 ? void 0 : _f.join(', ')) || '');
|
||||
placeholderMap.set('BRANCH', pr.branch || '');
|
||||
placeholderMap.set('BASE_BRANCH', pr.baseBranch);
|
||||
return replacePlaceholders(template, placeholderMap, placeholders, placeholderPrMap);
|
||||
}
|
||||
function replacePlaceholders(template, placeholderMap /* placeholderKey and original value */, placeholders /* placeholders to apply */, placeholderPrMap /* map to keep replaced placeholder values with their key */) {
|
||||
let transformed = template;
|
||||
for (const [key, value] of placeholderMap) {
|
||||
transformed = transformed.replaceAll(`\${{${key}}}`, value);
|
||||
// replace custom placeholders
|
||||
const phs = placeholders.get(key);
|
||||
if (phs) {
|
||||
for (const placeholder of phs) {
|
||||
const transformer = validateTransformer(placeholder.transformer);
|
||||
if (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) {
|
||||
const extractedValue = value.replace(transformer.pattern, transformer.target);
|
||||
// note: `.replace` will return the full string again if there was no match
|
||||
if (extractedValue && placeholderPrMap && extractedValue !== value) {
|
||||
(0, utils_1.createOrSet)(placeholderPrMap, placeholder.name, extractedValue);
|
||||
}
|
||||
transformed = transformed.replaceAll(`\${{${placeholder.name}}}`, extractedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transformed = transformed.replace(/\${{RELEASE_DIFF}}/g, `https://github.com/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}`);
|
||||
return transformed;
|
||||
}
|
||||
exports.fillAdditionalPlaceholders = fillAdditionalPlaceholders;
|
||||
function haveCommonElements(arr1, arr2) {
|
||||
return arr1.some(item => arr2.has(item));
|
||||
}
|
||||
function haveEveryElements(arr1, arr2) {
|
||||
return arr1.every(item => arr2.has(item));
|
||||
}
|
||||
function fillTemplate(pr, template) {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
function replacePrPlaceholders(template, placeholderPrMap /* map with all pr related custom placeholder values */) {
|
||||
let transformed = template;
|
||||
transformed = transformed.replace(/\${{NUMBER}}/g, pr.number.toString());
|
||||
transformed = transformed.replace(/\${{TITLE}}/g, pr.title);
|
||||
transformed = transformed.replace(/\${{URL}}/g, pr.htmlURL);
|
||||
transformed = transformed.replace(/\${{STATUS}}/g, pr.status);
|
||||
transformed = transformed.replace(/\${{CREATED_AT}}/g, pr.createdAt.toISOString());
|
||||
transformed = transformed.replace(/\${{MERGED_AT}}/g, ((_a = pr.mergedAt) === null || _a === void 0 ? void 0 : _a.toISOString()) || '');
|
||||
transformed = transformed.replace(/\${{MERGE_SHA}}/g, pr.mergeCommitSha);
|
||||
transformed = transformed.replace(/\${{AUTHOR}}/g, pr.author);
|
||||
transformed = transformed.replace(/\${{LABELS}}/g, ((_c = (_b = [...pr.labels]) === null || _b === void 0 ? void 0 : _b.filter(l => !l.startsWith('--rcba-'))) === null || _c === void 0 ? void 0 : _c.join(', ')) || '');
|
||||
transformed = transformed.replace(/\${{MILESTONE}}/g, pr.milestone || '');
|
||||
transformed = transformed.replace(/\${{BODY}}/g, pr.body);
|
||||
transformed = transformed.replace(/\${{ASSIGNEES}}/g, ((_d = pr.assignees) === null || _d === void 0 ? void 0 : _d.join(', ')) || '');
|
||||
transformed = transformed.replace(/\${{REVIEWERS}}/g, ((_e = pr.requestedReviewers) === null || _e === void 0 ? void 0 : _e.join(', ')) || '');
|
||||
transformed = transformed.replace(/\${{APPROVERS}}/g, ((_f = pr.approvedReviewers) === null || _f === void 0 ? void 0 : _f.join(', ')) || '');
|
||||
for (const [key, values] of placeholderPrMap) {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
transformed = transformed.replaceAll(`\${{${key}[${i}]}}`, values[i]);
|
||||
}
|
||||
transformed = transformed.replaceAll(`\${{${key}[*]}}`, values.join(''));
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
function cleanupPrPlaceHolders(template, placeholders /* placeholders to apply */) {
|
||||
let transformed = template;
|
||||
for (const [, phs] of placeholders) {
|
||||
for (const ph of phs) {
|
||||
transformed = transformed.replaceAll(new RegExp(`\\$\\{\\{${ph.name}\\[.+?\\]\\}\\}`, 'gu'), '');
|
||||
}
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
function transform(filled, transformers) {
|
||||
@@ -1825,7 +1880,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.writeOutput = exports.directoryExistsSync = exports.parseConfiguration = exports.resolveConfiguration = exports.failOrError = exports.retrieveRepositoryPath = void 0;
|
||||
exports.haveEveryElements = exports.haveCommonElements = exports.createOrSet = exports.writeOutput = exports.directoryExistsSync = exports.parseConfiguration = exports.resolveConfiguration = exports.failOrError = exports.retrieveRepositoryPath = void 0;
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
const fs = __importStar(__nccwpck_require__(7147));
|
||||
const path = __importStar(__nccwpck_require__(1017));
|
||||
@@ -1959,6 +2014,24 @@ function writeOutput(githubWorkspacePath, outputFile, changelog) {
|
||||
}
|
||||
}
|
||||
exports.writeOutput = writeOutput;
|
||||
function createOrSet(map, key, value) {
|
||||
const entry = map.get(key);
|
||||
if (!entry) {
|
||||
map.set(key, [value]);
|
||||
}
|
||||
else {
|
||||
entry.push(value);
|
||||
}
|
||||
}
|
||||
exports.createOrSet = createOrSet;
|
||||
function haveCommonElements(arr1, arr2) {
|
||||
return arr1.some(item => arr2.has(item));
|
||||
}
|
||||
exports.haveCommonElements = haveCommonElements;
|
||||
function haveEveryElements(arr1, arr2) {
|
||||
return arr1.every(item => arr2.has(item));
|
||||
}
|
||||
exports.haveEveryElements = haveEveryElements;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -14,6 +14,7 @@ export interface Configuration {
|
||||
transformers: Transformer[]
|
||||
tag_resolver: TagResolver
|
||||
base_branches: string[]
|
||||
custom_placeholders?: Placeholder[]
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@@ -57,6 +58,12 @@ export interface TagResolver {
|
||||
transformer?: Transformer // 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
|
||||
}
|
||||
|
||||
export const DefaultConfiguration: Configuration = {
|
||||
max_tags_to_fetch: 200, // the amount of tags to fetch from the github API
|
||||
max_pull_requests: 200, // the amount of pull requests to process
|
||||
@@ -94,5 +101,6 @@ export const DefaultConfiguration: Configuration = {
|
||||
filter: undefined, // filter out all tags not matching the regex
|
||||
transformer: undefined // transforms the tag name using the regex, run after the filter
|
||||
},
|
||||
base_branches: [] // target branches for the merged PR ignoring PRs with different target branch, by default it will get all PRs
|
||||
base_branches: [], // target branches for the merged PR ignoring PRs with different target branch, by default it will get all PRs
|
||||
custom_placeholders: []
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import {Commits, filterCommits, DiffInfo, DefaultDiffInfo} from './commits'
|
||||
import {Configuration, DefaultConfiguration} from './configuration'
|
||||
import {PullRequestInfo, PullRequests} from './pullRequests'
|
||||
import {Octokit} from '@octokit/rest'
|
||||
import {buildChangelog, fillAdditionalPlaceholders} from './transform'
|
||||
import {buildChangelog, replaceEmptyTemplate} from './transform'
|
||||
import {failOrError} from './utils'
|
||||
import {TagInfo} from './tags'
|
||||
|
||||
@@ -61,7 +61,7 @@ export class ReleaseNotes {
|
||||
|
||||
if (mergedPullRequests.length === 0) {
|
||||
core.warning(`⚠️ No pull requests found`)
|
||||
return fillAdditionalPlaceholders(
|
||||
return replaceEmptyTemplate(
|
||||
this.options.configuration.empty_template || DefaultConfiguration.empty_template,
|
||||
this.options
|
||||
)
|
||||
|
||||
+141
-65
@@ -1,8 +1,17 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Category, DefaultConfiguration, Extractor, Transformer} from './configuration'
|
||||
import {Category, DefaultConfiguration, Extractor, Placeholder, Transformer} from './configuration'
|
||||
import {PullRequestInfo, sortPullRequests} from './pullRequests'
|
||||
import {ReleaseNotesOptions} from './releaseNotes'
|
||||
import {DiffInfo} from './commits'
|
||||
import {createOrSet, haveCommonElements, haveEveryElements} from './utils'
|
||||
|
||||
export interface RegexTransformer {
|
||||
pattern: RegExp | null
|
||||
target: string
|
||||
onProperty?: ('title' | 'author' | 'milestone' | 'body' | 'status' | 'branch')[] | undefined
|
||||
method?: 'replace' | 'match' | undefined
|
||||
onEmpty?: string | undefined
|
||||
}
|
||||
|
||||
export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], options: ReleaseNotesOptions): string {
|
||||
// sort to target order
|
||||
@@ -51,13 +60,23 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
}
|
||||
}
|
||||
|
||||
// keep reference for the placeholder values
|
||||
const placeholders = new Map<string, Placeholder[]>()
|
||||
for (const ph of config.custom_placeholders || []) {
|
||||
createOrSet(placeholders, ph.source, ph)
|
||||
}
|
||||
const placeholderPrMap = new Map<string, string[]>()
|
||||
|
||||
const validatedTransformers = validateTransformers(config.transformers)
|
||||
const transformedMap = new Map<PullRequestInfo, string>()
|
||||
// convert PRs to their text representation
|
||||
for (const pr of prs) {
|
||||
transformedMap.set(
|
||||
pr,
|
||||
transform(fillTemplate(pr, config.pr_template || DefaultConfiguration.pr_template), validatedTransformers)
|
||||
transform(
|
||||
fillPrTemplate(pr, config.pr_template || DefaultConfiguration.pr_template, placeholders, placeholderPrMap),
|
||||
validatedTransformers
|
||||
)
|
||||
)
|
||||
}
|
||||
core.info(`ℹ️ Used ${validatedTransformers.length} transformers to adjust message`)
|
||||
@@ -215,79 +234,144 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`)
|
||||
|
||||
// fill template
|
||||
let transformedChangelog = config.template || DefaultConfiguration.template
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CHANGELOG}}/g, changelog)
|
||||
transformedChangelog = transformedChangelog.replace(/\${{UNCATEGORIZED}}/g, changelogUncategorized)
|
||||
transformedChangelog = transformedChangelog.replace(/\${{OPEN}}/g, changelogOpen)
|
||||
transformedChangelog = transformedChangelog.replace(/\${{IGNORED}}/g, changelogIgnored)
|
||||
|
||||
const placeholderMap = new Map<string, string>()
|
||||
placeholderMap.set('CHANGELOG', changelog)
|
||||
placeholderMap.set('UNCATEGORIZED', changelogUncategorized)
|
||||
placeholderMap.set('OPEN', changelogOpen)
|
||||
placeholderMap.set('IGNORED', changelogIgnored)
|
||||
// fill other placeholders
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CATEGORIZED_COUNT}}/g, categorizedPrs.length.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{UNCATEGORIZED_COUNT}}/g, uncategorizedPrs.length.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{OPEN_COUNT}}/g, openPrs.length.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{IGNORED_COUNT}}/g, ignoredPrs.length.toString())
|
||||
placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString())
|
||||
placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString())
|
||||
placeholderMap.set('OPEN_COUNT', openPrs.length.toString())
|
||||
placeholderMap.set('IGNORED_COUNT', ignoredPrs.length.toString())
|
||||
// code change placeholders
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CHANGED_FILES}}/g, diffInfo.changedFiles.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{ADDITIONS}}/g, diffInfo.additions.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{DELETIONS}}/g, diffInfo.deletions.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{CHANGES}}/g, diffInfo.changes.toString())
|
||||
transformedChangelog = transformedChangelog.replace(/\${{COMMITS}}/g, diffInfo.commits.toString())
|
||||
transformedChangelog = fillAdditionalPlaceholders(transformedChangelog, options)
|
||||
placeholderMap.set('CHANGED_FILES', diffInfo.changedFiles.toString())
|
||||
placeholderMap.set('ADDITIONS', diffInfo.additions.toString())
|
||||
placeholderMap.set('DELETIONS', diffInfo.deletions.toString())
|
||||
placeholderMap.set('CHANGES', diffInfo.changes.toString())
|
||||
placeholderMap.set('COMMITS', diffInfo.commits.toString())
|
||||
fillAdditionalPlaceholders(options, placeholderMap)
|
||||
|
||||
let transformedChangelog = config.template || DefaultConfiguration.template
|
||||
transformedChangelog = replacePlaceholders(transformedChangelog, placeholderMap, placeholders, placeholderPrMap)
|
||||
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap)
|
||||
transformedChangelog = cleanupPrPlaceHolders(transformedChangelog, placeholders)
|
||||
core.info(`ℹ️ Filled template`)
|
||||
return transformedChangelog
|
||||
}
|
||||
|
||||
export function fillAdditionalPlaceholders(text: string, options: ReleaseNotesOptions): string {
|
||||
let transformed = text
|
||||
// repository placeholders
|
||||
transformed = transformed.replace(/\${{OWNER}}/g, options.owner)
|
||||
transformed = transformed.replace(/\${{REPO}}/g, options.repo)
|
||||
transformed = transformed.replace(/\${{FROM_TAG}}/g, options.fromTag.name)
|
||||
transformed = transformed.replace(/\${{FROM_TAG_DATE}}/g, options.fromTag.date?.toISOString() || '')
|
||||
transformed = transformed.replace(/\${{TO_TAG}}/g, options.toTag.name)
|
||||
transformed = transformed.replace(/\${{TO_TAG_DATE}}/g, options.toTag.date?.toISOString() || '')
|
||||
export function replaceEmptyTemplate(template: string, options: ReleaseNotesOptions): string {
|
||||
const placeholders = new Map<string, Placeholder[]>()
|
||||
for (const ph of options.configuration.custom_placeholders || []) {
|
||||
createOrSet(placeholders, ph.source, ph)
|
||||
}
|
||||
const placeholderMap = new Map<string, string>()
|
||||
fillAdditionalPlaceholders(options, placeholderMap)
|
||||
return replacePlaceholders(template, placeholderMap, placeholders)
|
||||
}
|
||||
|
||||
function fillAdditionalPlaceholders(
|
||||
options: ReleaseNotesOptions,
|
||||
placeholderMap: Map<string, string> /* placeholderKey and original value */
|
||||
): void {
|
||||
placeholderMap.set('OWNER', options.owner)
|
||||
placeholderMap.set('REPO', options.repo)
|
||||
placeholderMap.set('FROM_TAG', options.fromTag.name)
|
||||
placeholderMap.set('FROM_TAG_DATE', options.fromTag.date?.toISOString() || '')
|
||||
placeholderMap.set('TO_TAG', options.toTag.name)
|
||||
placeholderMap.set('TO_TAG_DATE', options.toTag.date?.toISOString() || '')
|
||||
const fromDate = options.fromTag.date
|
||||
const toDate = options.toTag.date
|
||||
if (fromDate !== undefined && toDate !== undefined) {
|
||||
transformed = transformed.replace(/\${{DAYS_SINCE}}/g, toDate.diff(fromDate, 'days').toString() || '')
|
||||
placeholderMap.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '')
|
||||
} else {
|
||||
transformed = transformed.replace(/\${{DAYS_SINCE}}/g, '')
|
||||
placeholderMap.set('DAYS_SINCE', '')
|
||||
}
|
||||
transformed = transformed.replace(
|
||||
/\${{RELEASE_DIFF}}/g,
|
||||
placeholderMap.set(
|
||||
'RELEASE_DIFF',
|
||||
`https://github.com/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}`
|
||||
)
|
||||
}
|
||||
|
||||
function fillPrTemplate(
|
||||
pr: PullRequestInfo,
|
||||
template: string,
|
||||
placeholders: Map<string, Placeholder[]> /* placeholders to apply */,
|
||||
placeholderPrMap: Map<string, string[]> /* map to keep replaced placeholder values with their key */
|
||||
): string {
|
||||
const placeholderMap = new Map<string, string>()
|
||||
placeholderMap.set('NUMBER', pr.number.toString())
|
||||
placeholderMap.set('TITLE', pr.title)
|
||||
placeholderMap.set('URL', pr.htmlURL)
|
||||
placeholderMap.set('STATUS', pr.status)
|
||||
placeholderMap.set('CREATED_AT', pr.createdAt.toISOString())
|
||||
placeholderMap.set('MERGED_AT', pr.mergedAt?.toISOString() || '')
|
||||
placeholderMap.set('MERGE_SHA', pr.mergeCommitSha)
|
||||
placeholderMap.set('AUTHOR', pr.author)
|
||||
placeholderMap.set('LABELS', [...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || '')
|
||||
placeholderMap.set('MILESTONE', pr.milestone || '')
|
||||
placeholderMap.set('BODY', pr.body)
|
||||
placeholderMap.set('ASSIGNEES', pr.assignees?.join(', ') || '')
|
||||
placeholderMap.set('REVIEWERS', pr.requestedReviewers?.join(', ') || '')
|
||||
placeholderMap.set('APPROVERS', pr.approvedReviewers?.join(', ') || '')
|
||||
placeholderMap.set('BRANCH', pr.branch || '')
|
||||
placeholderMap.set('BASE_BRANCH', pr.baseBranch)
|
||||
return replacePlaceholders(template, placeholderMap, placeholders, placeholderPrMap)
|
||||
}
|
||||
|
||||
function replacePlaceholders(
|
||||
template: string,
|
||||
placeholderMap: Map<string, string> /* placeholderKey and original value */,
|
||||
placeholders: Map<string, Placeholder[]> /* placeholders to apply */,
|
||||
placeholderPrMap?: Map<string, string[]> /* map to keep replaced placeholder values with their key */
|
||||
): string {
|
||||
let transformed = template
|
||||
for (const [key, value] of placeholderMap) {
|
||||
transformed = transformed.replaceAll(`\${{${key}}}`, value)
|
||||
|
||||
// replace custom placeholders
|
||||
const phs = placeholders.get(key)
|
||||
if (phs) {
|
||||
for (const placeholder of phs) {
|
||||
const transformer = validateTransformer(placeholder.transformer)
|
||||
if (transformer?.pattern) {
|
||||
const extractedValue = value.replace(transformer.pattern, transformer.target)
|
||||
// note: `.replace` will return the full string again if there was no match
|
||||
if (extractedValue && placeholderPrMap && extractedValue !== value) {
|
||||
createOrSet(placeholderPrMap, placeholder.name, extractedValue)
|
||||
}
|
||||
transformed = transformed.replaceAll(`\${{${placeholder.name}}}`, extractedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return transformed
|
||||
}
|
||||
|
||||
function haveCommonElements(arr1: string[], arr2: Set<string>): Boolean {
|
||||
return arr1.some(item => arr2.has(item))
|
||||
}
|
||||
|
||||
function haveEveryElements(arr1: string[], arr2: Set<string>): Boolean {
|
||||
return arr1.every(item => arr2.has(item))
|
||||
}
|
||||
|
||||
function fillTemplate(pr: PullRequestInfo, template: string): string {
|
||||
function replacePrPlaceholders(
|
||||
template: string,
|
||||
placeholderPrMap: Map<string, string[]> /* map with all pr related custom placeholder values */
|
||||
): string {
|
||||
let transformed = template
|
||||
transformed = transformed.replace(/\${{NUMBER}}/g, pr.number.toString())
|
||||
transformed = transformed.replace(/\${{TITLE}}/g, pr.title)
|
||||
transformed = transformed.replace(/\${{URL}}/g, pr.htmlURL)
|
||||
transformed = transformed.replace(/\${{STATUS}}/g, pr.status)
|
||||
transformed = transformed.replace(/\${{CREATED_AT}}/g, pr.createdAt.toISOString())
|
||||
transformed = transformed.replace(/\${{MERGED_AT}}/g, pr.mergedAt?.toISOString() || '')
|
||||
transformed = transformed.replace(/\${{MERGE_SHA}}/g, pr.mergeCommitSha)
|
||||
transformed = transformed.replace(/\${{AUTHOR}}/g, pr.author)
|
||||
transformed = transformed.replace(
|
||||
/\${{LABELS}}/g,
|
||||
[...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || ''
|
||||
)
|
||||
transformed = transformed.replace(/\${{MILESTONE}}/g, pr.milestone || '')
|
||||
transformed = transformed.replace(/\${{BODY}}/g, pr.body)
|
||||
transformed = transformed.replace(/\${{ASSIGNEES}}/g, pr.assignees?.join(', ') || '')
|
||||
transformed = transformed.replace(/\${{REVIEWERS}}/g, pr.requestedReviewers?.join(', ') || '')
|
||||
transformed = transformed.replace(/\${{APPROVERS}}/g, pr.approvedReviewers?.join(', ') || '')
|
||||
for (const [key, values] of placeholderPrMap) {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
transformed = transformed.replaceAll(`\${{${key}[${i}]}}`, values[i])
|
||||
}
|
||||
transformed = transformed.replaceAll(`\${{${key}[*]}}`, values.join(''))
|
||||
}
|
||||
return transformed
|
||||
}
|
||||
|
||||
function cleanupPrPlaceHolders(
|
||||
template: string,
|
||||
placeholders: Map<string, Placeholder[]> /* placeholders to apply */
|
||||
): string {
|
||||
let transformed = template
|
||||
for (const [, phs] of placeholders) {
|
||||
for (const ph of phs) {
|
||||
transformed = transformed.replaceAll(new RegExp(`\\$\\{\\{${ph.name}\\[.+?\\]\\}\\}`, 'gu'), '')
|
||||
}
|
||||
}
|
||||
return transformed
|
||||
}
|
||||
|
||||
@@ -399,11 +483,3 @@ function extractValuesFromString(value: string, extractor: RegexTransformer): st
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export interface RegexTransformer {
|
||||
pattern: RegExp | null
|
||||
target: string
|
||||
onProperty?: ('title' | 'author' | 'milestone' | 'body' | 'status' | 'branch')[] | undefined
|
||||
method?: 'replace' | 'match' | undefined
|
||||
onEmpty?: string | undefined
|
||||
}
|
||||
|
||||
@@ -132,3 +132,20 @@ export function writeOutput(githubWorkspacePath: string, outputFile: string, cha
|
||||
}
|
||||
|
||||
export type Unpacked<T> = T extends (infer U)[] ? U : T
|
||||
|
||||
export function createOrSet<T>(map: Map<String, T[]>, key: string, value: T): void {
|
||||
const entry = map.get(key)
|
||||
if (!entry) {
|
||||
map.set(key, [value])
|
||||
} else {
|
||||
entry.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
export function haveCommonElements(arr1: string[], arr2: Set<string>): Boolean {
|
||||
return arr1.some(item => arr2.has(item))
|
||||
}
|
||||
|
||||
export function haveEveryElements(arr1: string[], arr2: Set<string>): Boolean {
|
||||
return arr1.every(item => arr2.has(item))
|
||||
}
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"lib": [ "ES2021.String" ] /* Enable custom `ES2021.String` extension in typescript for `replaceAll` */
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user