Merge pull request #838 from mikepenz/develop

dev -> main
This commit is contained in:
Mike Penz
2022-07-29 18:52:26 +02:00
committed by GitHub
17 changed files with 450 additions and 585 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"printWidth": 80, "printWidth": 120,
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"semi": false, "semi": false,
+1 -1
View File
@@ -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 | | | `${{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. | | | `${{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. | | | `${{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 ### Configuration Specification
-2
View File
@@ -1,5 +1,3 @@
import {resolveConfiguration} from '../src/utils'
import {ReleaseNotesBuilder} from '../src/releaseNotesBuilder'
import { TagInfo, sortTags, filterTags } from '../src/tags'; import { TagInfo, sortTags, filterTags } from '../src/tags';
jest.setTimeout(180000) jest.setTimeout(180000)
+45 -5
View File
@@ -1,7 +1,7 @@
import {buildChangelog} from '../src/transform' import {buildChangelog} from '../src/transform'
import {PullRequestInfo} from '../src/pullRequests' import {PullRequestInfo} from '../src/pullRequests'
import moment from 'moment' import moment from 'moment'
import { DefaultConfiguration, Configuration } from '../src/configuration'; import { Configuration, DefaultConfiguration } from '../src/configuration';
import { DefaultDiffInfo } from '../src/commits'; import { DefaultDiffInfo } from '../src/commits';
jest.setTimeout(180000) jest.setTimeout(180000)
@@ -41,7 +41,7 @@ mergedPullRequests.push(
repoName: 'test-repo', repoName: 'test-repo',
labels: new Set<string>(), labels: new Set<string>(),
milestone: '', milestone: '',
body: 'no magic body for this matter', body: 'no magic body1 for this matter',
assignees: [], assignees: [],
requestedReviewers: [], requestedReviewers: [],
approvedReviewers: [], approvedReviewers: [],
@@ -59,7 +59,7 @@ mergedPullRequests.push(
repoName: 'test-repo', repoName: 'test-repo',
labels: new Set<string>(), labels: new Set<string>(),
milestone: '', milestone: '',
body: 'no magic body for this matter', body: 'no magic body2 for this matter',
assignees: [], assignees: [],
requestedReviewers: [], requestedReviewers: [],
approvedReviewers: [], approvedReviewers: [],
@@ -77,7 +77,7 @@ mergedPullRequests.push(
repoName: 'test-repo', repoName: 'test-repo',
labels: new Set<string>(), labels: new Set<string>(),
milestone: '', milestone: '',
body: 'no magic body for this matter', body: 'no magic body3 for this matter',
assignees: [], assignees: [],
requestedReviewers: [], requestedReviewers: [],
approvedReviewers: [], approvedReviewers: [],
@@ -95,7 +95,7 @@ mergedPullRequests.push(
repoName: 'test-repo', repoName: 'test-repo',
labels: new Set<string>(), labels: new Set<string>(),
milestone: '', milestone: '',
body: 'no magic body for this matter', body: 'no magic body4 for this matter',
assignees: [], assignees: [],
requestedReviewers: [], requestedReviewers: [],
approvedReviewers: [], approvedReviewers: [],
@@ -583,3 +583,43 @@ it('Use exclude labels to not include a PR within a category.', async () => {
`## 🚀 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` `## 🚀 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`)
})
Generated Vendored
+141 -92
View File
@@ -217,7 +217,8 @@ exports.DefaultConfiguration = {
filter: undefined, filter: undefined,
transformer: undefined // transforms the tag name using the regex, run after the filter 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: []
}; };
@@ -282,38 +283,20 @@ class GitCommandManager {
} }
latestTag() { latestTag() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const revListOutput = yield this.execGit([ const revListOutput = yield this.execGit(['rev-list', '--tags', '--skip=0', '--max-count=1']);
'rev-list', const output = yield this.execGit(['describe', '--abbrev=0', '--tags', revListOutput.stdout.trim()]);
'--tags',
'--skip=0',
'--max-count=1'
]);
const output = yield this.execGit([
'describe',
'--abbrev=0',
'--tags',
revListOutput.stdout.trim()
]);
return output.stdout.trim(); return output.stdout.trim();
}); });
} }
initialCommit() { initialCommit() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const revListOutput = yield this.execGit([ const revListOutput = yield this.execGit(['rev-list', '--max-parents=0', 'HEAD']);
'rev-list',
'--max-parents=0',
'HEAD'
]);
return revListOutput.stdout.trim(); return revListOutput.stdout.trim();
}); });
} }
tagCreation(tagName) { tagCreation(tagName) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const creationDate = yield this.execGit([ const creationDate = yield this.execGit(['for-each-ref', '--format="%(creatordate:rfc)"', `refs/tags/${tagName}`]);
'for-each-ref',
'--format="%(creatordate:rfc)"',
`refs/tags/${tagName}`
]);
return creationDate.stdout.trim().replace(/"/g, ''); return creationDate.stdout.trim().replace(/"/g, '');
}); });
} }
@@ -800,8 +783,7 @@ class ReleaseNotes {
core.setOutput('commits', diffInfo.commits); core.setOutput('commits', diffInfo.commits);
if (mergedPullRequests.length === 0) { if (mergedPullRequests.length === 0) {
core.warning(`⚠️ No pull requests found`); core.warning(`⚠️ No pull requests found`);
return (0, transform_1.fillAdditionalPlaceholders)(this.options.configuration.empty_template || return (0, transform_1.replaceEmptyTemplate)(this.options.configuration.empty_template || configuration_1.DefaultConfiguration.empty_template, this.options);
configuration_1.DefaultConfiguration.empty_template, this.options);
} }
core.startGroup('📦 Build changelog'); core.startGroup('📦 Build changelog');
const resultChangelog = (0, transform_1.buildChangelog)(diffInfo, mergedPullRequests, this.options); const resultChangelog = (0, transform_1.buildChangelog)(diffInfo, mergedPullRequests, this.options);
@@ -841,8 +823,7 @@ class ReleaseNotes {
const lastCommit = commits[commits.length - 1]; const lastCommit = commits[commits.length - 1];
let fromDate = firstCommit.date; let fromDate = firstCommit.date;
const toDate = lastCommit.date; const toDate = lastCommit.date;
const maxDays = configuration.max_back_track_time_days || const maxDays = configuration.max_back_track_time_days || configuration_1.DefaultConfiguration.max_back_track_time_days;
configuration_1.DefaultConfiguration.max_back_track_time_days;
const maxFromDate = toDate.clone().subtract(maxDays, 'days'); const maxFromDate = toDate.clone().subtract(maxDays, 'days');
if (maxFromDate.isAfter(fromDate)) { if (maxFromDate.isAfter(fromDate)) {
core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`); core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`);
@@ -852,8 +833,7 @@ class ReleaseNotes {
const pullRequestsApi = new pullRequests_1.PullRequests(octokit); const pullRequestsApi = new pullRequests_1.PullRequests(octokit);
const pullRequests = yield pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate, configuration.max_pull_requests || configuration_1.DefaultConfiguration.max_pull_requests); const pullRequests = yield pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate, configuration.max_pull_requests || configuration_1.DefaultConfiguration.max_pull_requests);
core.info(`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`); core.info(`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`);
const prCommits = (0, commits_1.filterCommits)(commits, configuration.exclude_merge_branches || const prCommits = (0, commits_1.filterCommits)(commits, configuration.exclude_merge_branches || configuration_1.DefaultConfiguration.exclude_merge_branches);
configuration_1.DefaultConfiguration.exclude_merge_branches);
core.info(`️ Retrieved ${prCommits.length} release commits for ${owner}/${repo}`); core.info(`️ Retrieved ${prCommits.length} release commits for ${owner}/${repo}`);
// create array of commits for this release // create array of commits for this release
const releaseCommitHashes = prCommits.map(commmit => { const releaseCommitHashes = prCommits.map(commmit => {
@@ -866,8 +846,7 @@ class ReleaseNotes {
let allPullRequests = mergedPullRequests; let allPullRequests = mergedPullRequests;
if (includeOpen) { if (includeOpen) {
// retrieve all open pull requests // retrieve all open pull requests
const openPullRequests = yield pullRequestsApi.getOpen(owner, repo, configuration.max_pull_requests || const openPullRequests = yield pullRequestsApi.getOpen(owner, repo, configuration.max_pull_requests || configuration_1.DefaultConfiguration.max_pull_requests);
configuration_1.DefaultConfiguration.max_pull_requests);
core.info(`️ Retrieved ${openPullRequests.length} open PRs for ${owner}/${repo}`); core.info(`️ Retrieved ${openPullRequests.length} open PRs for ${owner}/${repo}`);
// all pull requests // all pull requests
allPullRequests = allPullRequests.concat(openPullRequests); allPullRequests = allPullRequests.concat(openPullRequests);
@@ -911,8 +890,7 @@ class ReleaseNotes {
if (commits.length === 0) { if (commits.length === 0) {
return [diffInfo, []]; return [diffInfo, []];
} }
const prCommits = (0, commits_1.filterCommits)(commits, configuration.exclude_merge_branches || const prCommits = (0, commits_1.filterCommits)(commits, configuration.exclude_merge_branches || configuration_1.DefaultConfiguration.exclude_merge_branches);
configuration_1.DefaultConfiguration.exclude_merge_branches);
core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`); core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`);
const prs = prCommits.map(function (commit) { const prs = prCommits.map(function (commit) {
return { return {
@@ -1032,8 +1010,7 @@ class ReleaseNotesBuilder {
// ensure proper from <-> to tag range // ensure proper from <-> to tag range
core.startGroup(`🔖 Resolve tags`); core.startGroup(`🔖 Resolve tags`);
const tagsApi = new tags_1.Tags(octokit); const tagsApi = new tags_1.Tags(octokit);
const tagRange = yield tagsApi.retrieveRange(this.repositoryPath, this.owner, this.repo, this.fromTag, this.toTag, this.ignorePreReleases, this.configuration.max_tags_to_fetch || const tagRange = yield tagsApi.retrieveRange(this.repositoryPath, this.owner, this.repo, this.fromTag, this.toTag, this.ignorePreReleases, this.configuration.max_tags_to_fetch || configuration_1.DefaultConfiguration.max_tags_to_fetch, this.configuration.tag_resolver || configuration_1.DefaultConfiguration.tag_resolver);
configuration_1.DefaultConfiguration.max_tags_to_fetch, this.configuration.tag_resolver || configuration_1.DefaultConfiguration.tag_resolver);
let thisTag = tagRange.to; let thisTag = tagRange.to;
if (!thisTag) { if (!thisTag) {
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError); (0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError);
@@ -1468,10 +1445,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.validateTransformer = exports.fillAdditionalPlaceholders = exports.buildChangelog = void 0; exports.validateTransformer = exports.replaceEmptyTemplate = exports.buildChangelog = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const configuration_1 = __nccwpck_require__(5527); const configuration_1 = __nccwpck_require__(5527);
const pullRequests_1 = __nccwpck_require__(4217); const pullRequests_1 = __nccwpck_require__(4217);
const utils_1 = __nccwpck_require__(918);
function buildChangelog(diffInfo, prs, options) { function buildChangelog(diffInfo, prs, options) {
// sort to target order // sort to target order
const config = options.configuration; const config = options.configuration;
@@ -1517,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 validatedTransformers = validateTransformers(config.transformers);
const transformedMap = new Map(); const transformedMap = new Map();
// convert PRs to their text representation // convert PRs to their text representation
for (const pr of prs) { 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(`️ Used ${validatedTransformers.length} transformers to adjust message`);
core.info(`✒️ Wrote messages for ${prs.length} pull requests`); core.info(`✒️ Wrote messages for ${prs.length} pull requests`);
@@ -1538,7 +1522,7 @@ function buildChangelog(diffInfo, prs, options) {
const uncategorizedPrs = []; const uncategorizedPrs = [];
// bring elements in order // bring elements in order
for (const [pr, body] of transformedMap) { 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); ignoredPrs.push(body);
continue; continue;
} }
@@ -1549,7 +1533,7 @@ function buildChangelog(diffInfo, prs, options) {
for (const [category, pullRequests] of categorized) { for (const [category, pullRequests] of categorized) {
// check if any exclude label matches // check if any exclude label matches
if (category.exclude_labels !== undefined) { 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()) { if (core.isDebug()) {
const prNum = pr.number; const prNum = pr.number;
const prLabels = pr.labels; const prLabels = pr.labels;
@@ -1560,13 +1544,13 @@ function buildChangelog(diffInfo, prs, options) {
} }
} }
if (category.exhaustive === true) { 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); pullRequests.push(body);
matched = true; matched = true;
} }
} }
else { 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); pullRequests.push(body);
matched = true; matched = true;
} }
@@ -1644,72 +1628,119 @@ function buildChangelog(diffInfo, prs, options) {
} }
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`); core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`);
// fill template // fill template
let transformedChangelog = config.template || configuration_1.DefaultConfiguration.template; const placeholderMap = new Map();
transformedChangelog = transformedChangelog.replace(/\${{CHANGELOG}}/g, changelog); placeholderMap.set('CHANGELOG', changelog);
transformedChangelog = transformedChangelog.replace(/\${{UNCATEGORIZED}}/g, changelogUncategorized); placeholderMap.set('UNCATEGORIZED', changelogUncategorized);
transformedChangelog = transformedChangelog.replace(/\${{OPEN}}/g, changelogOpen); placeholderMap.set('OPEN', changelogOpen);
transformedChangelog = transformedChangelog.replace(/\${{IGNORED}}/g, changelogIgnored); placeholderMap.set('IGNORED', changelogIgnored);
// fill other placeholders // fill other placeholders
transformedChangelog = transformedChangelog.replace(/\${{CATEGORIZED_COUNT}}/g, categorizedPrs.length.toString()); placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString());
transformedChangelog = transformedChangelog.replace(/\${{UNCATEGORIZED_COUNT}}/g, uncategorizedPrs.length.toString()); placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString());
transformedChangelog = transformedChangelog.replace(/\${{OPEN_COUNT}}/g, openPrs.length.toString()); placeholderMap.set('OPEN_COUNT', openPrs.length.toString());
transformedChangelog = transformedChangelog.replace(/\${{IGNORED_COUNT}}/g, ignoredPrs.length.toString()); placeholderMap.set('IGNORED_COUNT', ignoredPrs.length.toString());
// code change placeholders // code change placeholders
transformedChangelog = transformedChangelog.replace(/\${{CHANGED_FILES}}/g, diffInfo.changedFiles.toString()); placeholderMap.set('CHANGED_FILES', diffInfo.changedFiles.toString());
transformedChangelog = transformedChangelog.replace(/\${{ADDITIONS}}/g, diffInfo.additions.toString()); placeholderMap.set('ADDITIONS', diffInfo.additions.toString());
transformedChangelog = transformedChangelog.replace(/\${{DELETIONS}}/g, diffInfo.deletions.toString()); placeholderMap.set('DELETIONS', diffInfo.deletions.toString());
transformedChangelog = transformedChangelog.replace(/\${{CHANGES}}/g, diffInfo.changes.toString()); placeholderMap.set('CHANGES', diffInfo.changes.toString());
transformedChangelog = transformedChangelog.replace(/\${{COMMITS}}/g, diffInfo.commits.toString()); placeholderMap.set('COMMITS', diffInfo.commits.toString());
transformedChangelog = fillAdditionalPlaceholders(transformedChangelog, options); 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`); core.info(`️ Filled template`);
return transformedChangelog; return transformedChangelog;
} }
exports.buildChangelog = buildChangelog; 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; var _a, _b;
let transformed = text; placeholderMap.set('OWNER', options.owner);
// repository placeholders placeholderMap.set('REPO', options.repo);
transformed = transformed.replace(/\${{OWNER}}/g, options.owner); placeholderMap.set('FROM_TAG', options.fromTag.name);
transformed = transformed.replace(/\${{REPO}}/g, options.repo); placeholderMap.set('FROM_TAG_DATE', ((_a = options.fromTag.date) === null || _a === void 0 ? void 0 : _a.toISOString()) || '');
transformed = transformed.replace(/\${{FROM_TAG}}/g, options.fromTag.name); placeholderMap.set('TO_TAG', options.toTag.name);
transformed = transformed.replace(/\${{FROM_TAG_DATE}}/g, ((_a = options.fromTag.date) === null || _a === void 0 ? void 0 : _a.toISOString()) || ''); placeholderMap.set('TO_TAG_DATE', ((_b = options.toTag.date) === null || _b === void 0 ? void 0 : _b.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()) || '');
const fromDate = options.fromTag.date; const fromDate = options.fromTag.date;
const toDate = options.toTag.date; const toDate = options.toTag.date;
if (fromDate !== undefined && toDate !== undefined) { if (fromDate !== undefined && toDate !== undefined) {
transformed = transformed.replace(/\${{DAYS_SINCE}}/g, toDate.diff(fromDate, 'days').toString() || ''); placeholderMap.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '');
} }
else { 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; return transformed;
} }
exports.fillAdditionalPlaceholders = fillAdditionalPlaceholders; function replacePrPlaceholders(template, placeholderPrMap /* map with all pr related custom placeholder values */) {
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;
let transformed = template; let transformed = template;
transformed = transformed.replace(/\${{NUMBER}}/g, pr.number.toString()); for (const [key, values] of placeholderPrMap) {
transformed = transformed.replace(/\${{TITLE}}/g, pr.title); for (let i = 0; i < values.length; i++) {
transformed = transformed.replace(/\${{URL}}/g, pr.htmlURL); transformed = transformed.replaceAll(`\${{${key}[${i}]}}`, values[i]);
transformed = transformed.replace(/\${{STATUS}}/g, pr.status); }
transformed = transformed.replace(/\${{CREATED_AT}}/g, pr.createdAt.toISOString()); transformed = transformed.replaceAll(`\${{${key}[*]}}`, values.join(''));
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); return transformed;
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(', ')) || ''); function cleanupPrPlaceHolders(template, placeholders /* placeholders to apply */) {
transformed = transformed.replace(/\${{MILESTONE}}/g, pr.milestone || ''); let transformed = template;
transformed = transformed.replace(/\${{BODY}}/g, pr.body); for (const [, phs] of placeholders) {
transformed = transformed.replace(/\${{ASSIGNEES}}/g, ((_d = pr.assignees) === null || _d === void 0 ? void 0 : _d.join(', ')) || ''); for (const ph of phs) {
transformed = transformed.replace(/\${{REVIEWERS}}/g, ((_e = pr.requestedReviewers) === null || _e === void 0 ? void 0 : _e.join(', ')) || ''); transformed = transformed.replaceAll(new RegExp(`\\$\\{\\{${ph.name}\\[.+?\\]\\}\\}`, 'gu'), '');
transformed = transformed.replace(/\${{APPROVERS}}/g, ((_f = pr.approvedReviewers) === null || _f === void 0 ? void 0 : _f.join(', ')) || ''); }
}
return transformed; return transformed;
} }
function transform(filled, transformers) { function transform(filled, transformers) {
@@ -1849,7 +1880,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); 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 core = __importStar(__nccwpck_require__(2186));
const fs = __importStar(__nccwpck_require__(7147)); const fs = __importStar(__nccwpck_require__(7147));
const path = __importStar(__nccwpck_require__(1017)); const path = __importStar(__nccwpck_require__(1017));
@@ -1983,6 +2014,24 @@ function writeOutput(githubWorkspacePath, outputFile, changelog) {
} }
} }
exports.writeOutput = writeOutput; 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;
/***/ }), /***/ }),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -21
View File
@@ -31,23 +31,13 @@ export interface CommitInfo {
export class Commits { export class Commits {
constructor(private octokit: Octokit) {} constructor(private octokit: Octokit) {}
async getDiff( async getDiff(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> {
owner: string,
repo: string,
base: string,
head: string
): Promise<DiffInfo> {
const diff: DiffInfo = await this.getDiffRemote(owner, repo, base, head) const diff: DiffInfo = await this.getDiffRemote(owner, repo, base, head)
diff.commitInfo = this.sortCommits(diff.commitInfo) diff.commitInfo = this.sortCommits(diff.commitInfo)
return diff return diff
} }
private async getDiffRemote( private async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> {
owner: string,
repo: string,
base: string,
head: string
): Promise<DiffInfo> {
let changedFilesCount = 0 let changedFilesCount = 0
let additionCount = 0 let additionCount = 0
let deletionCount = 0 let deletionCount = 0
@@ -56,8 +46,7 @@ export class Commits {
// Fetch comparisons recursively until we don't find any commits // Fetch comparisons recursively until we don't find any commits
// This is because the GitHub API limits the number of commits returned in a single response. // This is because the GitHub API limits the number of commits returned in a single response.
let commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = let commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = []
[]
let compareHead = head let compareHead = head
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
@@ -84,9 +73,7 @@ export class Commits {
compareHead = `${commits[0].sha}^` compareHead = `${commits[0].sha}^`
} }
core.info( core.info(`️ Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`)
`️ Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`
)
return { return {
changedFiles: changedFilesCount, changedFiles: changedFilesCount,
@@ -135,10 +122,7 @@ export class Commits {
/** /**
* Filters out all commits which match the exclude pattern * Filters out all commits which match the exclude pattern
*/ */
export function filterCommits( export function filterCommits(commits: CommitInfo[], excludeMergeBranches: string[]): CommitInfo[] {
commits: CommitInfo[],
excludeMergeBranches: string[]
): CommitInfo[] {
const filteredCommits = [] const filteredCommits = []
for (const commit of commits) { for (const commit of commits) {
+9 -1
View File
@@ -14,6 +14,7 @@ export interface Configuration {
transformers: Transformer[] transformers: Transformer[]
tag_resolver: TagResolver tag_resolver: TagResolver
base_branches: string[] base_branches: string[]
custom_placeholders?: Placeholder[]
} }
export interface Category { export interface Category {
@@ -57,6 +58,12 @@ export interface TagResolver {
transformer?: Transformer // transforms the tag name using the regex, run after the filter 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 = { export const DefaultConfiguration: Configuration = {
max_tags_to_fetch: 200, // the amount of tags to fetch from the github API max_tags_to_fetch: 200, // the amount of tags to fetch from the github API
max_pull_requests: 200, // the amount of pull requests to process max_pull_requests: 200, // the amount of pull requests to process
@@ -94,5 +101,6 @@ export const DefaultConfiguration: Configuration = {
filter: undefined, // filter out all tags not matching the regex filter: undefined, // filter out all tags not matching the regex
transformer: undefined // transforms the tag name using the regex, run after the filter 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: []
} }
+8 -36
View File
@@ -2,9 +2,7 @@ import * as exec from '@actions/exec'
import * as io from '@actions/io' import * as io from '@actions/io'
import {directoryExistsSync} from './utils' import {directoryExistsSync} from './utils'
export async function createCommandManager( export async function createCommandManager(workingDirectory: string): Promise<GitCommandManager> {
workingDirectory: string
): Promise<GitCommandManager> {
return await GitCommandManager.createCommandManager(workingDirectory) return await GitCommandManager.createCommandManager(workingDirectory)
} }
@@ -20,52 +18,28 @@ class GitCommandManager {
} }
async latestTag(): Promise<string> { async latestTag(): Promise<string> {
const revListOutput = await this.execGit([ const revListOutput = await this.execGit(['rev-list', '--tags', '--skip=0', '--max-count=1'])
'rev-list', const output = await this.execGit(['describe', '--abbrev=0', '--tags', revListOutput.stdout.trim()])
'--tags',
'--skip=0',
'--max-count=1'
])
const output = await this.execGit([
'describe',
'--abbrev=0',
'--tags',
revListOutput.stdout.trim()
])
return output.stdout.trim() return output.stdout.trim()
} }
async initialCommit(): Promise<string> { async initialCommit(): Promise<string> {
const revListOutput = await this.execGit([ const revListOutput = await this.execGit(['rev-list', '--max-parents=0', 'HEAD'])
'rev-list',
'--max-parents=0',
'HEAD'
])
return revListOutput.stdout.trim() return revListOutput.stdout.trim()
} }
async tagCreation(tagName: string): Promise<string> { async tagCreation(tagName: string): Promise<string> {
const creationDate = await this.execGit([ const creationDate = await this.execGit(['for-each-ref', '--format="%(creatordate:rfc)"', `refs/tags/${tagName}`])
'for-each-ref',
'--format="%(creatordate:rfc)"',
`refs/tags/${tagName}`
])
return creationDate.stdout.trim().replace(/"/g, '') return creationDate.stdout.trim().replace(/"/g, '')
} }
static async createCommandManager( static async createCommandManager(workingDirectory: string): Promise<GitCommandManager> {
workingDirectory: string
): Promise<GitCommandManager> {
const result = new GitCommandManager() const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory) await result.initializeCommandManager(workingDirectory)
return result return result
} }
private async execGit( private async execGit(args: string[], allowAllExitCodes = false, silent = false): Promise<GitOutput> {
args: string[],
allowAllExitCodes = false,
silent = false
): Promise<GitOutput> {
directoryExistsSync(this.workingDirectory, true) directoryExistsSync(this.workingDirectory, true)
const result = new GitOutput() const result = new GitOutput()
@@ -88,9 +62,7 @@ class GitCommandManager {
return result return result
} }
private async initializeCommandManager( private async initializeCommandManager(workingDirectory: string): Promise<void> {
workingDirectory: string
): Promise<void> {
this.workingDirectory = workingDirectory this.workingDirectory = workingDirectory
this.gitPath = await io.which('git', true) this.gitPath = await io.which('git', true)
} }
+2 -8
View File
@@ -1,11 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import { import {parseConfiguration, resolveConfiguration, retrieveRepositoryPath, writeOutput} from './utils'
parseConfiguration,
resolveConfiguration,
retrieveRepositoryPath,
writeOutput
} from './utils'
import {ReleaseNotesBuilder} from './releaseNotesBuilder' import {ReleaseNotesBuilder} from './releaseNotesBuilder'
import {Configuration} from './configuration' import {Configuration} from './configuration'
@@ -44,8 +39,7 @@ async function run(): Promise<void> {
const ignorePreReleases = core.getInput('ignorePreReleases') === 'true' const ignorePreReleases = core.getInput('ignorePreReleases') === 'true'
const failOnError = core.getInput('failOnError') === 'true' const failOnError = core.getInput('failOnError') === 'true'
const fetchReviewers = core.getInput('fetchReviewers') === 'true' const fetchReviewers = core.getInput('fetchReviewers') === 'true'
const fetchReleaseInformation = const fetchReleaseInformation = core.getInput('fetchReleaseInformation') === 'true'
core.getInput('fetchReleaseInformation') === 'true'
const commitMode = core.getInput('commitMode') === 'true' const commitMode = core.getInput('commitMode') === 'true'
const result = await new ReleaseNotesBuilder( const result = await new ReleaseNotesBuilder(
+11 -43
View File
@@ -26,20 +26,14 @@ export interface PullRequestInfo {
type PullData = RestEndpointMethodTypes['pulls']['get']['response']['data'] type PullData = RestEndpointMethodTypes['pulls']['get']['response']['data']
type PullsListData = type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
RestEndpointMethodTypes['pulls']['list']['response']['data']
type PullReviewData = type PullReviewData = RestEndpointMethodTypes['pulls']['listReviews']['response']['data']
RestEndpointMethodTypes['pulls']['listReviews']['response']['data']
export class PullRequests { export class PullRequests {
constructor(private octokit: Octokit) {} constructor(private octokit: Octokit) {}
async getSingle( async getSingle(owner: string, repo: string, prNumber: number): Promise<PullRequestInfo | null> {
owner: string,
repo: string,
prNumber: number
): Promise<PullRequestInfo | null> {
try { try {
const {data} = await this.octokit.pulls.get({ const {data} = await this.octokit.pulls.get({
owner, owner,
@@ -49,9 +43,7 @@ export class PullRequests {
return mapPullRequest(data) return mapPullRequest(data)
} catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) { } catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.warning( core.warning(`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`)
`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`
)
return null return null
} }
} }
@@ -98,11 +90,7 @@ export class PullRequests {
return sortPrs(mergedPRs) return sortPrs(mergedPRs)
} }
async getOpen( async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
owner: string,
repo: string,
maxPullRequests: number
): Promise<PullRequestInfo[]> {
const openPrs: PullRequestInfo[] = [] const openPrs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({ const options = this.octokit.pulls.list.endpoint.merge({
owner, owner,
@@ -134,11 +122,7 @@ export class PullRequests {
return sortPrs(openPrs) return sortPrs(openPrs)
} }
async getReviewers( async getReviewers(owner: string, repo: string, pr: PullRequestInfo): Promise<PullReviewData[]> {
owner: string,
repo: string,
pr: PullRequestInfo
): Promise<PullReviewData[]> {
const options = this.octokit.pulls.listReviews.endpoint.merge({ const options = this.octokit.pulls.listReviews.endpoint.merge({
owner, owner,
repo, repo,
@@ -164,10 +148,7 @@ function sortPrs(pullRequests: PullRequestInfo[]): PullRequestInfo[] {
}) })
} }
export function sortPullRequests( export function sortPullRequests(pullRequests: PullRequestInfo[], sort: Sort | string): PullRequestInfo[] {
pullRequests: PullRequestInfo[],
sort: Sort | string
): PullRequestInfo[] {
let sortConfig: Sort let sortConfig: Sort
// legacy handling to support string sort config // legacy handling to support string sort config
@@ -191,11 +172,7 @@ export function sortPullRequests(
return pullRequests return pullRequests
} }
export function compare( export function compare(a: PullRequestInfo, b: PullRequestInfo, sort: Sort): number {
a: PullRequestInfo,
b: PullRequestInfo,
sort: Sort
): number {
if (sort.on_property === 'mergedAt') { if (sort.on_property === 'mergedAt') {
const aa = a.mergedAt || a.createdAt const aa = a.mergedAt || a.createdAt
const bb = b.mergedAt || b.createdAt const bb = b.mergedAt || b.createdAt
@@ -212,10 +189,7 @@ export function compare(
} }
// helper function to add a special open label to prs not merged. // helper function to add a special open label to prs not merged.
function attachSpeciaLabels( function attachSpeciaLabels(status: 'open' | 'merged', labels: Set<string>): Set<string> {
status: 'open' | 'merged',
labels: Set<string>
): Set<string> {
labels.add(`--rcba-${status}`) labels.add(`--rcba-${status}`)
return labels return labels
} }
@@ -234,17 +208,11 @@ const mapPullRequest = (
mergeCommitSha: pr.merge_commit_sha || '', mergeCommitSha: pr.merge_commit_sha || '',
author: pr.user?.login || '', author: pr.user?.login || '',
repoName: pr.base.repo.full_name, repoName: pr.base.repo.full_name,
labels: attachSpeciaLabels( labels: attachSpeciaLabels(status, new Set(pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || [])),
status,
new Set(
pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || []
)
),
milestone: pr.milestone?.title || '', milestone: pr.milestone?.title || '',
body: pr.body || '', body: pr.body || '',
assignees: pr.assignees?.map(asignee => asignee?.login || '') || [], assignees: pr.assignees?.map(asignee => asignee?.login || '') || [],
requestedReviewers: requestedReviewers: pr.requested_reviewers?.map(reviewer => reviewer?.login || '') || [],
pr.requested_reviewers?.map(reviewer => reviewer?.login || '') || [],
approvedReviewers: [], approvedReviewers: [],
status status
}) })
+21 -56
View File
@@ -3,7 +3,7 @@ import {Commits, filterCommits, DiffInfo, DefaultDiffInfo} from './commits'
import {Configuration, DefaultConfiguration} from './configuration' import {Configuration, DefaultConfiguration} from './configuration'
import {PullRequestInfo, PullRequests} from './pullRequests' import {PullRequestInfo, PullRequests} from './pullRequests'
import {Octokit} from '@octokit/rest' import {Octokit} from '@octokit/rest'
import {buildChangelog, fillAdditionalPlaceholders} from './transform' import {buildChangelog, replaceEmptyTemplate} from './transform'
import {failOrError} from './utils' import {failOrError} from './utils'
import {TagInfo} from './tags' import {TagInfo} from './tags'
@@ -61,55 +61,40 @@ export class ReleaseNotes {
if (mergedPullRequests.length === 0) { if (mergedPullRequests.length === 0) {
core.warning(`⚠️ No pull requests found`) core.warning(`⚠️ No pull requests found`)
return fillAdditionalPlaceholders( return replaceEmptyTemplate(
this.options.configuration.empty_template || this.options.configuration.empty_template || DefaultConfiguration.empty_template,
DefaultConfiguration.empty_template,
this.options this.options
) )
} }
core.startGroup('📦 Build changelog') core.startGroup('📦 Build changelog')
const resultChangelog = buildChangelog( const resultChangelog = buildChangelog(diffInfo, mergedPullRequests, this.options)
diffInfo,
mergedPullRequests,
this.options
)
core.endGroup() core.endGroup()
return resultChangelog return resultChangelog
} }
private async getCommitHistory(octokit: Octokit): Promise<DiffInfo> { private async getCommitHistory(octokit: Octokit): Promise<DiffInfo> {
const {owner, repo, fromTag, toTag, failOnError} = this.options const {owner, repo, fromTag, toTag, failOnError} = this.options
core.info( core.info(`️ Comparing ${owner}/${repo} - '${fromTag.name}...${toTag.name}'`)
`️ Comparing ${owner}/${repo} - '${fromTag.name}...${toTag.name}'`
)
const commitsApi = new Commits(octokit) const commitsApi = new Commits(octokit)
let diffInfo: DiffInfo let diffInfo: DiffInfo
try { try {
diffInfo = await commitsApi.getDiff(owner, repo, fromTag.name, toTag.name) diffInfo = await commitsApi.getDiff(owner, repo, fromTag.name, toTag.name)
} catch (error) { } catch (error) {
failOrError( failOrError(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError)
`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`,
failOnError
)
return DefaultDiffInfo return DefaultDiffInfo
} }
if (diffInfo.commitInfo.length === 0) { if (diffInfo.commitInfo.length === 0) {
core.warning( core.warning(`⚠️ No commits found between - ${fromTag.name}...${toTag.name}`)
`⚠️ No commits found between - ${fromTag.name}...${toTag.name}`
)
return DefaultDiffInfo return DefaultDiffInfo
} }
return diffInfo return diffInfo
} }
private async getMergedPullRequests( private async getMergedPullRequests(octokit: Octokit): Promise<[DiffInfo, PullRequestInfo[]]> {
octokit: Octokit const {owner, repo, includeOpen, fetchReviewers, configuration} = this.options
): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, includeOpen, fetchReviewers, configuration} =
this.options
const diffInfo = await this.getCommitHistory(octokit) const diffInfo = await this.getCommitHistory(octokit)
const commits = diffInfo.commitInfo const commits = diffInfo.commitInfo
@@ -122,18 +107,14 @@ export class ReleaseNotes {
let fromDate = firstCommit.date let fromDate = firstCommit.date
const toDate = lastCommit.date const toDate = lastCommit.date
const maxDays = const maxDays = configuration.max_back_track_time_days || DefaultConfiguration.max_back_track_time_days
configuration.max_back_track_time_days ||
DefaultConfiguration.max_back_track_time_days
const maxFromDate = toDate.clone().subtract(maxDays, 'days') const maxFromDate = toDate.clone().subtract(maxDays, 'days')
if (maxFromDate.isAfter(fromDate)) { if (maxFromDate.isAfter(fromDate)) {
core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`) core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`)
fromDate = maxFromDate fromDate = maxFromDate
} }
core.info( core.info(`️ Fetching PRs between dates ${fromDate.toISOString()} to ${toDate.toISOString()} for ${owner}/${repo}`)
`️ Fetching PRs between dates ${fromDate.toISOString()} to ${toDate.toISOString()} for ${owner}/${repo}`
)
const pullRequestsApi = new PullRequests(octokit) const pullRequestsApi = new PullRequests(octokit)
const pullRequests = await pullRequestsApi.getBetweenDates( const pullRequests = await pullRequestsApi.getBetweenDates(
@@ -144,19 +125,14 @@ export class ReleaseNotes {
configuration.max_pull_requests || DefaultConfiguration.max_pull_requests configuration.max_pull_requests || DefaultConfiguration.max_pull_requests
) )
core.info( core.info(`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`)
`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`
)
const prCommits = filterCommits( const prCommits = filterCommits(
commits, commits,
configuration.exclude_merge_branches || configuration.exclude_merge_branches || DefaultConfiguration.exclude_merge_branches
DefaultConfiguration.exclude_merge_branches
) )
core.info( core.info(`️ Retrieved ${prCommits.length} release commits for ${owner}/${repo}`)
`️ Retrieved ${prCommits.length} release commits for ${owner}/${repo}`
)
// create array of commits for this release // create array of commits for this release
const releaseCommitHashes = prCommits.map(commmit => { const releaseCommitHashes = prCommits.map(commmit => {
@@ -174,25 +150,19 @@ export class ReleaseNotes {
const openPullRequests = await pullRequestsApi.getOpen( const openPullRequests = await pullRequestsApi.getOpen(
owner, owner,
repo, repo,
configuration.max_pull_requests || configuration.max_pull_requests || DefaultConfiguration.max_pull_requests
DefaultConfiguration.max_pull_requests
) )
core.info( core.info(`️ Retrieved ${openPullRequests.length} open PRs for ${owner}/${repo}`)
`️ Retrieved ${openPullRequests.length} open PRs for ${owner}/${repo}`
)
// all pull requests // all pull requests
allPullRequests = allPullRequests.concat(openPullRequests) allPullRequests = allPullRequests.concat(openPullRequests)
core.info( core.info(`️ Retrieved ${allPullRequests.length} total PRs for ${owner}/${repo}`)
`️ Retrieved ${allPullRequests.length} total PRs for ${owner}/${repo}`
)
} }
// retrieve base branches we allow // retrieve base branches we allow
const baseBranches = const baseBranches = configuration.base_branches || DefaultConfiguration.base_branches
configuration.base_branches || DefaultConfiguration.base_branches
const baseBranchPatterns = baseBranches.map(baseBranch => { const baseBranchPatterns = baseBranches.map(baseBranch => {
return new RegExp(baseBranch.replace('\\\\', '\\'), 'gu') return new RegExp(baseBranch.replace('\\\\', '\\'), 'gu')
}) })
@@ -213,9 +183,7 @@ export class ReleaseNotes {
for (const pr of finalPrs) { for (const pr of finalPrs) {
await pullRequestsApi.getReviewers(owner, repo, pr) await pullRequestsApi.getReviewers(owner, repo, pr)
if (pr.approvedReviewers.length > 0) { if (pr.approvedReviewers.length > 0) {
core.info( core.info(`️ Retrieved ${pr.approvedReviewers.length} reviewer(s) for PR ${owner}/${repo}/#${pr.number}`)
`️ Retrieved ${pr.approvedReviewers.length} reviewer(s) for PR ${owner}/${repo}/#${pr.number}`
)
} }
} }
} else { } else {
@@ -225,9 +193,7 @@ export class ReleaseNotes {
return [diffInfo, finalPrs] return [diffInfo, finalPrs]
} }
private async generateCommitPRs( private async generateCommitPRs(octokit: Octokit): Promise<[DiffInfo, PullRequestInfo[]]> {
octokit: Octokit
): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, configuration} = this.options const {owner, repo, configuration} = this.options
const diffInfo = await this.getCommitHistory(octokit) const diffInfo = await this.getCommitHistory(octokit)
@@ -238,8 +204,7 @@ export class ReleaseNotes {
const prCommits = filterCommits( const prCommits = filterCommits(
commits, commits,
configuration.exclude_merge_branches || configuration.exclude_merge_branches || DefaultConfiguration.exclude_merge_branches
DefaultConfiguration.exclude_merge_branches
) )
core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`) core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`)
+4 -18
View File
@@ -57,8 +57,7 @@ export class ReleaseNotesBuilder {
this.fromTag, this.fromTag,
this.toTag, this.toTag,
this.ignorePreReleases, this.ignorePreReleases,
this.configuration.max_tags_to_fetch || this.configuration.max_tags_to_fetch || DefaultConfiguration.max_tags_to_fetch,
DefaultConfiguration.max_tags_to_fetch,
this.configuration.tag_resolver || DefaultConfiguration.tag_resolver this.configuration.tag_resolver || DefaultConfiguration.tag_resolver
) )
@@ -73,10 +72,7 @@ export class ReleaseNotesBuilder {
let previousTag = tagRange.from let previousTag = tagRange.from
if (previousTag == null) { if (previousTag == null) {
failOrError( failOrError(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError)
`💥 Unable to retrieve previous tag given ${this.toTag}`,
this.failOnError
)
return null return null
} }
core.setOutput('fromTag', previousTag.name) core.setOutput('fromTag', previousTag.name)
@@ -85,18 +81,8 @@ export class ReleaseNotesBuilder {
if (this.fetchReleaseInformation) { if (this.fetchReleaseInformation) {
// load release information from the GitHub API // load release information from the GitHub API
core.info(`️ Fetching release information was enabled`) core.info(`️ Fetching release information was enabled`)
thisTag = await tagsApi.fillTagInformation( thisTag = await tagsApi.fillTagInformation(this.repositoryPath, this.owner, this.repo, thisTag)
this.repositoryPath, previousTag = await tagsApi.fillTagInformation(this.repositoryPath, this.owner, this.repo, previousTag)
this.owner,
this.repo,
thisTag
)
previousTag = await tagsApi.fillTagInformation(
this.repositoryPath,
this.owner,
this.repo,
previousTag
)
} else { } else {
core.debug(`️ Fetching release information was disabled`) core.debug(`️ Fetching release information was disabled`)
} }
+20 -70
View File
@@ -26,11 +26,7 @@ export interface SortableTagInfo extends TagInfo {
export class Tags { export class Tags {
constructor(private octokit: Octokit) {} constructor(private octokit: Octokit) {}
async getTags( async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
owner: string,
repo: string,
maxTagsToFetch: number
): Promise<TagInfo[]> {
const tagsInfo: TagInfo[] = [] const tagsInfo: TagInfo[] = []
const options = this.octokit.repos.listTags.endpoint.merge({ const options = this.octokit.repos.listTags.endpoint.merge({
owner, owner,
@@ -40,8 +36,7 @@ export class Tags {
}) })
for await (const response of this.octokit.paginate.iterator(options)) { for await (const response of this.octokit.paginate.iterator(options)) {
type TagsListData = type TagsListData = RestEndpointMethodTypes['repos']['listTags']['response']['data']
RestEndpointMethodTypes['repos']['listTags']['response']['data']
const tags: TagsListData = response.data as TagsListData const tags: TagsListData = response.data as TagsListData
for (const tag of tags) { for (const tag of tags) {
@@ -63,12 +58,7 @@ export class Tags {
return tagsInfo return tagsInfo
} }
async fillTagInformation( async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> {
repositoryPath: string,
owner: string,
repo: string,
tagInfo: TagInfo
): Promise<TagInfo> {
const options = this.octokit.repos.getReleaseByTag.endpoint.merge({ const options = this.octokit.repos.getReleaseByTag.endpoint.merge({
owner, owner,
repo, repo,
@@ -77,14 +67,11 @@ export class Tags {
try { try {
const response = await this.octokit.request(options) const response = await this.octokit.request(options)
type ReleaseInformation = type ReleaseInformation = RestEndpointMethodTypes['repos']['getReleaseByTag']['response']['data']
RestEndpointMethodTypes['repos']['getReleaseByTag']['response']['data']
const release: ReleaseInformation = response.data as ReleaseInformation const release: ReleaseInformation = response.data as ReleaseInformation
tagInfo.date = moment(release.created_at) tagInfo.date = moment(release.created_at)
core.info( core.info(`️ Retrieved information about the release associated with ${tagInfo.name} from the GitHub API`)
`️ Retrieved information about the release associated with ${tagInfo.name} from the GitHub API`
)
} catch (error) { } catch (error) {
core.info( core.info(
`⚠️ No release information found for ${tagInfo.name}, trying to retrieve tag creation time as fallback.` `⚠️ No release information found for ${tagInfo.name}, trying to retrieve tag creation time as fallback.`
@@ -117,13 +104,9 @@ export class Tags {
const length = tags.length const length = tags.length
if (tags.length > 1) { if (tags.length > 1) {
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
if ( if (tags[i].name.toLocaleLowerCase('en') === tag.toLocaleLowerCase('en')) {
tags[i].name.toLocaleLowerCase('en') === tag.toLocaleLowerCase('en')
) {
if (ignorePreReleases) { if (ignorePreReleases) {
core.info( core.info(`️ Enabled 'ignorePreReleases', searching for the closest release`)
`️ Enabled 'ignorePreReleases', searching for the closest release`
)
for (let ii = i + 1; ii < length; ii++) { for (let ii = i + 1; ii < length; ii++) {
if (!tags[ii].name.includes('-')) { if (!tags[ii].name.includes('-')) {
return tags[ii] return tags[ii]
@@ -134,15 +117,11 @@ export class Tags {
} }
} }
} else { } else {
core.info( core.info(`️ Only one tag found for the given repository. Usually this is the case for the initial release.`)
`️ Only one tag found for the given repository. Usually this is the case for the initial release.`
)
// if not specified try to retrieve tag from git // if not specified try to retrieve tag from git
const gitHelper = await createCommandManager(repositoryPath) const gitHelper = await createCommandManager(repositoryPath)
const initialCommit = await gitHelper.initialCommit() const initialCommit = await gitHelper.initialCommit()
core.info( core.info(`🔖 Resolved initial commit (${initialCommit}) from 'git rev-list --max-parents=0 HEAD'`)
`🔖 Resolved initial commit (${initialCommit}) from 'git rev-list --max-parents=0 HEAD'`
)
return {name: initialCommit, commit: initialCommit} return {name: initialCommit, commit: initialCommit}
} }
return tags[0] return tags[0]
@@ -203,25 +182,19 @@ export class Tags {
// if not specified try to retrieve tag from github.context.ref // if not specified try to retrieve tag from github.context.ref
if (github.context.ref?.startsWith('refs/tags/') === true) { if (github.context.ref?.startsWith('refs/tags/') === true) {
toTag = github.context.ref.replace('refs/tags/', '') toTag = github.context.ref.replace('refs/tags/', '')
core.info( core.info(`🔖 Resolved current tag (${toTag}) from the 'github.context.ref'`)
`🔖 Resolved current tag (${toTag}) from the 'github.context.ref'`
)
resultToTag = { resultToTag = {
name: toTag, name: toTag,
commit: toTag commit: toTag
} }
} else if (tags.length > 1) { } else if (tags.length > 1) {
resultToTag = tags[0] resultToTag = tags[0]
core.info( core.info(`🔖 Resolved current tag (${resultToTag.name}) from the tags git API`)
`🔖 Resolved current tag (${resultToTag.name}) from the tags git API`
)
} else { } else {
// if not specified try to retrieve tag from git // if not specified try to retrieve tag from git
const gitHelper = await createCommandManager(repositoryPath) const gitHelper = await createCommandManager(repositoryPath)
const latestTag = await gitHelper.latestTag() const latestTag = await gitHelper.latestTag()
core.info( core.info(`🔖 Resolved current tag (${latestTag}) from 'git rev-list --tags --skip=0 --max-count=1'`)
`🔖 Resolved current tag (${latestTag}) from 'git rev-list --tags --skip=0 --max-count=1'`
)
resultToTag = { resultToTag = {
name: latestTag, name: latestTag,
commit: latestTag commit: latestTag
@@ -241,17 +214,10 @@ export class Tags {
if (!fromTag) { if (!fromTag) {
core.debug(`fromTag undefined, trying to resolve via API`) core.debug(`fromTag undefined, trying to resolve via API`)
resultFromTag = await this.findPredecessorTag( resultFromTag = await this.findPredecessorTag(tags, repositoryPath, toTag, ignorePreReleases)
tags,
repositoryPath,
toTag,
ignorePreReleases
)
if (resultFromTag != null) { if (resultFromTag != null) {
core.info( core.info(`🔖 Resolved previous tag (${resultFromTag.name}) from the tags git API`)
`🔖 Resolved previous tag (${resultFromTag.name}) from the tags git API`
)
} }
} else { } else {
resultFromTag = { resultFromTag = {
@@ -271,20 +237,12 @@ export class Tags {
* Uses the provided filter (if available) to filter out any tags not currently relevant. * Uses the provided filter (if available) to filter out any tags not currently relevant.
* https://github.com/mikepenz/release-changelog-builder-action/issues/566 * https://github.com/mikepenz/release-changelog-builder-action/issues/566
*/ */
export function filterTags( export function filterTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[] {
tags: TagInfo[],
tagResolver: TagResolver
): TagInfo[] {
const filter = tagResolver.filter const filter = tagResolver.filter
if (filter !== undefined) { if (filter !== undefined) {
const regex = new RegExp( const regex = new RegExp(filter.pattern.replace('\\\\', '\\'), filter.flags ?? 'gu')
filter.pattern.replace('\\\\', '\\'),
filter.flags ?? 'gu'
)
const filteredTags = tags.filter(tag => tag.name.match(regex) !== null) const filteredTags = tags.filter(tag => tag.name.match(regex) !== null)
core.debug( core.debug(`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`)
`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`
)
return filteredTags return filteredTags
} else { } else {
return tags return tags
@@ -294,16 +252,10 @@ export function filterTags(
/** /**
* Helper function to transform the tag name given the transformer * Helper function to transform the tag name given the transformer
*/ */
function transformTags( function transformTags(tags: TagInfo[], transformer: RegexTransformer): TagInfo[] {
tags: TagInfo[],
transformer: RegexTransformer
): TagInfo[] {
return tags.map(function (tag) { return tags.map(function (tag) {
if (transformer.pattern) { if (transformer.pattern) {
const transformedName = tag.name.replace( const transformedName = tag.name.replace(transformer.pattern, transformer.target)
transformer.pattern,
transformer.target
)
core.debug(`️ Transformed ${tag.name} to ${transformedName}`) core.debug(`️ Transformed ${tag.name} to ${transformedName}`)
return { return {
tmp: tag.name, // remember the original name tmp: tag.name, // remember the original name
@@ -347,9 +299,7 @@ function semVerSorting(tags: TagInfo[]): TagInfo[] {
loose: true loose: true
}) !== null }) !== null
if (!isValid) { if (!isValid) {
core.debug( core.debug(`⚠️ dropped tag ${tag.name} because it is not a valid semver tag`)
`⚠️ dropped tag ${tag.name} because it is not a valid semver tag`
)
} }
return isValid return isValid
}) })
+152 -195
View File
@@ -1,19 +1,19 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import { import {Category, DefaultConfiguration, Extractor, Placeholder, Transformer} from './configuration'
Category,
DefaultConfiguration,
Extractor,
Transformer
} from './configuration'
import {PullRequestInfo, sortPullRequests} from './pullRequests' import {PullRequestInfo, sortPullRequests} from './pullRequests'
import {ReleaseNotesOptions} from './releaseNotes' import {ReleaseNotesOptions} from './releaseNotes'
import {DiffInfo} from './commits' import {DiffInfo} from './commits'
import {createOrSet, haveCommonElements, haveEveryElements} from './utils'
export function buildChangelog( export interface RegexTransformer {
diffInfo: DiffInfo, pattern: RegExp | null
prs: PullRequestInfo[], target: string
options: ReleaseNotesOptions onProperty?: ('title' | 'author' | 'milestone' | 'body' | 'status' | 'branch')[] | undefined
): string { method?: 'replace' | 'match' | undefined
onEmpty?: string | undefined
}
export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], options: ReleaseNotesOptions): string {
// sort to target order // sort to target order
const config = options.configuration const config = options.configuration
const sort = config.sort || DefaultConfiguration.sort const sort = config.sort || DefaultConfiguration.sort
@@ -33,18 +33,14 @@ export function buildChangelog(
if (extracted !== null && extracted.length > 0) { if (extracted !== null && extracted.length > 0) {
deduplicatedMap.set(extracted[0], pr) deduplicatedMap.set(extracted[0], pr)
} else { } else {
core.info( core.info(` PR (${pr.number}) did not resolve an ID using the \`duplicate_filter\``)
` PR (${pr.number}) did not resolve an ID using the \`duplicate_filter\``
)
unmatched.push(pr) unmatched.push(pr)
} }
} }
const deduplicatedPRs = Array.from(deduplicatedMap.values()) const deduplicatedPRs = Array.from(deduplicatedMap.values())
deduplicatedPRs.push(...unmatched) // add all unmatched PRs to map deduplicatedPRs.push(...unmatched) // add all unmatched PRs to map
const removedElements = prs.length - deduplicatedPRs.length const removedElements = prs.length - deduplicatedPRs.length
core.info( core.info(`️ Removed ${removedElements} pull requests during deduplication`)
`️ Removed ${removedElements} pull requests during deduplication`
)
prs = sortPullRequests(deduplicatedPRs, sort) // resort deduplicatedPRs prs = sortPullRequests(deduplicatedPRs, sort) // resort deduplicatedPRs
} else { } else {
core.warning(`⚠️ Configured \`duplicate_filter\` invalid.`) core.warning(`⚠️ Configured \`duplicate_filter\` invalid.`)
@@ -64,6 +60,13 @@ export function buildChangelog(
} }
} }
// 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 validatedTransformers = validateTransformers(config.transformers)
const transformedMap = new Map<PullRequestInfo, string>() const transformedMap = new Map<PullRequestInfo, string>()
// convert PRs to their text representation // convert PRs to their text representation
@@ -71,24 +74,18 @@ export function buildChangelog(
transformedMap.set( transformedMap.set(
pr, pr,
transform( transform(
fillTemplate( fillPrTemplate(pr, config.pr_template || DefaultConfiguration.pr_template, placeholders, placeholderPrMap),
pr,
config.pr_template || DefaultConfiguration.pr_template
),
validatedTransformers validatedTransformers
) )
) )
} }
core.info( core.info(`️ Used ${validatedTransformers.length} transformers to adjust message`)
`️ Used ${validatedTransformers.length} transformers to adjust message`
)
core.info(`✒️ Wrote messages for ${prs.length} pull requests`) core.info(`✒️ Wrote messages for ${prs.length} pull requests`)
// bring PRs into the order of categories // bring PRs into the order of categories
const categorized = new Map<Category, string[]>() const categorized = new Map<Category, string[]>()
const categories = config.categories || DefaultConfiguration.categories const categories = config.categories || DefaultConfiguration.categories
const ignoredLabels = const ignoredLabels = config.ignore_labels || DefaultConfiguration.ignore_labels
config.ignore_labels || DefaultConfiguration.ignore_labels
for (const category of categories) { for (const category of categories) {
categorized.set(category, []) categorized.set(category, [])
@@ -203,9 +200,7 @@ export function buildChangelog(
for (const pr of uncategorizedPrs) { for (const pr of uncategorizedPrs) {
changelogUncategorized = `${changelogUncategorized + pr}\n` changelogUncategorized = `${changelogUncategorized + pr}\n`
} }
core.info( core.info(`✒️ Wrote ${uncategorizedPrs.length} non categorized pull requests down`)
`✒️ Wrote ${uncategorizedPrs.length} non categorized pull requests down`
)
if (core.isDebug()) { if (core.isDebug()) {
for (const pr of uncategorizedPrs) { for (const pr of uncategorizedPrs) {
core.debug(` ${pr}`) core.debug(` ${pr}`)
@@ -239,148 +234,144 @@ export function buildChangelog(
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`) core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`)
// fill template // fill template
let transformedChangelog = config.template || DefaultConfiguration.template const placeholderMap = new Map<string, string>()
transformedChangelog = transformedChangelog.replace( placeholderMap.set('CHANGELOG', changelog)
/\${{CHANGELOG}}/g, placeholderMap.set('UNCATEGORIZED', changelogUncategorized)
changelog placeholderMap.set('OPEN', changelogOpen)
) placeholderMap.set('IGNORED', changelogIgnored)
transformedChangelog = transformedChangelog.replace(
/\${{UNCATEGORIZED}}/g,
changelogUncategorized
)
transformedChangelog = transformedChangelog.replace(
/\${{OPEN}}/g,
changelogOpen
)
transformedChangelog = transformedChangelog.replace(
/\${{IGNORED}}/g,
changelogIgnored
)
// fill other placeholders // fill other placeholders
transformedChangelog = transformedChangelog.replace( placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString())
/\${{CATEGORIZED_COUNT}}/g, placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString())
categorizedPrs.length.toString() placeholderMap.set('OPEN_COUNT', openPrs.length.toString())
) placeholderMap.set('IGNORED_COUNT', ignoredPrs.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()
)
// code change placeholders // code change placeholders
transformedChangelog = transformedChangelog.replace( placeholderMap.set('CHANGED_FILES', diffInfo.changedFiles.toString())
/\${{CHANGED_FILES}}/g, placeholderMap.set('ADDITIONS', diffInfo.additions.toString())
diffInfo.changedFiles.toString() placeholderMap.set('DELETIONS', diffInfo.deletions.toString())
) placeholderMap.set('CHANGES', diffInfo.changes.toString())
transformedChangelog = transformedChangelog.replace( placeholderMap.set('COMMITS', diffInfo.commits.toString())
/\${{ADDITIONS}}/g, fillAdditionalPlaceholders(options, placeholderMap)
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
)
let transformedChangelog = config.template || DefaultConfiguration.template
transformedChangelog = replacePlaceholders(transformedChangelog, placeholderMap, placeholders, placeholderPrMap)
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap)
transformedChangelog = cleanupPrPlaceHolders(transformedChangelog, placeholders)
core.info(`️ Filled template`) core.info(`️ Filled template`)
return transformedChangelog return transformedChangelog
} }
export function fillAdditionalPlaceholders( export function replaceEmptyTemplate(template: string, options: ReleaseNotesOptions): string {
text: string, const placeholders = new Map<string, Placeholder[]>()
options: ReleaseNotesOptions for (const ph of options.configuration.custom_placeholders || []) {
): string { createOrSet(placeholders, ph.source, ph)
let transformed = text }
// repository placeholders const placeholderMap = new Map<string, string>()
transformed = transformed.replace(/\${{OWNER}}/g, options.owner) fillAdditionalPlaceholders(options, placeholderMap)
transformed = transformed.replace(/\${{REPO}}/g, options.repo) return replacePlaceholders(template, placeholderMap, placeholders)
transformed = transformed.replace(/\${{FROM_TAG}}/g, options.fromTag.name) }
transformed = transformed.replace(
/\${{FROM_TAG_DATE}}/g, function fillAdditionalPlaceholders(
options.fromTag.date?.toISOString() || '' options: ReleaseNotesOptions,
) placeholderMap: Map<string, string> /* placeholderKey and original value */
transformed = transformed.replace(/\${{TO_TAG}}/g, options.toTag.name) ): void {
transformed = transformed.replace( placeholderMap.set('OWNER', options.owner)
/\${{TO_TAG_DATE}}/g, placeholderMap.set('REPO', options.repo)
options.toTag.date?.toISOString() || '' 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 fromDate = options.fromTag.date
const toDate = options.toTag.date const toDate = options.toTag.date
if (fromDate !== undefined && toDate !== undefined) { if (fromDate !== undefined && toDate !== undefined) {
transformed = transformed.replace( placeholderMap.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '')
/\${{DAYS_SINCE}}/g,
toDate.diff(fromDate, 'days').toString() || ''
)
} else { } else {
transformed = transformed.replace(/\${{DAYS_SINCE}}/g, '') placeholderMap.set('DAYS_SINCE', '')
} }
transformed = transformed.replace( placeholderMap.set(
/\${{RELEASE_DIFF}}/g, 'RELEASE_DIFF',
`https://github.com/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}` `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 return transformed
} }
function haveCommonElements(arr1: string[], arr2: Set<string>): Boolean { function replacePrPlaceholders(
return arr1.some(item => arr2.has(item)) template: string,
} placeholderPrMap: Map<string, string[]> /* map with all pr related custom placeholder values */
): string {
function haveEveryElements(arr1: string[], arr2: Set<string>): Boolean {
return arr1.every(item => arr2.has(item))
}
function fillTemplate(pr: PullRequestInfo, template: string): string {
let transformed = template let transformed = template
transformed = transformed.replace(/\${{NUMBER}}/g, pr.number.toString()) for (const [key, values] of placeholderPrMap) {
transformed = transformed.replace(/\${{TITLE}}/g, pr.title) for (let i = 0; i < values.length; i++) {
transformed = transformed.replace(/\${{URL}}/g, pr.htmlURL) transformed = transformed.replaceAll(`\${{${key}[${i}]}}`, values[i])
transformed = transformed.replace(/\${{STATUS}}/g, pr.status) }
transformed = transformed.replace( transformed = transformed.replaceAll(`\${{${key}[*]}}`, values.join(''))
/\${{CREATED_AT}}/g, }
pr.createdAt.toISOString() return transformed
) }
transformed = transformed.replace(
/\${{MERGED_AT}}/g, function cleanupPrPlaceHolders(
pr.mergedAt?.toISOString() || '' template: string,
) placeholders: Map<string, Placeholder[]> /* placeholders to apply */
transformed = transformed.replace(/\${{MERGE_SHA}}/g, pr.mergeCommitSha) ): string {
transformed = transformed.replace(/\${{AUTHOR}}/g, pr.author) let transformed = template
transformed = transformed.replace( for (const [, phs] of placeholders) {
/\${{LABELS}}/g, for (const ph of phs) {
[...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || '' transformed = transformed.replaceAll(new RegExp(`\\$\\{\\{${ph.name}\\[.+?\\]\\}\\}`, 'gu'), '')
) }
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(', ') || ''
)
return transformed return transformed
} }
@@ -397,11 +388,8 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
return transformed return transformed
} }
function validateTransformers( function validateTransformers(specifiedTransformers: Transformer[]): RegexTransformer[] {
specifiedTransformers: Transformer[] const transformers = specifiedTransformers || DefaultConfiguration.transformers
): RegexTransformer[] {
const transformers =
specifiedTransformers || DefaultConfiguration.transformers
return transformers return transformers
.map(transformer => { .map(transformer => {
return validateTransformer(transformer) return validateTransformer(transformer)
@@ -412,9 +400,7 @@ function validateTransformers(
}) })
} }
export function validateTransformer( export function validateTransformer(transformer?: Transformer): RegexTransformer | null {
transformer?: Transformer
): RegexTransformer | null {
if (transformer === undefined) { if (transformer === undefined) {
return null return null
} }
@@ -436,10 +422,7 @@ export function validateTransformer(
} }
return { return {
pattern: new RegExp( pattern: new RegExp(transformer.pattern.replace('\\\\', '\\'), transformer.flags ?? 'gu'),
transformer.pattern.replace('\\\\', '\\'),
transformer.flags ?? 'gu'
),
target: transformer.target || '', target: transformer.target || '',
onProperty, onProperty,
method, method,
@@ -451,33 +434,20 @@ export function validateTransformer(
} }
} }
function extractValues( function extractValues(pr: PullRequestInfo, extractor: RegexTransformer, extractor_usecase: string): string[] | null {
pr: PullRequestInfo,
extractor: RegexTransformer,
extractor_usecase: string
): string[] | null {
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null return null
} }
if (extractor.onProperty !== undefined) { if (extractor.onProperty !== undefined) {
let results: string[] = [] let results: string[] = []
const list: ( const list: ('title' | 'author' | 'milestone' | 'body' | 'status' | 'branch')[] = extractor.onProperty
| 'title'
| 'author'
| 'milestone'
| 'body'
| 'status'
| 'branch'
)[] = extractor.onProperty
// eslint-disable-next-line @typescript-eslint/prefer-for-of // eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
const prop = list[i] const prop = list[i]
let value: string | undefined = pr[prop] let value: string | undefined = pr[prop]
if (value === undefined) { if (value === undefined) {
core.warning( core.warning(`⚠️ the provided property '${extractor.onProperty}' for \`${extractor_usecase}\` is not valid`)
`⚠️ the provided property '${extractor.onProperty}' for \`${extractor_usecase}\` is not valid`
)
value = pr['body'] value = pr['body']
} }
@@ -492,10 +462,7 @@ function extractValues(
} }
} }
function extractValuesFromString( function extractValuesFromString(value: string, extractor: RegexTransformer): string[] | null {
value: string,
extractor: RegexTransformer
): string[] | null {
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null return null
} }
@@ -516,13 +483,3 @@ function extractValuesFromString(
} }
return null 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
}
+26 -33
View File
@@ -23,10 +23,7 @@ export function retrieveRepositoryPath(providedPath: string): string {
/** /**
* Will automatically either report the message to the log, or mark the action as failed. Additionally defining the output failed, allowing it to be read in by other actions * Will automatically either report the message to the log, or mark the action as failed. Additionally defining the output failed, allowing it to be read in by other actions
*/ */
export function failOrError( export function failOrError(message: string | Error, failOnError: boolean): void {
message: string | Error,
failOnError: boolean
): void {
// if we report any failure, consider the action to have failed, may not make the build fail // if we report any failure, consider the action to have failed, may not make the build fail
core.setOutput('failed', true) core.setOutput('failed', true)
if (failOnError) { if (failOnError) {
@@ -39,16 +36,10 @@ export function failOrError(
/** /**
* Retrieves the configuration given the file path, if not found it will fallback to the `DefaultConfiguration` * Retrieves the configuration given the file path, if not found it will fallback to the `DefaultConfiguration`
*/ */
export function resolveConfiguration( export function resolveConfiguration(githubWorkspacePath: string, configurationFile: string): Configuration {
githubWorkspacePath: string,
configurationFile: string
): Configuration {
let configuration = DefaultConfiguration let configuration = DefaultConfiguration
if (configurationFile) { if (configurationFile) {
const configurationPath = path.resolve( const configurationPath = path.resolve(githubWorkspacePath, configurationFile)
githubWorkspacePath,
configurationFile
)
core.debug(`configurationPath = '${configurationPath}'`) core.debug(`configurationPath = '${configurationPath}'`)
const providedConfiguration = readConfiguration(configurationPath) const providedConfiguration = readConfiguration(configurationPath)
if (providedConfiguration) { if (providedConfiguration) {
@@ -76,9 +67,7 @@ function readConfiguration(filename: string): Configuration | undefined {
} catch (error) { } catch (error) {
core.debug(`Failed to load configuration due to: ${error}`) core.debug(`Failed to load configuration due to: ${error}`)
} }
core.info( core.info(`⚠️ Configuration provided, but it couldn't be found. Fallback to Defaults.`)
`⚠️ Configuration provided, but it couldn't be found. Fallback to Defaults.`
)
return undefined return undefined
} }
/** /**
@@ -87,14 +76,10 @@ function readConfiguration(filename: string): Configuration | undefined {
export function parseConfiguration(config: string): Configuration | undefined { export function parseConfiguration(config: string): Configuration | undefined {
try { try {
// for compatiblity with the `yml` file we require to use `#{{}}` instead of `${{}}` - replace it here. // for compatiblity with the `yml` file we require to use `#{{}}` instead of `${{}}` - replace it here.
const configurationJSON: Configuration = JSON.parse( const configurationJSON: Configuration = JSON.parse(config.replace(/#{{/g, '${{'))
config.replace(/#{{/g, '${{')
)
return configurationJSON return configurationJSON
} catch (error) { } catch (error) {
core.info( core.info(`⚠️ Configuration provided, but it couldn't be parsed. Fallback to Defaults.`)
`⚠️ Configuration provided, but it couldn't be parsed. Fallback to Defaults.`
)
return undefined return undefined
} }
} }
@@ -102,10 +87,7 @@ export function parseConfiguration(config: string): Configuration | undefined {
/** /**
* Checks if a given directory exists * Checks if a given directory exists
*/ */
export function directoryExistsSync( export function directoryExistsSync(inputPath: string, required?: boolean): boolean {
inputPath: string,
required?: boolean
): boolean {
if (!inputPath) { if (!inputPath) {
throw new Error("Arg 'path' must not be empty") throw new Error("Arg 'path' must not be empty")
} }
@@ -122,9 +104,7 @@ export function directoryExistsSync(
throw new Error(`Directory '${inputPath}' does not exist`) throw new Error(`Directory '${inputPath}' does not exist`)
} }
throw new Error( throw new Error(`Encountered an error when checking whether path '${inputPath}' exists: ${error.message}`)
`Encountered an error when checking whether path '${inputPath}' exists: ${error.message}`
)
} }
if (stats.isDirectory()) { if (stats.isDirectory()) {
@@ -139,11 +119,7 @@ export function directoryExistsSync(
/** /**
* Writes the changelog to the given the file * Writes the changelog to the given the file
*/ */
export function writeOutput( export function writeOutput(githubWorkspacePath: string, outputFile: string, changelog: string | null): void {
githubWorkspacePath: string,
outputFile: string,
changelog: string | null
): void {
if (outputFile && changelog) { if (outputFile && changelog) {
const outputPath = path.resolve(githubWorkspacePath, outputFile) const outputPath = path.resolve(githubWorkspacePath, outputFile)
core.debug(`outputPath = '${outputPath}'`) core.debug(`outputPath = '${outputPath}'`)
@@ -156,3 +132,20 @@ export function writeOutput(
} }
export type Unpacked<T> = T extends (infer U)[] ? U : T 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
View File
@@ -6,7 +6,8 @@
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "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. */ "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "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"] "exclude": ["node_modules", "**/*.test.ts"]
} }