Merge pull request #456 from mikepenz/feature/exhaustive_category_matching

Exhaustive label matching for categories
This commit is contained in:
Mike Penz
2021-08-27 11:44:34 +02:00
committed by GitHub
11 changed files with 203 additions and 125 deletions
+2 -1
View File
@@ -290,6 +290,7 @@ Table of descriptions for the `configuration.json` options to configure the resu
| categories | An array of `category` specifications, offering a flexible way to group changes into categories |
| category.title | The display name of a category in the changelog |
| category.labels | An array of labels, to match pull request labels against. If any PR label matches any category label, the pull request will show up under this category |
| category.exhaustive | Will require all labels defined within this category to be present on the matching PR. |
| ignore_labels | An array of labels, to match pull request labels against. If any PR label overlaps, the pull request will be ignored from the changelog. This takes precedence over category labels |
| sort | The sort order of pull requests. [ASC, DESC] |
| template | Specifies the global template to pick for creating the changelog. See [Template placeholders](#template-placeholders) for possible values |
@@ -325,7 +326,7 @@ $ npm run build && npm run package
$ npm test
# Verify lint is happy
$ npm run lint -- --fix
$ npm run lint -- --fixnpm run lint -- --fix
```
It's suggested to export the token to your path before running the tests so that API calls can be done to GitHub.
+121 -72
View File
@@ -6,79 +6,79 @@ import { DefaultConfiguration } from '../src/configuration';
jest.setTimeout(180000)
let configuration = DefaultConfiguration
configuration.categories = [
{
"title": "## 🚀 Features",
"labels": ["[Feature]"]
},
{
"title": "## 🐛 Fixes",
"labels": ["[Bug]", "[Issue]"]
},
{
"title": "## 🧪 Tests",
"labels": ["[Test]"]
}
]
configuration.categories = [
{
"title": "## 🚀 Features",
"labels": ["[Feature]"]
},
{
"title": "## 🐛 Fixes",
"labels": ["[Bug]", "[Issue]"]
},
{
"title": "## 🧪 Tests",
"labels": ["[Test]"]
}
]
let mergedPullRequests: PullRequestInfo[] = []
mergedPullRequests.push({
number: 1,
title: "[Feature][AB-1234] - this is a PR 1 title message",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
}, {
number: 2,
title: "[Issue][AB-4321] - this is a PR 2 title message",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
}, {
number: 3,
title: "[Issue][Feature][AB-1234321] - this is a PR 3 title message",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
}, {
number: 4,
title: "[AB-404] - not found label",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
})
let mergedPullRequests: PullRequestInfo[] = []
mergedPullRequests.push({
number: 1,
title: "[Feature][AB-1234] - this is a PR 1 title message",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
}, {
number: 2,
title: "[Issue][AB-4321] - this is a PR 2 title message",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
}, {
number: 3,
title: "[Issue][Feature][AB-1234321] - this is a PR 3 title message",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
}, {
number: 4,
title: "[AB-404] - not found label",
htmlURL: "",
baseBranch: "",
mergedAt: moment(),
mergeCommitSha: "sha1",
author: "Mike",
repoName: "test-repo",
labels: new Set<string>(),
milestone: "",
body: "no magic body for this matter",
assignees: [],
requestedReviewers: []
})
it('Extract label from title, combined regex', async () => {
configuration.label_extractor = [
@@ -189,3 +189,52 @@ it('Extract label from title, match multiple', async () => {
expect(resultChangelog).toStrictEqual(`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`)
})
it('Extract label from title, match multiple exhaustive', async () => {
let customConfig = configuration
customConfig.categories = [
{
"title": "## 🚀 Features and 🐛 Issues",
"labels": ["[Feature]", "[Issue]"],
"exhaustive": true
},
{
"title": "## 🚀 Features",
"labels": ["[Feature]", "[Feature2]"],
"exhaustive": true
},
{
"title": "## 🐛 Fixes",
"labels": ["[Issue]", "[Issue2]"],
"exhaustive": true
}
]
customConfig.label_extractor = [
{
"pattern": "\\[Feature\\]",
"on_property": "title",
"method": "match"
},
{
"pattern": "\\[Issue\\]",
"on_property": "title",
"method": "match"
}
]
const resultChangelog = buildChangelog(
mergedPullRequests,
{
owner: "mikepenz",
repo: "test-repo",
fromTag: "1.0.0",
toTag: "2.0.0",
failOnError: false,
commitMode: false,
configuration: customConfig
}
)
expect(resultChangelog).toStrictEqual(`## 🚀 Features and 🐛 Issues\n\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`)
})
Generated Vendored
+37 -26
View File
@@ -80,7 +80,7 @@ class Commits {
sha: commit.sha || '',
summary: commit.commit.message.split('\n')[0],
message: commit.commit.message,
date: moment_1.default((_a = commit.commit.committer) === null || _a === void 0 ? void 0 : _a.date),
date: (0, moment_1.default)((_a = commit.commit.committer) === null || _a === void 0 ? void 0 : _a.date),
author: ((_b = commit.commit.author) === null || _b === void 0 ? void 0 : _b.name) || '',
prNumber: undefined
});
@@ -269,7 +269,7 @@ class GitCommandManager {
}
execGit(args, allowAllExitCodes = false, silent = false) {
return __awaiter(this, void 0, void 0, function* () {
utils_1.directoryExistsSync(this.workingDirectory, true);
(0, utils_1.directoryExistsSync)(this.workingDirectory, true);
const result = new GitOutput();
const stdout = [];
const options = {
@@ -349,10 +349,10 @@ function run() {
try {
// read in path specification, resolve github workspace, and repo path
const inputPath = core.getInput('path');
const repositoryPath = utils_1.retrieveRepositoryPath(inputPath);
const repositoryPath = (0, utils_1.retrieveRepositoryPath)(inputPath);
// read in configuration file if possible
const configurationFile = core.getInput('configuration');
const configuration = utils_1.resolveConfiguration(repositoryPath, configurationFile);
const configuration = (0, utils_1.resolveConfiguration)(repositoryPath, configurationFile);
// read in repository inputs
const token = core.getInput('token');
const owner = core.getInput('owner') || github.context.repo.owner;
@@ -370,10 +370,10 @@ function run() {
const outputFile = core.getInput('outputFile');
if (outputFile !== '') {
core.debug(`Enabled writing the changelog to disk`);
utils_1.writeOutput(repositoryPath, outputFile, result);
(0, utils_1.writeOutput)(repositoryPath, outputFile, result);
}
}
catch (error) {
catch (error /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.setFailed(error.message);
}
});
@@ -444,7 +444,7 @@ class PullRequests {
});
return mapPullRequest(data);
}
catch (e) {
catch (e /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.warning(`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`);
return null;
}
@@ -471,7 +471,7 @@ class PullRequests {
}
const firstPR = prs[0];
if (firstPR === undefined ||
(firstPR.merged_at && fromDate.isAfter(moment_1.default(firstPR.merged_at))) ||
(firstPR.merged_at && fromDate.isAfter((0, moment_1.default)(firstPR.merged_at))) ||
mergedPRs.length >= maxPullRequests) {
if (mergedPRs.length >= maxPullRequests) {
core.warning(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests}`);
@@ -526,7 +526,7 @@ const mapPullRequest = (pr) => {
title: pr.title,
htmlURL: pr.html_url,
baseBranch: pr.base.ref,
mergedAt: moment_1.default(pr.merged_at),
mergedAt: (0, moment_1.default)(pr.merged_at),
mergeCommitSha: pr.merge_commit_sha || '',
author: ((_a = pr.user) === null || _a === void 0 ? void 0 : _a.login) || '',
repoName: pr.base.repo.full_name,
@@ -612,7 +612,7 @@ class ReleaseNotes {
return null;
}
core.startGroup('📦 Build changelog');
const resultChangelog = transform_1.buildChangelog(mergedPullRequests, this.options);
const resultChangelog = (0, transform_1.buildChangelog)(mergedPullRequests, this.options);
core.endGroup();
return resultChangelog;
});
@@ -627,7 +627,7 @@ class ReleaseNotes {
commits = yield commitsApi.getDiff(owner, repo, fromTag, toTag);
}
catch (error) {
utils_1.failOrError(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError);
(0, utils_1.failOrError)(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError);
return [];
}
if (commits.length === 0) {
@@ -659,7 +659,7 @@ class ReleaseNotes {
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);
core.info(`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`);
const prCommits = 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);
core.info(`️ Retrieved ${prCommits.length} release commits for ${owner}/${repo}`);
// create array of commits for this release
@@ -691,7 +691,7 @@ class ReleaseNotes {
if (commits.length === 0) {
return [];
}
const prCommits = 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);
core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`);
return prCommits.map(function (commit) {
@@ -778,7 +778,7 @@ class ReleaseNotesBuilder {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (!this.owner) {
utils_1.failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
return null;
}
else {
@@ -786,7 +786,7 @@ class ReleaseNotesBuilder {
core.debug(`Resolved 'owner' as ${this.owner}`);
}
if (!this.repo) {
utils_1.failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'owner'`, this.failOnError);
return null;
}
else {
@@ -805,7 +805,7 @@ class ReleaseNotesBuilder {
configuration_1.DefaultConfiguration.max_tags_to_fetch, this.configuration.tag_resolver || configuration_1.DefaultConfiguration.tag_resolver);
const thisTag = (_a = tagRange.to) === null || _a === void 0 ? void 0 : _a.name;
if (!thisTag) {
utils_1.failOrError(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError);
(0, utils_1.failOrError)(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError);
return null;
}
else {
@@ -815,7 +815,7 @@ class ReleaseNotesBuilder {
}
const previousTag = (_b = tagRange.from) === null || _b === void 0 ? void 0 : _b.name;
if (previousTag == null) {
utils_1.failOrError(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError);
(0, utils_1.failOrError)(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError);
return null;
}
this.fromTag = previousTag;
@@ -832,7 +832,7 @@ class ReleaseNotesBuilder {
};
const releaseNotes = new releaseNotes_1.ReleaseNotes(octokit, options);
return ((yield releaseNotes.pull()) ||
transform_1.fillAdditionalPlaceholders(this.configuration.empty_template ||
(0, transform_1.fillAdditionalPlaceholders)(this.configuration.empty_template ||
configuration_1.DefaultConfiguration.empty_template, options));
});
}
@@ -953,7 +953,7 @@ class Tags {
else {
core.info(`️ 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
const gitHelper = yield gitHelper_1.createCommandManager(repositoryPath);
const gitHelper = yield (0, gitHelper_1.createCommandManager)(repositoryPath);
const initialCommit = yield gitHelper.initialCommit();
core.info(`🔖 Resolved initial commit (${initialCommit}) from 'git rev-list --max-parents=0 HEAD'`);
return { name: initialCommit, commit: initialCommit };
@@ -991,7 +991,7 @@ class Tags {
}
else {
// if not specified try to retrieve tag from git
const gitHelper = yield gitHelper_1.createCommandManager(repositoryPath);
const gitHelper = yield (0, gitHelper_1.createCommandManager)(repositoryPath);
const latestTag = yield gitHelper.latestTag();
core.info(`🔖 Resolved current tag (${latestTag}) from 'git rev-list --tags --skip=0 --max-count=1'`);
resultToTag = {
@@ -1133,7 +1133,7 @@ function buildChangelog(prs, options) {
const config = options.configuration;
const sort = config.sort || configuration_1.DefaultConfiguration.sort;
const sortAsc = sort.toUpperCase() === 'ASC';
prs = pullRequests_1.sortPullRequests(prs, sortAsc);
prs = (0, pullRequests_1.sortPullRequests)(prs, sortAsc);
core.info(`️ Sorted all pull requests ascending: ${sort}`);
// extract additional labels from the commit message
const labelExtractors = validateTransformers(config.label_extractor);
@@ -1195,9 +1195,17 @@ function buildChangelog(prs, options) {
}
let matched = false;
for (const [category, pullRequests] of categorized) {
if (haveCommonElements(category.labels.map(lbl => lbl.toLocaleLowerCase()), pr.labels)) {
pullRequests.push(body);
matched = true;
if (category.exhaustive === true) {
if (haveEveryElements(category.labels.map(lbl => lbl.toLocaleLowerCase()), pr.labels)) {
pullRequests.push(body);
matched = true;
}
}
else {
if (haveCommonElements(category.labels.map(lbl => lbl.toLocaleLowerCase()), pr.labels)) {
pullRequests.push(body);
matched = true;
}
}
}
if (!matched) {
@@ -1264,6 +1272,9 @@ 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;
let transformed = template;
@@ -1432,7 +1443,7 @@ function directoryExistsSync(inputPath, required) {
try {
stats = fs.statSync(inputPath);
}
catch (error) {
catch (error /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
if (error.code === 'ENOENT') {
if (!required) {
return false;
@@ -1460,7 +1471,7 @@ function writeOutput(githubWorkspacePath, outputFile, changelog) {
try {
fs.writeFileSync(outputPath, changelog);
}
catch (error) {
catch (error /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.warning(`⚠️ Could not write the file to disk - ${error.message}`);
}
}
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -31,7 +31,7 @@
"js-yaml": "^4.1.0",
"prettier": "2.3.2",
"ts-jest": "^27.0.5",
"typescript": "^4.3.5"
"typescript": "^4.4.2"
}
},
"node_modules/@actions/core": {
@@ -6616,9 +6616,9 @@
}
},
"node_modules/typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -12049,9 +12049,9 @@
}
},
"typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
"dev": true
},
"unbox-primitive": {
+1 -1
View File
@@ -54,6 +54,6 @@
"js-yaml": "^4.1.0",
"prettier": "2.3.2",
"ts-jest": "^27.0.5",
"typescript": "^4.3.5"
"typescript": "^4.4.2"
}
}
+6 -5
View File
@@ -16,19 +16,20 @@ export interface Configuration {
}
export interface Category {
title: string
labels: string[]
title: string // the title of this category
labels: string[] // labels to associate PRs to this category
exhaustive?: boolean // requires all labels to be present in the PR
}
export interface Transformer {
pattern: string
target?: string
pattern: string // the regex pattern to match
target?: string // the target string to transform the source string using the regex to
flags?: string // the regex flag to use for RegExp
}
export interface Extractor extends Transformer {
on_property?: 'title' | 'author' | 'milestone' | 'body' | undefined // retrieve the property to extract the value from
method?: 'replace' | 'match' | undefined // the method to use to extract the value
method?: 'replace' | 'match' | undefined // the method to use to extract the value, `match` will not use the `target` property
}
export interface TagResolver {
+1 -1
View File
@@ -56,7 +56,7 @@ async function run(): Promise<void> {
core.debug(`Enabled writing the changelog to disk`)
writeOutput(repositoryPath, outputFile, result)
}
} catch (error) {
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.setFailed(error.message)
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ export class PullRequests {
})
return mapPullRequest(data)
} catch (e) {
} catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.warning(
`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`
)
+24 -8
View File
@@ -103,14 +103,26 @@ export function buildChangelog(
let matched = false
for (const [category, pullRequests] of categorized) {
if (
haveCommonElements(
category.labels.map(lbl => lbl.toLocaleLowerCase()),
pr.labels
)
) {
pullRequests.push(body)
matched = true
if (category.exhaustive === true) {
if (
haveEveryElements(
category.labels.map(lbl => lbl.toLocaleLowerCase()),
pr.labels
)
) {
pullRequests.push(body)
matched = true
}
} else {
if (
haveCommonElements(
category.labels.map(lbl => lbl.toLocaleLowerCase()),
pr.labels
)
) {
pullRequests.push(body)
matched = true
}
}
}
@@ -213,6 +225,10 @@ 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 {
let transformed = template
transformed = transformed.replace(/\${{NUMBER}}/g, pr.number.toString())
+2 -2
View File
@@ -96,7 +96,7 @@ export function directoryExistsSync(
let stats: fs.Stats
try {
stats = fs.statSync(inputPath)
} catch (error) {
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
if (error.code === 'ENOENT') {
if (!required) {
return false
@@ -132,7 +132,7 @@ export function writeOutput(
core.debug(`outputPath = '${outputPath}'`)
try {
fs.writeFileSync(outputPath, changelog)
} catch (error) {
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
core.warning(`⚠️ Could not write the file to disk - ${error.message}`)
}
}