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
+59 -13
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 |
| 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).
+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'
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) {
Generated Vendored
+111 -49
View File
@@ -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;
}
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 {
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 = {
+91 -19
View File
@@ -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
View File
@@ -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
+5 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
}