- drop exec and execAll in favor of the much simpler regexr based solution

- define regex, define target pattern. success 🎉
This commit is contained in:
Mike Penz
2024-03-01 16:42:29 +00:00
committed by GitHub
parent 40d2beb495
commit 2a0a3aa6dc
5 changed files with 135 additions and 118 deletions
+55 -12
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 |
@@ -494,9 +490,56 @@ Table of descriptions for the `configuration.json` options to configure the resu
| 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 |
### 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.
### Custom placeholders 🧪 | **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`.
@@ -526,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. 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).
Generated Vendored
+37 -51
View File
@@ -922,7 +922,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.transformStringToValue = exports.transformStringToOptionalValue = exports.transformStringToValues = exports.applyCaptureGroup = exports.buildRegex = exports.validateRegex = 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 validateRegex(regex) { function validateRegex(regex) {
if (regex === undefined) { if (regex === undefined) {
@@ -970,65 +970,19 @@ function buildRegex(regex, target, onProperty, method, onEmpty) {
} }
} }
exports.buildRegex = buildRegex; exports.buildRegex = buildRegex;
// eslint-disable-next-line no-undef
function applyCaptureGroup(value, target) {
const groups = value['groups'];
if (groups) {
const matched = groups[target];
if (matched) {
// if we had a perfect group match return that.
return matched;
}
}
if (target.startsWith('$') && !target.startsWith('$$')) {
// if we start with $ offer support for matching index based capture groups
const index = Number(target.substring(1));
if (!isNaN(index) && index < value.length) {
return value[index];
}
}
return null;
}
exports.applyCaptureGroup = applyCaptureGroup;
function transformStringToValues(value, extractor) { function transformStringToValues(value, extractor) {
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null; return null;
} }
if (extractor.method === 'exec' || extractor.method === 'execAll') { if (extractor.method === 'regexr') {
// eslint-disable-next-line no-undef const matches = transformRegexr(extractor.pattern, value, extractor.target);
let matches; if (matches !== null && matches.size > 0) {
const result = new Set(); return [...matches];
// match regex to all occurrences in the string if we run `execAll`
// otherwise just do the first match with exec
do {
matches = extractor.pattern.exec(value);
if (matches) {
if (extractor.target) {
const matchedGroup = applyCaptureGroup(matches, extractor.target);
if (matchedGroup) {
result.add(matchedGroup);
}
}
else {
for (const match of matches) {
result.add(match);
}
}
}
} while (matches && extractor.method === 'execAll');
if (result.size > 0) {
return [...result];
} }
} }
else if (extractor.method === 'match') { else if (extractor.method === 'match') {
const matches = value.match(extractor.pattern); const matches = value.match(extractor.pattern);
if (matches !== null && matches.length > 0) { if (matches !== null && matches.length > 0) {
if (extractor.target) {
const matchedGroup = applyCaptureGroup(matches, extractor.target);
if (matchedGroup) {
return [matchedGroup];
}
}
return matches.map(match => match || ''); return matches.map(match => match || '');
} }
} }
@@ -1064,6 +1018,38 @@ function transformStringToValue(value, extractor) {
return transformStringToOptionalValue(value, extractor) || ''; return transformStringToOptionalValue(value, extractor) || '';
} }
exports.transformStringToValue = transformStringToValue; 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;
}
/***/ }), /***/ }),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+40 -52
View File
@@ -34,7 +34,7 @@ export function buildRegex(
regex: Regex, regex: Regex,
target: string | undefined, target: string | undefined,
onProperty?: Property[] | undefined, onProperty?: Property[] | undefined,
method?: 'replace' | 'replaceAll' | 'match' | 'exec' | 'execAll' | undefined, method?: 'replace' | 'replaceAll' | 'match' | 'regexr' | undefined,
onEmpty?: string | undefined onEmpty?: string | undefined
): RegexTransformer | null { ): RegexTransformer | null {
try { try {
@@ -51,66 +51,19 @@ export function buildRegex(
} }
} }
// eslint-disable-next-line no-undef
export function applyCaptureGroup(value: RegExpMatchArray, target: string): string | null {
const groups = value['groups']
if (groups) {
const matched = groups[target]
if (matched) {
// if we had a perfect group match return that.
return matched
}
}
if (target.startsWith('$') && !target.startsWith('$$')) {
// if we start with $ offer support for matching index based capture groups
const index = Number(target.substring(1))
if (!isNaN(index) && index < value.length) {
return value[index]
}
}
return null
}
export function transformStringToValues(value: string, extractor: RegexTransformer): string[] | null { export function transformStringToValues(value: string, extractor: RegexTransformer): string[] | null {
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null return null
} }
if (extractor.method === 'exec' || extractor.method === 'execAll') { if (extractor.method === 'regexr') {
// eslint-disable-next-line no-undef const matches = transformRegexr(extractor.pattern, value, extractor.target)
let matches: RegExpMatchArray | null if (matches !== null && matches.size > 0) {
const result: Set<string> = new Set() return [...matches]
// match regex to all occurrences in the string if we run `execAll`
// otherwise just do the first match with exec
do {
matches = extractor.pattern.exec(value)
if (matches) {
if (extractor.target) {
const matchedGroup = applyCaptureGroup(matches, extractor.target)
if (matchedGroup) {
result.add(matchedGroup)
}
} else {
for (const match of matches) {
result.add(match)
}
}
}
} while (matches && extractor.method === 'execAll')
if (result.size > 0) {
return [...result]
} }
} else if (extractor.method === 'match') { } else if (extractor.method === 'match') {
const matches = value.match(extractor.pattern) const matches = value.match(extractor.pattern)
if (matches !== null && matches.length > 0) { if (matches !== null && matches.length > 0) {
if (extractor.target) {
const matchedGroup = applyCaptureGroup(matches, extractor.target)
if (matchedGroup) {
return [matchedGroup]
}
}
return matches.map(match => match || '') return matches.map(match => match || '')
} }
} else if (extractor.method === 'replaceAll') { } else if (extractor.method === 'replaceAll') {
@@ -142,3 +95,38 @@ export function transformStringToOptionalValue(value: string, extractor: RegexTr
export function transformStringToValue(value: string, extractor: RegexTransformer): string { export function transformStringToValue(value: string, extractor: RegexTransformer): string {
return transformStringToOptionalValue(value, extractor) || '' 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
}
+2 -2
View File
@@ -43,7 +43,7 @@ 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
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' | 'exec' | 'execAll' | undefined // the method to use to extract the value, `match` will not use the `target` property 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) 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)
} }
@@ -55,6 +55,6 @@ export interface RegexTransformer {
pattern: RegExp | null pattern: RegExp | null
target: string target: string
onProperty?: Property[] onProperty?: Property[]
method?: 'replace' | 'replaceAll' | 'match' | 'exec' | 'execAll' method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
onEmpty?: string onEmpty?: string
} }