Merge pull request #1306 from mikepenz/feature/unify_regex_handling

Unify handling of regex transformers | New method
This commit is contained in:
Mike Penz
2024-03-01 17:54:25 +01:00
committed by GitHub
11 changed files with 368 additions and 138 deletions
+55 -9
View File
@@ -472,12 +472,8 @@ Table of descriptions for the `configuration.json` options to configure the resu
| pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values | | pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values |
| empty_template | Template to pick if no changes are detected. See [Template placeholders](#template-placeholders) for possible values | | empty_template | Template to pick if no changes are detected. See [Template placeholders](#template-placeholders) for possible values |
| label_extractor | An array of `Extractor` specifications, offering a flexible API to extract additinal labels from a PR (Default: `body`, Default in commit mode: `commit message`). | | label_extractor | An array of `Extractor` specifications, offering a flexible API to extract additinal labels from a PR (Default: `body`, Default in commit mode: `commit message`). |
| label_extractor.pattern | A `regex` pattern, extracting values of the change message. | | label_extractor.<REGEX> | Please see the documentation related to `Regex Configuration` for more details. |
| label_extractor.target | The result pattern. The result text will be used as label. If empty, no label is created. (Unused for `match` method) |
| label_extractor.on_property | The property to retrieve the text from. This is optional. Defaults to: `body`. Alternative values: `title`, `author`, `milestone`. | | label_extractor.on_property | The property to retrieve the text from. This is optional. Defaults to: `body`. Alternative values: `title`, `author`, `milestone`. |
| label_extractor.method | The extraction method used. Defaults to: `replace`. Alternative value: `match`. The method specified references the JavaScript String method. |
| label_extractor.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
| label_extractor.on_empty | Defines the placeholder to be filled in, if the regex does not lead to a result. |
| duplicate_filter | Defines the `Extractor` to use for retrieving the identifier for a PR. In case of duplicates will keep the last matching pull request (depends on `sort`). See `label_extractor` for details on `Extractor` properties. | | duplicate_filter | Defines the `Extractor` to use for retrieving the identifier for a PR. In case of duplicates will keep the last matching pull request (depends on `sort`). See `label_extractor` for details on `Extractor` properties. |
| reference | Defines the `Extractor` to use for resolving the "PR-number" for a parent PR. In case of a match, the child PR will not be included in the release notes. See `label_extractor` for details on `Extractor` properties. | | reference | Defines the `Extractor` to use for resolving the "PR-number" for a parent PR. In case of a match, the child PR will not be included in the release notes. See `label_extractor` for details on `Extractor` properties. |
| transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified | | transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified |
@@ -489,11 +485,61 @@ Table of descriptions for the `configuration.json` options to configure the resu
| exclude_merge_branches | An array of branches to be ignored from processing as merge commits | | exclude_merge_branches | An array of branches to be ignored from processing as merge commits |
| tag_resolver | Section to provide configuration for the tag resolving logic. Used if no `fromTag` is provided | | tag_resolver | Section to provide configuration for the tag resolving logic. Used if no `fromTag` is provided |
| tag_resolver.method | Defines the method to use. Current options are: `semver`, `sort`. Default: `semver` | | tag_resolver.method | Defines the method to use. Current options are: `semver`, `sort`. Default: `semver` |
| tag_resolver.filter | Defines a regex which is used to filter out tags not matching. | | tag_resolver.filter | Defines a regex object which is used to filter out tags not matching. |
| tag_resolver.transformer | Defines a regex transformer used to optionally transform the tag after the filter was applied. Allows to adjust the format to e.g. semver. | | tag_resolver.transformer | Defines a regex transformer used to optionally transform the tag after the filter was applied. Allows to adjust the format to e.g. semver. |
| base_branches | The target branches for the merged PR, ingnores PRs with different target branch. Values can be a `regex`. Default: allow all base branches | | base_branches | The target branches for the merged PR, ingnores PRs with different target branch. Values can be a `regex`. Default: allow all base branches |
| trim_values | Defines if all values inserted in templates are `trimmed`. Default: false | | trim_values | Defines if all values inserted in templates are `trimmed`. Default: false |
### Custom placeholders 🧪
### Regex Configuration
Since v5.x or newer, the regex configuration was unified to allow the same functionalities to be used for the various usecases.
This applies to all configurations outlined in `Configuration Specification` and `Custom placeholders` that allow a regex object.
| **Input** | **Description** |
|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <parent>.pattern | The `regex` pattern to use |
| <parent>.target | The result pattern. The result text will be used as label. If empty, no label is created. (Usage depends on the `method` used for the regex) |
| <parent>.method | The extraction method used. Defaults to: `replace`. Alternative values: `replaceAll`, `match`. These methods specified references the JavaScript String method. And a special method `regexr`, that functions similar to the `list` within the regexr tool. |
| <parent>.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
| <parent>.on_empty | Defines the placeholder to be filled in, if the regex does not lead to a result. |
Example regex configuration block (Sample extracts a ticket number from the title)
PR title input
```
[XYZ-1234] This is my PR title
```
Regex replace pattern
```
{
"name": "TICKET",
"source": "TITLE",
"transformer": {
"pattern": "\\s*\\[([A-Z].{2,4}-.{2,5})\\][\\S\\s]*",
"target": ", [$1](https://corp.ticket-system.com/browse/$1)"
}
}
```
Regex replace pattern
```
{
"name": "TICKET",
"source": "TITLE",
"transformer": {
"pattern": "\\[([A-Z]{2,4}-.{2,5})\\]",
"method": "regexr",
"target": '- [$1](https://corp.ticket-system.com/browse/$1)'
}
}
```
> [!WARNING]
> Usages of `\` in the json have to be escaped. E.g. `\` becomes `\\`.
### Custom placeholders
Starting with v3.2.0 the action provides a feature of defining `CUSTOM_PLACEHOLDERS`. Starting with v3.2.0 the action provides a feature of defining `CUSTOM_PLACEHOLDERS`.
@@ -524,11 +570,11 @@ Custom placeholders can be defined via the `configuration.json` as `custom_place
This example will look for JIRA tickets in the EPIC project, and extract all of these tickets. The exciting part for that case is, that the ticket is PR bound, but can be used in the global TEMPLATE, but equally also in the PR template. This is unique for CUSTOM PLACEHOLDERS as standard palceholders do not offer this functionality. This example will look for JIRA tickets in the EPIC project, and extract all of these tickets. The exciting part for that case is, that the ticket is PR bound, but can be used in the global TEMPLATE, but equally also in the PR template. This is unique for CUSTOM PLACEHOLDERS as standard palceholders do not offer this functionality.
| **Input** | **Description** | | **Input** | **Description** |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------| |-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| custom_placeholders | An array of `Placeholder` specifications, offering a flexible API to extract custom placeholders from existing placeholders. | | custom_placeholders | An array of `Placeholder` specifications, offering a flexible API to extract custom placeholders from existing placeholders. |
| custom_placeholders.name | The name of the custom placeholder. Will be used within the template. | | custom_placeholders.name | The name of the custom placeholder. Will be used within the template. |
| custom_placeholders.source | The source PLACEHOLDER, requires to be one of the existing Template or PR Template placeholders. | | custom_placeholders.source | The source PLACEHOLDER, requires to be one of the existing Template or PR Template placeholders. |
| custom_placeholders.transformer | The transformer specification used to extract the value from the original source PLACEHOLDER. | | custom_placeholders.transformer.<REGEX> | The transformer specification used to extract the value from the original source PLACEHOLDER. |
A placeholder with the name as `CUSTOM_PLACEHOLDER` can be used as `#{{CUSTOM_PLACEHOLDER}}` in the target template. A placeholder with the name as `CUSTOM_PLACEHOLDER` can be used as `#{{CUSTOM_PLACEHOLDER}}` in the target template.
By default the same restriction applies as for PR vs template placeholder. E.g. a global placeholder can only be used in the global template (and not in the PR template). By default the same restriction applies as for PR vs template placeholder. E.g. a global placeholder can only be used in the global template (and not in the PR template).
+57
View File
@@ -0,0 +1,57 @@
import { transformStringToValue, validateRegex } from '../src/pr-collector/regexUtils'
import { Regex } from '../src/pr-collector/types'
jest.setTimeout(180000)
it('Replace into target', async () => {
const regex: Regex = {
pattern: '.*(\\[Feature\\]|\\[Issue\\]).*',
target: '$1',
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue("[Feature] TEST", validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Replace all into target', async () => {
const regex: Regex = {
pattern: '.*(\\[Feature\\]|\\[Issue\\]).*',
method: 'replaceAll',
target: '$1',
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue("[Feature] TEST", validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Match without target', async () => {
const regex: Regex = {
pattern: '\\[Feature\\]|\\[Issue\\]',
method: 'match'
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue("[Feature] TEST", validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Match into target', async () => {
const regex: Regex = {
pattern: '(?<label>\\[Feature\\]|\\[Issue\\])',
method: 'match',
target: '$1',
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue("[Feature] TEST", validatedRegex!!)).toStrictEqual(`[Feature]`)
})
it('Match into named group', async () => {
const regex: Regex = {
pattern: '(?<label>\\[Feature\\]|\\[Issue\\])',
method: 'match',
target: 'label',
}
const validatedRegex = validateRegex(regex)
expect(validateRegex).not.toBeNull()
expect(transformStringToValue("[Feature] TEST", validatedRegex!!)).toStrictEqual(`[Feature]`)
})
+12 -7
View File
@@ -1,4 +1,5 @@
import { validateTransformer } from '../src/pr-collector/regexUtils' import { TagResolver } from '../src/configuration'
import { validateRegex } from '../src/pr-collector/regexUtils'
import {filterTags, prepareAndSortTags, TagInfo, transformTags} from '../src/pr-collector/tags' import {filterTags, prepareAndSortTags, TagInfo, transformTags} from '../src/pr-collector/tags'
jest.setTimeout(180000) jest.setTimeout(180000)
@@ -100,14 +101,16 @@ it('Should filter tags correctly using the regex', async () => {
{name: '20.0.2', commit: ''} {name: '20.0.2', commit: ''}
] ]
const tagResolver = { const tagResolver: TagResolver = {
method: 'non-existing-method', method: 'non-existing-method',
filter: { filter: {
pattern: 'api-(.+)', pattern: 'api-(.+)',
method: 'match',
flags: 'gu' flags: 'gu'
} }
} }
const filtered = filterTags(tags, tagResolver) const filter = validateRegex(tagResolver.filter)
const filtered = filterTags(tags, filter)
.map(function (tag) { .map(function (tag) {
return tag.name return tag.name
}) })
@@ -131,14 +134,16 @@ it('Should filter tags correctly using the regex (inverse)', async () => {
{name: '20.0.2', commit: ''} {name: '20.0.2', commit: ''}
] ]
const tagResolver = { const tagResolver: TagResolver = {
method: 'non-existing-method', method: 'non-existing-method',
filter: { filter: {
pattern: '^(?!\\w+-)(.+)', pattern: '^(?!\\w+-)(.+)',
method: 'match',
flags: 'gu' flags: 'gu'
} }
} }
const filtered = filterTags(tags, tagResolver) const filter = validateRegex(tagResolver.filter)
const filtered = filterTags(tags, filter)
.map(function (tag) { .map(function (tag) {
return tag.name return tag.name
}) })
@@ -160,7 +165,7 @@ it('Should transform tags correctly using the regex', async () => {
{name: '20.0.2', commit: ''} {name: '20.0.2', commit: ''}
] ]
const tagResolver = { const tagResolver: TagResolver = {
method: 'non-existing-method', method: 'non-existing-method',
transformer: { transformer: {
pattern: '(api\-)?(.+)', pattern: '(api\-)?(.+)',
@@ -168,7 +173,7 @@ it('Should transform tags correctly using the regex', async () => {
} }
} }
const transformer = validateTransformer(tagResolver.transformer) const transformer = validateRegex(tagResolver.transformer)
if(transformer != null) { if(transformer != null) {
const transformed = transformTags(tags, transformer) const transformed = transformTags(tags, transformer)
.map(function (tag) { .map(function (tag) {
Generated Vendored
+111 -49
View File
@@ -922,27 +922,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.buildRegex = exports.validateTransformer = void 0; exports.transformStringToValue = exports.transformStringToOptionalValue = exports.transformStringToValues = exports.buildRegex = exports.validateRegex = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
function validateTransformer(transformer) { function validateRegex(regex) {
if (transformer === undefined) { if (regex === undefined) {
return null; return null;
} }
try { try {
let target = undefined; const target = regex.target;
if (transformer.hasOwnProperty('target')) { const method = regex.method;
target = transformer.target; const onEmpty = regex.on_empty;
}
let onProperty = undefined; let onProperty = undefined;
let method = undefined; if (regex.hasOwnProperty('on_property')) {
let onEmpty = undefined; onProperty = regex.on_property;
if (transformer.hasOwnProperty('method')) {
method = transformer.method;
onEmpty = transformer.on_empty;
onProperty = transformer.on_property;
}
else if (transformer.hasOwnProperty('on_property')) {
onProperty = transformer.on_property;
} }
// legacy handling, transform single value input to array // legacy handling, transform single value input to array
if (!Array.isArray(onProperty)) { if (!Array.isArray(onProperty)) {
@@ -950,14 +942,14 @@ function validateTransformer(transformer) {
onProperty = [onProperty]; onProperty = [onProperty];
} }
} }
return buildRegex(transformer, target, onProperty, method, onEmpty); return buildRegex(regex, target, onProperty, method, onEmpty);
} }
catch (e) { catch (e) {
core.warning(`⚠️ Failed to validate transformer: ${transformer.pattern}`); core.warning(`⚠️ Failed to validate transformer: ${regex.pattern}`);
return null; return null;
} }
} }
exports.validateTransformer = validateTransformer; exports.validateRegex = validateRegex;
/** /**
* Constructs the RegExp, providing the configured Regex and additional values * Constructs the RegExp, providing the configured Regex and additional values
*/ */
@@ -978,6 +970,86 @@ function buildRegex(regex, target, onProperty, method, onEmpty) {
} }
} }
exports.buildRegex = buildRegex; exports.buildRegex = buildRegex;
function transformStringToValues(value, extractor) {
if (extractor.pattern == null) {
return null;
}
if (extractor.method === 'regexr') {
const matches = transformRegexr(extractor.pattern, value, extractor.target);
if (matches !== null && matches.size > 0) {
return [...matches];
}
}
else if (extractor.method === 'match') {
const matches = value.match(extractor.pattern);
if (matches !== null && matches.length > 0) {
return matches.map(match => match || '');
}
}
else if (extractor.method === 'replaceAll') {
const match = value.replaceAll(extractor.pattern, extractor.target);
if (match !== '') {
return [match];
}
}
else {
const match = value.replace(extractor.pattern, extractor.target);
if (match !== '') {
return [match];
}
}
if (extractor.onEmpty !== undefined) {
return [extractor.onEmpty];
}
return null;
}
exports.transformStringToValues = transformStringToValues;
function transformStringToOptionalValue(value, extractor) {
const result = transformStringToValues(value, extractor);
if (result != null && result.length > 0) {
return result[0];
}
else {
return null;
}
}
exports.transformStringToOptionalValue = transformStringToOptionalValue;
function transformStringToValue(value, extractor) {
return transformStringToOptionalValue(value, extractor) || '';
}
exports.transformStringToValue = transformStringToValue;
function transformRegexr(regex, source, target) {
/**
* Util funtion extracted from regexr and is licensed under:
*
* RegExr: Learn, Build, & Test RegEx
* Copyright (C) 2017 gskinner.com, inc.
* https://github.com/gskinner/regexr/blob/master/dev/src/helpers/BrowserSolver.js#L111-L136
*/
let repl;
let ref;
if (target.search(/\$[&1-9`']/) === -1) {
target = `$&${target}`;
}
const firstOnly = true; // for now we don't support multi matches for PRs, future improvement
const adaptedRegex = new RegExp(regex.source, regex.flags.replace('g', ''));
const result = new Set();
do {
ref = source.replace(adaptedRegex, '\b'); // bell char - just a placeholder to find
const index = ref.indexOf('\b');
const empty = ref.length > source.length;
if (index === -1) {
break;
}
repl = source.replace(adaptedRegex, target);
result.add(repl.substr(index, repl.length - ref.length + 1));
source = ref.substr(index + (empty ? 2 : 1));
if (firstOnly) {
break;
}
} while (source.length);
return result;
}
/***/ }), /***/ }),
@@ -1084,10 +1156,11 @@ class Tags {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let tags = []; let tags = [];
if (!toTag || !fromTag) { if (!toTag || !fromTag) {
const filterRegex = (0, regexUtils_1.validateRegex)(tagResolver.filter);
// filter out tags not matching the specified filter // filter out tags not matching the specified filter
const filteredTags = filterTags( const filteredTags = filterTags(
// retrieve the tags from the API // retrieve the tags from the API
yield this.getTags(owner, repo, maxTagsToFetch), tagResolver); yield this.getTags(owner, repo, maxTagsToFetch), filterRegex);
// check if a transformer, legacy handling, transform single value input to array // check if a transformer, legacy handling, transform single value input to array
let tagTransfomers = undefined; let tagTransfomers = undefined;
if (tagResolver.transformer !== undefined) { if (tagResolver.transformer !== undefined) {
@@ -1102,7 +1175,7 @@ class Tags {
let transformedTags = filteredTags; let transformedTags = filteredTags;
if (tagTransfomers !== undefined && tagTransfomers.length > 0) { if (tagTransfomers !== undefined && tagTransfomers.length > 0) {
for (const transformer of tagTransfomers) { for (const transformer of tagTransfomers) {
const tagTransformer = (0, regexUtils_1.validateTransformer)(transformer); const tagTransformer = (0, regexUtils_1.validateRegex)(transformer);
if (tagTransformer != null) { if (tagTransformer != null) {
core.debug(`️ Using configured tagTransformer (${transformer.pattern})`); core.debug(`️ Using configured tagTransformer (${transformer.pattern})`);
transformedTags = transformTags(transformedTags, tagTransformer); transformedTags = transformTags(transformedTags, tagTransformer);
@@ -1186,12 +1259,9 @@ exports.Tags = 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
*/ */
function filterTags(tags, tagResolver) { function filterTags(tags, filterRegex) {
var _a; if (filterRegex !== null) {
const filter = tagResolver.filter; const filteredTags = tags.filter(tag => (0, regexUtils_1.transformStringToOptionalValue)(tag.name, filterRegex) !== null);
if (filter !== undefined) {
const regex = new RegExp(filter.pattern.replace('\\\\', '\\'), (_a = filter.flags) !== null && _a !== void 0 ? _a : 'gu');
const filteredTags = tags.filter(tag => tag.name.match(regex) !== null);
core.debug(`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`); core.debug(`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`);
return filteredTags; return filteredTags;
} }
@@ -1206,7 +1276,7 @@ exports.filterTags = filterTags;
function transformTags(tags, transformer) { function transformTags(tags, transformer) {
return tags.map(function (tag) { return tags.map(function (tag) {
if (transformer.pattern) { if (transformer.pattern) {
const transformedName = tag.name.replace(transformer.pattern, transformer.target); const transformedName = (0, regexUtils_1.transformStringToValue)(tag.name, transformer);
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
@@ -1414,7 +1484,7 @@ const regexUtils_1 = __nccwpck_require__(5351);
* Checks if any of the rules match the given PR * Checks if any of the rules match the given PR
*/ */
function matchesRules(rules, pr, exhaustive) { function matchesRules(rules, pr, exhaustive) {
const transformers = rules.map(rule => (0, regexUtils_1.validateTransformer)(rule)).filter(t => t !== null); const transformers = rules.map(rule => (0, regexUtils_1.validateRegex)(rule)).filter(t => t !== null);
if (exhaustive) { if (exhaustive) {
return transformers.every(transformer => { return transformers.every(transformer => {
return matches(pr, transformer, 'rule'); return matches(pr, transformer, 'rule');
@@ -2453,7 +2523,7 @@ function buildChangelog(diffInfo, origPrs, options) {
core.info(`️ Sorted all pull requests ascending: ${JSON.stringify(sort)}`); core.info(`️ Sorted all pull requests ascending: ${JSON.stringify(sort)}`);
// establish parent child PR relations // establish parent child PR relations
if (config.reference !== undefined) { if (config.reference !== undefined) {
const reference = (0, regexUtils_1.validateTransformer)(config.reference); const reference = (0, regexUtils_1.validateRegex)(config.reference);
if (reference !== null) { if (reference !== null) {
core.info(`️ Identifying PR references using \`reference\``); core.info(`️ Identifying PR references using \`reference\``);
const mapped = new Map(); const mapped = new Map();
@@ -2491,7 +2561,7 @@ function buildChangelog(diffInfo, origPrs, options) {
} }
// drop duplicate pull requests // drop duplicate pull requests
if (config.duplicate_filter !== undefined) { if (config.duplicate_filter !== undefined) {
const extractor = (0, regexUtils_1.validateTransformer)(config.duplicate_filter); const extractor = (0, regexUtils_1.validateRegex)(config.duplicate_filter);
if (extractor !== null) { if (extractor !== null) {
core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``); core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``);
const deduplicatedMap = new Map(); const deduplicatedMap = new Map();
@@ -2804,17 +2874,18 @@ function handlePlaceholder(template, key, value, placeholders /* placeholders to
const phs = placeholders.get(key); const phs = placeholders.get(key);
if (phs) { if (phs) {
for (const placeholder of phs) { for (const placeholder of phs) {
const transformer = (0, regexUtils_1.validateTransformer)(placeholder.transformer); const transformer = (0, regexUtils_1.validateRegex)(placeholder.transformer);
if (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) { if (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) {
const extractedValue = value.replace(transformer.pattern, transformer.target); const extractedValue = (0, regexUtils_1.transformStringToOptionalValue)(value, transformer);
// note: `.replace` will return the full string again if there was no match // note: `.replace` will return the full string again if there was no match
if (extractedValue && (extractedValue !== value || (extractedValue === value && value.match(transformer.pattern)))) { // note: This is mostly backwards compatiblity
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
if (placeholderPrMap) { if (placeholderPrMap) {
(0, utils_1.createOrSet)(placeholderPrMap, placeholder.name, extractedValue); (0, utils_1.createOrSet)(placeholderPrMap, placeholder.name, extractedValue);
} }
transformed = transformed.replaceAll(`#{{${placeholder.name}}}`, configuration.trim_values ? extractedValue.trim() : extractedValue); transformed = transformed.replaceAll(`#{{${placeholder.name}}}`, configuration.trim_values ? extractedValue.trim() : extractedValue);
if (core.isDebug()) { if (core.isDebug()) {
core.debug(` Custom Placeholder successfully matched data - ${extractValues} (${placeholder.name})`); core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`);
} }
} }
else if (core.isDebug() && extractedValue === value) { else if (core.isDebug() && extractedValue === value) {
@@ -2899,7 +2970,7 @@ function validateTransformers(specifiedTransformers) {
const transformers = specifiedTransformers; const transformers = specifiedTransformers;
return transformers return transformers
.map(transformer => { .map(transformer => {
return (0, regexUtils_1.validateTransformer)(transformer); return (0, regexUtils_1.validateRegex)(transformer);
}) })
.filter(transformer => (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) != null) .filter(transformer => (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) != null)
.map(transformer => { .map(transformer => {
@@ -2932,23 +3003,14 @@ function extractValuesFromString(value, extractor) {
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null; return null;
} }
if (extractor.method === 'match') { const transformed = (0, regexUtils_1.transformStringToValues)(value, extractor);
const lables = value.match(extractor.pattern); if (transformed) {
if (lables !== null && lables.length > 0) { return transformed.map(val => (val === null || val === void 0 ? void 0 : val.toLocaleLowerCase('en')) || '');
return lables.map(label => (label === null || label === void 0 ? void 0 : label.toLocaleLowerCase('en')) || '');
}
} }
else { else {
const label = value.replace(extractor.pattern, extractor.target);
if (label !== '') {
return [label.toLocaleLowerCase('en')];
}
}
if (extractor.onEmpty !== undefined) {
return [extractor.onEmpty.toLocaleLowerCase('en')];
}
return null; return null;
} }
}
/***/ }), /***/ }),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1,4 +1,4 @@
import {Extractor, PullConfiguration, Regex, Rule, Sort, Transformer} from './pr-collector/types' import {Extractor, PullConfiguration, Regex, Rule, Sort} from './pr-collector/types'
export interface Configuration extends PullConfiguration { export interface Configuration extends PullConfiguration {
max_tags_to_fetch: number max_tags_to_fetch: number
@@ -14,7 +14,7 @@ export interface Configuration extends PullConfiguration {
label_extractor: Extractor[] label_extractor: Extractor[]
duplicate_filter?: Extractor // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`) duplicate_filter?: Extractor // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`)
reference?: Extractor // extracts a reference from a PR, used to establish parent child relations. This will remove the child from the main PR list. reference?: Extractor // extracts a reference from a PR, used to establish parent child relations. This will remove the child from the main PR list.
transformers: Transformer[] transformers: Regex[]
tag_resolver: TagResolver tag_resolver: TagResolver
base_branches: string[] base_branches: string[]
custom_placeholders?: Placeholder[] custom_placeholders?: Placeholder[]
@@ -51,13 +51,13 @@ export type Property =
export interface TagResolver { export interface TagResolver {
method: string // semver, sort method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting filter?: Regex // the regex to filter the tags, prior to sorting
transformer?: Transformer // transforms the tag name using the regex, run after the filter transformer?: Regex // transforms the tag name using the regex, run after the filter
} }
export interface Placeholder { export interface Placeholder {
name: string // the name of the new placeholder name: string // the name of the new placeholder
source: string // the src placeholder which will be used to apply the transformer on source: string // the src placeholder which will be used to apply the transformer on
transformer: Transformer // the transformer to use to transform the original placeholder into the custom placheolder transformer: Regex // the transformer to use to transform the original placeholder into the custom placheolder
} }
export const DefaultConfiguration: Configuration = { export const DefaultConfiguration: Configuration = {
+91 -19
View File
@@ -1,25 +1,17 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Extractor, Property, Regex, RegexTransformer, Transformer} from './types' import {Extractor, Property, Regex, RegexTransformer} from './types'
export function validateTransformer(transformer?: Regex): RegexTransformer | null { export function validateRegex(regex?: Regex): RegexTransformer | null {
if (transformer === undefined) { if (regex === undefined) {
return null return null
} }
try { try {
let target = undefined const target = regex.target
if (transformer.hasOwnProperty('target')) { const method = regex.method
target = (transformer as Transformer).target const onEmpty = regex.on_empty
}
let onProperty = undefined let onProperty = undefined
let method = undefined if (regex.hasOwnProperty('on_property')) {
let onEmpty = undefined onProperty = (regex as Extractor).on_property
if (transformer.hasOwnProperty('method')) {
method = (transformer as Extractor).method
onEmpty = (transformer as Extractor).on_empty
onProperty = (transformer as Extractor).on_property
} else if (transformer.hasOwnProperty('on_property')) {
onProperty = (transformer as Extractor).on_property
} }
// legacy handling, transform single value input to array // legacy handling, transform single value input to array
if (!Array.isArray(onProperty)) { if (!Array.isArray(onProperty)) {
@@ -28,9 +20,9 @@ export function validateTransformer(transformer?: Regex): RegexTransformer | nul
} }
} }
return buildRegex(transformer, target, onProperty, method, onEmpty) return buildRegex(regex, target, onProperty, method, onEmpty)
} catch (e) { } catch (e) {
core.warning(`⚠️ Failed to validate transformer: ${transformer.pattern}`) core.warning(`⚠️ Failed to validate transformer: ${regex.pattern}`)
return null return null
} }
} }
@@ -42,7 +34,7 @@ export function buildRegex(
regex: Regex, regex: Regex,
target: string | undefined, target: string | undefined,
onProperty?: Property[] | undefined, onProperty?: Property[] | undefined,
method?: 'replace' | 'match' | undefined, method?: 'replace' | 'replaceAll' | 'match' | 'regexr' | undefined,
onEmpty?: string | undefined onEmpty?: string | undefined
): RegexTransformer | null { ): RegexTransformer | null {
try { try {
@@ -58,3 +50,83 @@ export function buildRegex(
return null return null
} }
} }
export function transformStringToValues(value: string, extractor: RegexTransformer): string[] | null {
if (extractor.pattern == null) {
return null
}
if (extractor.method === 'regexr') {
const matches = transformRegexr(extractor.pattern, value, extractor.target)
if (matches !== null && matches.size > 0) {
return [...matches]
}
} else if (extractor.method === 'match') {
const matches = value.match(extractor.pattern)
if (matches !== null && matches.length > 0) {
return matches.map(match => match || '')
}
} else if (extractor.method === 'replaceAll') {
const match = value.replaceAll(extractor.pattern, extractor.target)
if (match !== '') {
return [match]
}
} else {
const match = value.replace(extractor.pattern, extractor.target)
if (match !== '') {
return [match]
}
}
if (extractor.onEmpty !== undefined) {
return [extractor.onEmpty]
}
return null
}
export function transformStringToOptionalValue(value: string, extractor: RegexTransformer): string | null {
const result = transformStringToValues(value, extractor)
if (result != null && result.length > 0) {
return result[0]
} else {
return null
}
}
export function transformStringToValue(value: string, extractor: RegexTransformer): string {
return transformStringToOptionalValue(value, extractor) || ''
}
function transformRegexr(regex: RegExp, source: string, target: string): Set<string> | null {
/**
* Util funtion extracted from regexr and is licensed under:
*
* RegExr: Learn, Build, & Test RegEx
* Copyright (C) 2017 gskinner.com, inc.
* https://github.com/gskinner/regexr/blob/master/dev/src/helpers/BrowserSolver.js#L111-L136
*/
let repl
let ref
if (target.search(/\$[&1-9`']/) === -1) {
target = `$&${target}`
}
const firstOnly = true // for now we don't support multi matches for PRs, future improvement
const adaptedRegex = new RegExp(regex.source, regex.flags.replace('g', ''))
const result = new Set<string>()
do {
ref = source.replace(adaptedRegex, '\b') // bell char - just a placeholder to find
const index = ref.indexOf('\b')
const empty = ref.length > source.length
if (index === -1) {
break
}
repl = source.replace(adaptedRegex, target)
result.add(repl.substr(index, repl.length - ref.length + 1))
source = ref.substr(index + (empty ? 2 : 1))
if (firstOnly) {
break
}
} while (source.length)
return result
}
+11 -11
View File
@@ -2,10 +2,10 @@ import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as semver from 'semver' import * as semver from 'semver'
import {SemVer} from 'semver' import {SemVer} from 'semver'
import {RegexTransformer, TagResolver, Transformer} from './types' import {Regex, RegexTransformer, TagResolver} from './types'
import {createCommandManager} from './gitHelper' import {createCommandManager} from './gitHelper'
import moment from 'moment' import moment from 'moment'
import {validateTransformer} from './regexUtils' import {transformStringToOptionalValue, transformStringToValue, validateRegex} from './regexUtils'
import {BaseRepository} from '../repositories/BaseRepository' import {BaseRepository} from '../repositories/BaseRepository'
export interface TagResult { export interface TagResult {
@@ -88,15 +88,17 @@ export class Tags {
let tags: TagInfo[] = [] let tags: TagInfo[] = []
if (!toTag || !fromTag) { if (!toTag || !fromTag) {
const filterRegex = validateRegex(tagResolver.filter)
// filter out tags not matching the specified filter // filter out tags not matching the specified filter
const filteredTags = filterTags( const filteredTags = filterTags(
// retrieve the tags from the API // retrieve the tags from the API
await this.getTags(owner, repo, maxTagsToFetch), await this.getTags(owner, repo, maxTagsToFetch),
tagResolver filterRegex
) )
// check if a transformer, legacy handling, transform single value input to array // check if a transformer, legacy handling, transform single value input to array
let tagTransfomers: Transformer[] | undefined = undefined let tagTransfomers: Regex[] | undefined = undefined
if (tagResolver.transformer !== undefined) { if (tagResolver.transformer !== undefined) {
if (!Array.isArray(tagResolver.transformer)) { if (!Array.isArray(tagResolver.transformer)) {
tagTransfomers = [tagResolver.transformer] tagTransfomers = [tagResolver.transformer]
@@ -109,7 +111,7 @@ export class Tags {
let transformedTags: TagInfo[] = filteredTags let transformedTags: TagInfo[] = filteredTags
if (tagTransfomers !== undefined && tagTransfomers.length > 0) { if (tagTransfomers !== undefined && tagTransfomers.length > 0) {
for (const transformer of tagTransfomers) { for (const transformer of tagTransfomers) {
const tagTransformer = validateTransformer(transformer) const tagTransformer = validateRegex(transformer)
if (tagTransformer != null) { if (tagTransformer != null) {
core.debug(`️ Using configured tagTransformer (${transformer.pattern})`) core.debug(`️ Using configured tagTransformer (${transformer.pattern})`)
transformedTags = transformTags(transformedTags, tagTransformer) transformedTags = transformTags(transformedTags, tagTransformer)
@@ -196,11 +198,9 @@ export class Tags {
* Uses the provided filter (if available) to filter out any tags not currently relevant. * 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(tags: TagInfo[], tagResolver: TagResolver): TagInfo[] { export function filterTags(tags: TagInfo[], filterRegex: RegexTransformer | null): TagInfo[] {
const filter = tagResolver.filter if (filterRegex !== null) {
if (filter !== undefined) { const filteredTags = tags.filter(tag => transformStringToOptionalValue(tag.name, filterRegex) !== null)
const regex = new RegExp(filter.pattern.replace('\\\\', '\\'), filter.flags ?? 'gu')
const filteredTags = tags.filter(tag => tag.name.match(regex) !== null)
core.debug(`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`) core.debug(`️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`)
return filteredTags return filteredTags
} else { } else {
@@ -214,7 +214,7 @@ export function filterTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[]
export function transformTags(tags: TagInfo[], transformer: RegexTransformer): TagInfo[] { export function transformTags(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(transformer.pattern, transformer.target) const transformedName = transformStringToValue(tag.name, transformer)
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
+5 -8
View File
@@ -36,28 +36,25 @@ export interface Sort {
export interface TagResolver { export interface TagResolver {
method: string // semver, sort method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting filter?: Regex // the regex to filter the tags, prior to sorting
transformer?: Transformer | Transformer[] // transforms the tag name using the regex, run after the filter transformer?: Regex | Regex[] // transforms the tag name using the regex, run after the filter
} }
export interface Regex { export interface Regex {
pattern: string // the regex pattern to match pattern: string // the regex pattern to match
flags?: string // the regex flag to use for RegExp flags?: string // the regex flag to use for RegExp
}
export interface Transformer extends Regex {
target?: string // the target string to transform the source string using the regex to target?: string // the target string to transform the source string using the regex to
method?: 'replace' | 'replaceAll' | 'match' | 'regexr' | undefined // the method to use to extract the value, `match` will not use the `target` property
on_empty?: string | undefined // in case the regex results in an empty string, this value is gonna be used instead (only for label_extractor currently)
} }
export interface Extractor extends Transformer { export interface Extractor extends Regex {
on_property?: Property[] | Property | undefined // retrieve the property to extract the value from on_property?: Property[] | Property | undefined // retrieve the property to extract the value from
method?: 'replace' | 'match' | undefined // the method to use to extract the value, `match` will not use the `target` property
on_empty?: string | undefined // in case the regex results in an empty string, this value is gonna be used instead (only for label_extractor currently)
} }
export interface RegexTransformer { export interface RegexTransformer {
pattern: RegExp | null pattern: RegExp | null
target: string target: string
onProperty?: Property[] onProperty?: Property[]
method?: 'replace' | 'match' method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
onEmpty?: string onEmpty?: string
} }
+2 -2
View File
@@ -1,13 +1,13 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {RegexTransformer, Rule} from './pr-collector/types' import {RegexTransformer, Rule} from './pr-collector/types'
import {PullRequestInfo, retrieveProperty} from './pr-collector/pullRequests' import {PullRequestInfo, retrieveProperty} from './pr-collector/pullRequests'
import {validateTransformer} from './pr-collector/regexUtils' import {validateRegex} from './pr-collector/regexUtils'
/** /**
* Checks if any of the rules match the given PR * Checks if any of the rules match the given PR
*/ */
export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: Boolean): boolean { export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: Boolean): boolean {
const transformers: RegexTransformer[] = rules.map(rule => validateTransformer(rule)).filter(t => t !== null) as RegexTransformer[] const transformers: RegexTransformer[] = rules.map(rule => validateRegex(rule)).filter(t => t !== null) as RegexTransformer[]
if (exhaustive) { if (exhaustive) {
return transformers.every(transformer => { return transformers.every(transformer => {
return matches(pr, transformer, 'rule') return matches(pr, transformer, 'rule')
+15 -24
View File
@@ -10,8 +10,8 @@ import {
sortPullRequests sortPullRequests
} from './pr-collector/pullRequests' } from './pr-collector/pullRequests'
import {DiffInfo} from './pr-collector/commits' import {DiffInfo} from './pr-collector/commits'
import {validateTransformer} from './pr-collector/regexUtils' import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils'
import {RegexTransformer, Transformer} from './pr-collector/types' import {Regex, RegexTransformer} from './pr-collector/types'
import {ReleaseNotesOptions} from './releaseNotesBuilder' import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {matchesRules} from './regexUtils' import {matchesRules} from './regexUtils'
@@ -40,7 +40,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
// establish parent child PR relations // establish parent child PR relations
if (config.reference !== undefined) { if (config.reference !== undefined) {
const reference = validateTransformer(config.reference) const reference = validateRegex(config.reference)
if (reference !== null) { if (reference !== null) {
core.info(`️ Identifying PR references using \`reference\``) core.info(`️ Identifying PR references using \`reference\``)
@@ -77,7 +77,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
// drop duplicate pull requests // drop duplicate pull requests
if (config.duplicate_filter !== undefined) { if (config.duplicate_filter !== undefined) {
const extractor = validateTransformer(config.duplicate_filter) const extractor = validateRegex(config.duplicate_filter)
if (extractor !== null) { if (extractor !== null) {
core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``) core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``)
@@ -461,11 +461,12 @@ function handlePlaceholder(
const phs = placeholders.get(key) const phs = placeholders.get(key)
if (phs) { if (phs) {
for (const placeholder of phs) { for (const placeholder of phs) {
const transformer = validateTransformer(placeholder.transformer) const transformer = validateRegex(placeholder.transformer)
if (transformer?.pattern) { if (transformer?.pattern) {
const extractedValue = value.replace(transformer.pattern, transformer.target) const extractedValue = transformStringToOptionalValue(value, transformer)
// note: `.replace` will return the full string again if there was no match // note: `.replace` will return the full string again if there was no match
if (extractedValue && (extractedValue !== value || (extractedValue === value && value.match(transformer.pattern)))) { // note: This is mostly backwards compatiblity
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
if (placeholderPrMap) { if (placeholderPrMap) {
createOrSet(placeholderPrMap, placeholder.name, extractedValue) createOrSet(placeholderPrMap, placeholder.name, extractedValue)
} }
@@ -475,7 +476,7 @@ function handlePlaceholder(
) )
if (core.isDebug()) { if (core.isDebug()) {
core.debug(` Custom Placeholder successfully matched data - ${extractValues} (${placeholder.name})`) core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`)
} }
} else if (core.isDebug() && extractedValue === value) { } else if (core.isDebug() && extractedValue === value) {
core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`) core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`)
@@ -580,11 +581,11 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
return transformed return transformed
} }
function validateTransformers(specifiedTransformers: Transformer[]): RegexTransformer[] { function validateTransformers(specifiedTransformers: Regex[]): RegexTransformer[] {
const transformers = specifiedTransformers const transformers = specifiedTransformers
return transformers return transformers
.map(transformer => { .map(transformer => {
return validateTransformer(transformer) return validateRegex(transformer)
}) })
.filter(transformer => transformer?.pattern != null) .filter(transformer => transformer?.pattern != null)
.map(transformer => { .map(transformer => {
@@ -619,20 +620,10 @@ function extractValuesFromString(value: string, extractor: RegexTransformer): st
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null return null
} }
const transformed = transformStringToValues(value, extractor)
if (extractor.method === 'match') { if (transformed) {
const lables = value.match(extractor.pattern) return transformed.map(val => val?.toLocaleLowerCase('en') || '')
if (lables !== null && lables.length > 0) {
return lables.map(label => label?.toLocaleLowerCase('en') || '')
}
} else { } else {
const label = value.replace(extractor.pattern, extractor.target)
if (label !== '') {
return [label.toLocaleLowerCase('en')]
}
}
if (extractor.onEmpty !== undefined) {
return [extractor.onEmpty.toLocaleLowerCase('en')]
}
return null return null
} }
}