Merge pull request #1306 from mikepenz/feature/unify_regex_handling
Unify handling of regex transformers | New method
This commit is contained in:
@@ -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 |
|
||||
| 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.pattern | A `regex` pattern, extracting values of the change message. |
|
||||
| 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.<REGEX> | Please see the documentation related to `Regex Configuration` for more details. |
|
||||
| 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. |
|
||||
| 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 |
|
||||
@@ -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 |
|
||||
| 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.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. |
|
||||
| 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 |
|
||||
### 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`.
|
||||
|
||||
@@ -523,12 +569,12 @@ 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.
|
||||
|
||||
| **Input** | **Description** |
|
||||
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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.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. |
|
||||
| **Input** | **Description** |
|
||||
|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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.source | The source PLACEHOLDER, requires to be one of the existing Template or PR Template placeholders. |
|
||||
| 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.
|
||||
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).
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
jest.setTimeout(180000)
|
||||
@@ -100,14 +101,16 @@ it('Should filter tags correctly using the regex', async () => {
|
||||
{name: '20.0.2', commit: ''}
|
||||
]
|
||||
|
||||
const tagResolver = {
|
||||
const tagResolver: TagResolver = {
|
||||
method: 'non-existing-method',
|
||||
filter: {
|
||||
pattern: 'api-(.+)',
|
||||
method: 'match',
|
||||
flags: 'gu'
|
||||
}
|
||||
}
|
||||
const filtered = filterTags(tags, tagResolver)
|
||||
const filter = validateRegex(tagResolver.filter)
|
||||
const filtered = filterTags(tags, filter)
|
||||
.map(function (tag) {
|
||||
return tag.name
|
||||
})
|
||||
@@ -131,14 +134,16 @@ it('Should filter tags correctly using the regex (inverse)', async () => {
|
||||
{name: '20.0.2', commit: ''}
|
||||
]
|
||||
|
||||
const tagResolver = {
|
||||
const tagResolver: TagResolver = {
|
||||
method: 'non-existing-method',
|
||||
filter: {
|
||||
pattern: '^(?!\\w+-)(.+)',
|
||||
method: 'match',
|
||||
flags: 'gu'
|
||||
}
|
||||
}
|
||||
const filtered = filterTags(tags, tagResolver)
|
||||
const filter = validateRegex(tagResolver.filter)
|
||||
const filtered = filterTags(tags, filter)
|
||||
.map(function (tag) {
|
||||
return tag.name
|
||||
})
|
||||
@@ -160,7 +165,7 @@ it('Should transform tags correctly using the regex', async () => {
|
||||
{name: '20.0.2', commit: ''}
|
||||
]
|
||||
|
||||
const tagResolver = {
|
||||
const tagResolver: TagResolver = {
|
||||
method: 'non-existing-method',
|
||||
transformer: {
|
||||
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) {
|
||||
const transformed = transformTags(tags, transformer)
|
||||
.map(function (tag) {
|
||||
|
||||
+111
-49
@@ -922,27 +922,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
return result;
|
||||
};
|
||||
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));
|
||||
function validateTransformer(transformer) {
|
||||
if (transformer === undefined) {
|
||||
function validateRegex(regex) {
|
||||
if (regex === undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
let target = undefined;
|
||||
if (transformer.hasOwnProperty('target')) {
|
||||
target = transformer.target;
|
||||
}
|
||||
const target = regex.target;
|
||||
const method = regex.method;
|
||||
const onEmpty = regex.on_empty;
|
||||
let onProperty = undefined;
|
||||
let method = undefined;
|
||||
let onEmpty = undefined;
|
||||
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;
|
||||
if (regex.hasOwnProperty('on_property')) {
|
||||
onProperty = regex.on_property;
|
||||
}
|
||||
// legacy handling, transform single value input to array
|
||||
if (!Array.isArray(onProperty)) {
|
||||
@@ -950,14 +942,14 @@ function validateTransformer(transformer) {
|
||||
onProperty = [onProperty];
|
||||
}
|
||||
}
|
||||
return buildRegex(transformer, target, onProperty, method, onEmpty);
|
||||
return buildRegex(regex, target, onProperty, method, onEmpty);
|
||||
}
|
||||
catch (e) {
|
||||
core.warning(`⚠️ Failed to validate transformer: ${transformer.pattern}`);
|
||||
core.warning(`⚠️ Failed to validate transformer: ${regex.pattern}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
exports.validateTransformer = validateTransformer;
|
||||
exports.validateRegex = validateRegex;
|
||||
/**
|
||||
* Constructs the RegExp, providing the configured Regex and additional values
|
||||
*/
|
||||
@@ -978,6 +970,86 @@ function buildRegex(regex, target, onProperty, method, onEmpty) {
|
||||
}
|
||||
}
|
||||
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* () {
|
||||
let tags = [];
|
||||
if (!toTag || !fromTag) {
|
||||
const filterRegex = (0, regexUtils_1.validateRegex)(tagResolver.filter);
|
||||
// filter out tags not matching the specified filter
|
||||
const filteredTags = filterTags(
|
||||
// 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
|
||||
let tagTransfomers = undefined;
|
||||
if (tagResolver.transformer !== undefined) {
|
||||
@@ -1102,7 +1175,7 @@ class Tags {
|
||||
let transformedTags = filteredTags;
|
||||
if (tagTransfomers !== undefined && tagTransfomers.length > 0) {
|
||||
for (const transformer of tagTransfomers) {
|
||||
const tagTransformer = (0, regexUtils_1.validateTransformer)(transformer);
|
||||
const tagTransformer = (0, regexUtils_1.validateRegex)(transformer);
|
||||
if (tagTransformer != null) {
|
||||
core.debug(`ℹ️ Using configured tagTransformer (${transformer.pattern})`);
|
||||
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.
|
||||
* https://github.com/mikepenz/release-changelog-builder-action/issues/566
|
||||
*/
|
||||
function filterTags(tags, tagResolver) {
|
||||
var _a;
|
||||
const filter = tagResolver.filter;
|
||||
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);
|
||||
function filterTags(tags, filterRegex) {
|
||||
if (filterRegex !== null) {
|
||||
const filteredTags = tags.filter(tag => (0, regexUtils_1.transformStringToOptionalValue)(tag.name, filterRegex) !== null);
|
||||
core.debug(`ℹ️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`);
|
||||
return filteredTags;
|
||||
}
|
||||
@@ -1206,7 +1276,7 @@ exports.filterTags = filterTags;
|
||||
function transformTags(tags, transformer) {
|
||||
return tags.map(function (tag) {
|
||||
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}`);
|
||||
return {
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
return transformers.every(transformer => {
|
||||
return matches(pr, transformer, 'rule');
|
||||
@@ -2453,7 +2523,7 @@ function buildChangelog(diffInfo, origPrs, options) {
|
||||
core.info(`ℹ️ Sorted all pull requests ascending: ${JSON.stringify(sort)}`);
|
||||
// establish parent child PR relations
|
||||
if (config.reference !== undefined) {
|
||||
const reference = (0, regexUtils_1.validateTransformer)(config.reference);
|
||||
const reference = (0, regexUtils_1.validateRegex)(config.reference);
|
||||
if (reference !== null) {
|
||||
core.info(`ℹ️ Identifying PR references using \`reference\``);
|
||||
const mapped = new Map();
|
||||
@@ -2491,7 +2561,7 @@ function buildChangelog(diffInfo, origPrs, options) {
|
||||
}
|
||||
// drop duplicate pull requests
|
||||
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) {
|
||||
core.info(`ℹ️ Remove duplicated pull requests using \`duplicate_filter\``);
|
||||
const deduplicatedMap = new Map();
|
||||
@@ -2804,17 +2874,18 @@ function handlePlaceholder(template, key, value, placeholders /* placeholders to
|
||||
const phs = placeholders.get(key);
|
||||
if (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) {
|
||||
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
|
||||
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) {
|
||||
(0, utils_1.createOrSet)(placeholderPrMap, placeholder.name, extractedValue);
|
||||
}
|
||||
transformed = transformed.replaceAll(`#{{${placeholder.name}}}`, configuration.trim_values ? extractedValue.trim() : extractedValue);
|
||||
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) {
|
||||
@@ -2899,7 +2970,7 @@ function validateTransformers(specifiedTransformers) {
|
||||
const transformers = specifiedTransformers;
|
||||
return transformers
|
||||
.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)
|
||||
.map(transformer => {
|
||||
@@ -2932,22 +3003,13 @@ function extractValuesFromString(value, extractor) {
|
||||
if (extractor.pattern == null) {
|
||||
return null;
|
||||
}
|
||||
if (extractor.method === 'match') {
|
||||
const lables = value.match(extractor.pattern);
|
||||
if (lables !== null && lables.length > 0) {
|
||||
return lables.map(label => (label === null || label === void 0 ? void 0 : label.toLocaleLowerCase('en')) || '');
|
||||
}
|
||||
const transformed = (0, regexUtils_1.transformStringToValues)(value, extractor);
|
||||
if (transformed) {
|
||||
return transformed.map(val => (val === null || val === void 0 ? void 0 : val.toLocaleLowerCase('en')) || '');
|
||||
}
|
||||
else {
|
||||
const label = value.replace(extractor.pattern, extractor.target);
|
||||
if (label !== '') {
|
||||
return [label.toLocaleLowerCase('en')];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (extractor.onEmpty !== undefined) {
|
||||
return [extractor.onEmpty.toLocaleLowerCase('en')];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -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 {
|
||||
max_tags_to_fetch: number
|
||||
@@ -14,7 +14,7 @@ export interface Configuration extends PullConfiguration {
|
||||
label_extractor: Extractor[]
|
||||
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.
|
||||
transformers: Transformer[]
|
||||
transformers: Regex[]
|
||||
tag_resolver: TagResolver
|
||||
base_branches: string[]
|
||||
custom_placeholders?: Placeholder[]
|
||||
@@ -51,13 +51,13 @@ export type Property =
|
||||
export interface TagResolver {
|
||||
method: string // semver, sort
|
||||
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 {
|
||||
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
|
||||
transformer: Regex // the transformer to use to transform the original placeholder into the custom placheolder
|
||||
}
|
||||
|
||||
export const DefaultConfiguration: Configuration = {
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
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 {
|
||||
if (transformer === undefined) {
|
||||
export function validateRegex(regex?: Regex): RegexTransformer | null {
|
||||
if (regex === undefined) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
let target = undefined
|
||||
if (transformer.hasOwnProperty('target')) {
|
||||
target = (transformer as Transformer).target
|
||||
}
|
||||
|
||||
const target = regex.target
|
||||
const method = regex.method
|
||||
const onEmpty = regex.on_empty
|
||||
let onProperty = undefined
|
||||
let method = undefined
|
||||
let onEmpty = undefined
|
||||
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
|
||||
if (regex.hasOwnProperty('on_property')) {
|
||||
onProperty = (regex as Extractor).on_property
|
||||
}
|
||||
// legacy handling, transform single value input to array
|
||||
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) {
|
||||
core.warning(`⚠️ Failed to validate transformer: ${transformer.pattern}`)
|
||||
core.warning(`⚠️ Failed to validate transformer: ${regex.pattern}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -42,7 +34,7 @@ export function buildRegex(
|
||||
regex: Regex,
|
||||
target: string | undefined,
|
||||
onProperty?: Property[] | undefined,
|
||||
method?: 'replace' | 'match' | undefined,
|
||||
method?: 'replace' | 'replaceAll' | 'match' | 'regexr' | undefined,
|
||||
onEmpty?: string | undefined
|
||||
): RegexTransformer | null {
|
||||
try {
|
||||
@@ -58,3 +50,83 @@ export function buildRegex(
|
||||
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
@@ -2,10 +2,10 @@ import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import * as semver from 'semver'
|
||||
import {SemVer} from 'semver'
|
||||
import {RegexTransformer, TagResolver, Transformer} from './types'
|
||||
import {Regex, RegexTransformer, TagResolver} from './types'
|
||||
import {createCommandManager} from './gitHelper'
|
||||
import moment from 'moment'
|
||||
import {validateTransformer} from './regexUtils'
|
||||
import {transformStringToOptionalValue, transformStringToValue, validateRegex} from './regexUtils'
|
||||
import {BaseRepository} from '../repositories/BaseRepository'
|
||||
|
||||
export interface TagResult {
|
||||
@@ -88,15 +88,17 @@ export class Tags {
|
||||
let tags: TagInfo[] = []
|
||||
|
||||
if (!toTag || !fromTag) {
|
||||
const filterRegex = validateRegex(tagResolver.filter)
|
||||
|
||||
// filter out tags not matching the specified filter
|
||||
const filteredTags = filterTags(
|
||||
// retrieve the tags from the API
|
||||
await this.getTags(owner, repo, maxTagsToFetch),
|
||||
tagResolver
|
||||
filterRegex
|
||||
)
|
||||
|
||||
// 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 (!Array.isArray(tagResolver.transformer)) {
|
||||
tagTransfomers = [tagResolver.transformer]
|
||||
@@ -109,7 +111,7 @@ export class Tags {
|
||||
let transformedTags: TagInfo[] = filteredTags
|
||||
if (tagTransfomers !== undefined && tagTransfomers.length > 0) {
|
||||
for (const transformer of tagTransfomers) {
|
||||
const tagTransformer = validateTransformer(transformer)
|
||||
const tagTransformer = validateRegex(transformer)
|
||||
if (tagTransformer != null) {
|
||||
core.debug(`ℹ️ Using configured tagTransformer (${transformer.pattern})`)
|
||||
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.
|
||||
* https://github.com/mikepenz/release-changelog-builder-action/issues/566
|
||||
*/
|
||||
export function filterTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[] {
|
||||
const filter = tagResolver.filter
|
||||
if (filter !== undefined) {
|
||||
const regex = new RegExp(filter.pattern.replace('\\\\', '\\'), filter.flags ?? 'gu')
|
||||
const filteredTags = tags.filter(tag => tag.name.match(regex) !== null)
|
||||
export function filterTags(tags: TagInfo[], filterRegex: RegexTransformer | null): TagInfo[] {
|
||||
if (filterRegex !== null) {
|
||||
const filteredTags = tags.filter(tag => transformStringToOptionalValue(tag.name, filterRegex) !== null)
|
||||
core.debug(`ℹ️ Filtered tags count: ${filteredTags.length}, original count: ${tags.length}`)
|
||||
return filteredTags
|
||||
} else {
|
||||
@@ -214,7 +214,7 @@ export function filterTags(tags: TagInfo[], tagResolver: TagResolver): TagInfo[]
|
||||
export function transformTags(tags: TagInfo[], transformer: RegexTransformer): TagInfo[] {
|
||||
return tags.map(function (tag) {
|
||||
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}`)
|
||||
return {
|
||||
tmp: tag.name, // remember the original name
|
||||
|
||||
@@ -36,28 +36,25 @@ export interface Sort {
|
||||
export interface TagResolver {
|
||||
method: string // semver, sort
|
||||
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 {
|
||||
pattern: string // the regex pattern to match
|
||||
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
|
||||
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
|
||||
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 {
|
||||
pattern: RegExp | null
|
||||
target: string
|
||||
onProperty?: Property[]
|
||||
method?: 'replace' | 'match'
|
||||
method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
|
||||
onEmpty?: string
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import * as core from '@actions/core'
|
||||
import {RegexTransformer, Rule} from './pr-collector/types'
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
return transformers.every(transformer => {
|
||||
return matches(pr, transformer, 'rule')
|
||||
|
||||
+15
-24
@@ -10,8 +10,8 @@ import {
|
||||
sortPullRequests
|
||||
} from './pr-collector/pullRequests'
|
||||
import {DiffInfo} from './pr-collector/commits'
|
||||
import {validateTransformer} from './pr-collector/regexUtils'
|
||||
import {RegexTransformer, Transformer} from './pr-collector/types'
|
||||
import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils'
|
||||
import {Regex, RegexTransformer} from './pr-collector/types'
|
||||
import {ReleaseNotesOptions} from './releaseNotesBuilder'
|
||||
import {matchesRules} from './regexUtils'
|
||||
|
||||
@@ -40,7 +40,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
||||
|
||||
// establish parent child PR relations
|
||||
if (config.reference !== undefined) {
|
||||
const reference = validateTransformer(config.reference)
|
||||
const reference = validateRegex(config.reference)
|
||||
if (reference !== null) {
|
||||
core.info(`ℹ️ Identifying PR references using \`reference\``)
|
||||
|
||||
@@ -77,7 +77,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
|
||||
|
||||
// drop duplicate pull requests
|
||||
if (config.duplicate_filter !== undefined) {
|
||||
const extractor = validateTransformer(config.duplicate_filter)
|
||||
const extractor = validateRegex(config.duplicate_filter)
|
||||
if (extractor !== null) {
|
||||
core.info(`ℹ️ Remove duplicated pull requests using \`duplicate_filter\``)
|
||||
|
||||
@@ -461,11 +461,12 @@ function handlePlaceholder(
|
||||
const phs = placeholders.get(key)
|
||||
if (phs) {
|
||||
for (const placeholder of phs) {
|
||||
const transformer = validateTransformer(placeholder.transformer)
|
||||
const transformer = validateRegex(placeholder.transformer)
|
||||
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
|
||||
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) {
|
||||
createOrSet(placeholderPrMap, placeholder.name, extractedValue)
|
||||
}
|
||||
@@ -475,7 +476,7 @@ function handlePlaceholder(
|
||||
)
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
function validateTransformers(specifiedTransformers: Transformer[]): RegexTransformer[] {
|
||||
function validateTransformers(specifiedTransformers: Regex[]): RegexTransformer[] {
|
||||
const transformers = specifiedTransformers
|
||||
return transformers
|
||||
.map(transformer => {
|
||||
return validateTransformer(transformer)
|
||||
return validateRegex(transformer)
|
||||
})
|
||||
.filter(transformer => transformer?.pattern != null)
|
||||
.map(transformer => {
|
||||
@@ -619,20 +620,10 @@ function extractValuesFromString(value: string, extractor: RegexTransformer): st
|
||||
if (extractor.pattern == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (extractor.method === 'match') {
|
||||
const lables = value.match(extractor.pattern)
|
||||
if (lables !== null && lables.length > 0) {
|
||||
return lables.map(label => label?.toLocaleLowerCase('en') || '')
|
||||
}
|
||||
const transformed = transformStringToValues(value, extractor)
|
||||
if (transformed) {
|
||||
return transformed.map(val => val?.toLocaleLowerCase('en') || '')
|
||||
} else {
|
||||
const label = value.replace(extractor.pattern, extractor.target)
|
||||
if (label !== '') {
|
||||
return [label.toLocaleLowerCase('en')]
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (extractor.onEmpty !== undefined) {
|
||||
return [extractor.onEmpty.toLocaleLowerCase('en')]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user