Merge pull request #1133 from mikepenz/feature/1074

Offer new API to reference parent / child PR relations
This commit is contained in:
Mike Penz
2023-06-04 21:05:45 +02:00
committed by GitHub
7 changed files with 230 additions and 11 deletions
+19 -2
View File
@@ -259,6 +259,12 @@ This configuration is a `JSON` in the following format. (The below showcases *ex
"on_property": "title",
"method": "match"
},
"reference": {
"pattern": ".*\\ \\#(.).*",
"on_property": "body",
"method": "replace",
"target": "$1"
},
"transformers": [
{
"pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)",
@@ -371,8 +377,10 @@ When using `*` values are joined by `,`.
| `${{REVIEWERS[*]}}` | GitHub Login names of specified reviewers. Requires `fetchReviewers` to be enabled. |
| `${{APPROVERS[*]}}` | GitHub Login names of users who approved the PR. |
Additionally there is a special array placeholder `REVIEWS` which allows access to it's properties:
`(KEY)[(*/index)].(property)` for example: `REVIEWS[*].author` or `REVIEWS[*].body`
Additionally there are special array placeholders like `REVIEWS` which allows access to it's properties via
`(KEY)[(*/index)].(property)`.
For example: `REVIEWS[*].author` or `REVIEWS[*].body`
| **Placeholder** | **Description** |
|-------------------------------|--------------------------------------------|
@@ -382,6 +390,14 @@ Additionally there is a special array placeholder `REVIEWS` which allows access
| `${{REVIEWS[*].submittedAt}}` | The date whent he review was submitted. |
| `${{REVIEWS[*].state}}` | The state of the given review. |
Similar to `REVIEWS`, `REFERENCED` PRs also offer special placeholders.
| **Placeholder** | **Description** |
|-------------------------------|---------------------------------------------------------------------------|
| `${{REFERENCED[*].number}}` | The PR number of the referenced PR. |
| `${{REFERENCED[*].title}}` | The title of the referenced PR. |
| `${{REFERENCED[*]."..."}}` | Allows to use most other PR properties as placeholder. |
</p>
</details>
@@ -448,6 +464,7 @@ Table of descriptions for the `configuration.json` options to configure the resu
| 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 |
| transformer.pattern | A `regex` pattern, extracting values of the change message. |
| transformer.target | The result pattern, the regex groups will be filled into. Allows for full transformation of a pull request message. Including potentially specified texts |
+21 -1
View File
@@ -266,7 +266,7 @@ pullRequestsWithLabels.push(
repoName: 'test-repo',
labels: ['issue', 'fix'],
milestone: '',
body: 'no magic body for this matter',
body: 'no magic body for this matter - #1',
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
@@ -399,6 +399,26 @@ it('Deduplicate duplicated PRs DESC', async () => {
)
})
it('Reference PRs', async () => {
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.categories = [
{
title: '',
labels: []
}
]
customConfig.pr_template = "${{NUMBER}} -- ${{REFERENCED[*].number}}"
customConfig.reference = {
pattern: '.*\ \#(.).*', // matches the 1 from "abcdefg #1 adfasdf"
on_property: 'body',
method: 'replace',
target: '$1'
}
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
`1 -- 2\n4 -- \n3 -- \n\n`
)
})
it('Use empty_content for empty category', async () => {
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.categories = [
Generated Vendored
+85 -3
View File
@@ -436,8 +436,9 @@ const pullRequests_1 = __nccwpck_require__(1948);
const regexUtils_1 = __nccwpck_require__(3078);
const regexUtils_2 = __nccwpck_require__(2364);
const EMPTY_MAP = new Map();
function buildChangelog(diffInfo, prs, options) {
function buildChangelog(diffInfo, origPrs, options) {
core.startGroup('📦 Build changelog');
let prs = origPrs;
if (prs.length === 0) {
core.warning(`⚠️ No pull requests found`);
const result = replaceEmptyTemplate(options.configuration.empty_template, options);
@@ -449,10 +450,48 @@ function buildChangelog(diffInfo, prs, options) {
const sort = config.sort;
prs = (0, pullRequests_1.sortPullRequests)(prs, sort);
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);
if (reference !== null) {
core.info(`️ Identifying PR references using \`reference\``);
const mapped = new Map();
for (const pr of prs) {
mapped.set(pr.number, pr);
}
const remappedPrs = [];
for (const pr of prs) {
const extracted = extractValues(pr, reference, 'reference');
if (extracted !== null && extracted.length > 0) {
const foundNumber = parseInt(extracted[0]);
const valid = !isNaN(foundNumber);
const parent = mapped.get(foundNumber);
if (valid && parent !== undefined) {
if (parent.childPrs === undefined) {
parent.childPrs = [];
}
parent.childPrs.push(pr);
}
else {
if (!valid)
core.warning(`⚠️ Extracted reference 'isNaN': ${extracted}`);
remappedPrs.push(pr);
}
}
else {
remappedPrs.push(pr);
}
}
prs = remappedPrs;
}
else {
core.warning(`⚠️ Configured \`reference\` invalid.`);
}
}
// drop duplicate pull requests
if (config.duplicate_filter !== undefined) {
const extractor = (0, regexUtils_1.validateTransformer)(config.duplicate_filter);
if (extractor != null) {
if (extractor !== null) {
core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``);
const deduplicatedMap = new Map();
const unmatched = [];
@@ -672,6 +711,7 @@ function buildChangelog(diffInfo, prs, options) {
transformedChangelog = replacePlaceholders(transformedChangelog, EMPTY_MAP, placeholderMap, placeholders, placeholderPrMap, config);
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap, config);
transformedChangelog = cleanupPrPlaceholders(transformedChangelog, placeholders);
transformedChangelog = cleanupPlaceholders(transformedChangelog);
core.info(`️ Filled template`);
core.endGroup();
return transformedChangelog;
@@ -709,6 +749,7 @@ function fillPrTemplate(pr, template, placeholders /* placeholders to apply */,
var _a, _b, _c, _d, _e, _f;
const arrayPlaceholderMap = new Map();
fillReviewPlaceholders(arrayPlaceholderMap, 'REVIEWS', pr.reviews || []);
fillChildPrPlaceholders(arrayPlaceholderMap, 'REFERENCED', pr.childPrs || []);
const placeholderMap = new Map();
placeholderMap.set('NUMBER', pr.number.toString());
placeholderMap.set('TITLE', pr.title);
@@ -771,6 +812,8 @@ function handlePlaceholder(template, key, value, placeholders /* placeholders to
return transformed;
}
function fillArrayPlaceholders(placeholderMap /* placeholderKey and original value */, key, values) {
if (values.length === 0)
return;
for (let i = 0; i < values.length; i++) {
placeholderMap.set(`${key}[${i}]`, values[i]);
}
@@ -778,6 +821,8 @@ function fillArrayPlaceholders(placeholderMap /* placeholderKey and original val
}
function fillReviewPlaceholders(placeholderMap /* placeholderKey and original value */, parentKey, values) {
var _a;
if (values.length === 0)
return;
// retrieve the keys from the CommentInfo object
for (const childKey of Object.keys(pullRequests_1.EMPTY_COMMENT_INFO)) {
for (let i = 0; i < values.length; i++) {
@@ -786,6 +831,18 @@ function fillReviewPlaceholders(placeholderMap /* placeholderKey and original va
placeholderMap.set(`${parentKey}[*].${childKey}`, values.map(value => { var _a; return ((_a = value[childKey]) === null || _a === void 0 ? void 0 : _a.toLocaleString('en')) || ''; }).join(', '));
}
}
function fillChildPrPlaceholders(placeholderMap /* placeholderKey and original value */, parentKey, values) {
var _a;
if (values.length === 0)
return;
// retrieve the keys from the PullRequestInfo object
for (const childKey of Object.keys(pullRequests_1.EMPTY_PULL_REQUEST_INFO)) {
for (let i = 0; i < values.length; i++) {
placeholderMap.set(`${parentKey}[${i}].${childKey}`, ((_a = values[i][childKey]) === null || _a === void 0 ? void 0 : _a.toLocaleString('en')) || '');
}
placeholderMap.set(`${parentKey}[*].${childKey}`, values.map(value => { var _a; return ((_a = value[childKey]) === null || _a === void 0 ? void 0 : _a.toLocaleString('en')) || ''; }).join(', '));
}
}
function replacePrPlaceholders(template, placeholderPrMap /* map with all pr related custom placeholder values */, configuration) {
let transformed = template;
for (const [key, values] of placeholderPrMap) {
@@ -805,6 +862,13 @@ function cleanupPrPlaceholders(template, placeholders) {
}
return transformed;
}
function cleanupPlaceholders(template) {
let transformed = template;
for (const phs of ['REVIEWS', 'REFERENCED', 'ASSIGNEES', 'REVIEWERS', 'APPROVERS']) {
transformed = transformed.replaceAll(new RegExp(`\\$\\{\\{${phs}\\[.+?\\]\\..*?\\}\\}`, 'gu'), '');
}
return transformed;
}
function transform(filled, transformers) {
if (transformers.length === 0) {
return filled;
@@ -17115,10 +17179,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.retrieveProperty = exports.compare = exports.sortPullRequests = exports.PullRequests = exports.EMPTY_COMMENT_INFO = void 0;
exports.retrieveProperty = exports.compare = exports.sortPullRequests = exports.PullRequests = exports.EMPTY_COMMENT_INFO = exports.EMPTY_PULL_REQUEST_INFO = void 0;
const core = __importStar(__nccwpck_require__(2242));
const moment_1 = __importDefault(__nccwpck_require__(8985));
const commits_1 = __nccwpck_require__(5789);
exports.EMPTY_PULL_REQUEST_INFO = {
number: 0,
title: "",
htmlURL: "",
baseBranch: "",
mergedAt: undefined,
createdAt: (0, moment_1.default)(),
mergeCommitSha: "",
author: "",
repoName: "",
labels: [],
milestone: "",
body: "",
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'open'
};
exports.EMPTY_COMMENT_INFO = {
id: 0,
htmlURL: '',
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+19
View File
@@ -36,6 +36,25 @@ export interface CommentInfo {
state: string | undefined
}
export const EMPTY_PULL_REQUEST_INFO: PullRequestInfo = {
number: 0,
title: "",
htmlURL: "",
baseBranch: "",
mergedAt: undefined,
createdAt: moment(),
mergeCommitSha: "",
author: "",
repoName: "",
labels: [],
milestone: "",
body: "",
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'open'
}
export const EMPTY_COMMENT_INFO: CommentInfo = {
id: 0,
htmlURL: '',
+1
View File
@@ -13,6 +13,7 @@ export interface Configuration extends PullConfiguration {
ignore_labels: string[]
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[]
tag_resolver: TagResolver
base_branches: string[]
+84 -4
View File
@@ -1,7 +1,14 @@
import * as core from '@actions/core'
import {Category, Configuration, Placeholder, Property} from './configuration'
import {createOrSet, haveCommonElementsArr, haveEveryElementsArr} from './utils'
import {CommentInfo, EMPTY_COMMENT_INFO, PullRequestInfo, retrieveProperty, sortPullRequests} from 'github-pr-collector/lib/pullRequests'
import {
CommentInfo,
EMPTY_COMMENT_INFO,
EMPTY_PULL_REQUEST_INFO,
PullRequestInfo,
retrieveProperty,
sortPullRequests
} from 'github-pr-collector/lib/pullRequests'
import {DiffInfo} from 'github-pr-collector/lib/commits'
import {validateTransformer} from 'github-pr-collector/lib/regexUtils'
import {Transformer, RegexTransformer} from 'github-pr-collector/lib/types'
@@ -10,8 +17,14 @@ import {matchesRules} from './regexUtils'
const EMPTY_MAP = new Map<string, string>()
export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], options: ReleaseNotesOptions): string {
export interface PullRequestData extends PullRequestInfo {
childPrs?: PullRequestInfo[]
}
export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], options: ReleaseNotesOptions): string {
core.startGroup('📦 Build changelog')
let prs: PullRequestData[] = origPrs
if (prs.length === 0) {
core.warning(`⚠️ No pull requests found`)
const result = replaceEmptyTemplate(options.configuration.empty_template, options)
@@ -25,10 +38,47 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
prs = sortPullRequests(prs, sort)
core.info(`️ Sorted all pull requests ascending: ${JSON.stringify(sort)}`)
// establish parent child PR relations
if (config.reference !== undefined) {
const reference = validateTransformer(config.reference)
if (reference !== null) {
core.info(`️ Identifying PR references using \`reference\``)
const mapped = new Map<number, PullRequestData>()
for (const pr of prs) {
mapped.set(pr.number, pr)
}
const remappedPrs: PullRequestData[] = []
for (const pr of prs) {
const extracted = extractValues(pr, reference, 'reference')
if (extracted !== null && extracted.length > 0) {
const foundNumber = parseInt(extracted[0])
const valid = !isNaN(foundNumber)
const parent = mapped.get(foundNumber)
if (valid && parent !== undefined) {
if (parent.childPrs === undefined) {
parent.childPrs = []
}
parent.childPrs.push(pr)
} else {
if (!valid) core.warning(`⚠️ Extracted reference 'isNaN': ${extracted}`)
remappedPrs.push(pr)
}
} else {
remappedPrs.push(pr)
}
}
prs = remappedPrs
} else {
core.warning(`⚠️ Configured \`reference\` invalid.`)
}
}
// drop duplicate pull requests
if (config.duplicate_filter !== undefined) {
const extractor = validateTransformer(config.duplicate_filter)
if (extractor != null) {
if (extractor !== null) {
core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``)
const deduplicatedMap = new Map<string, PullRequestInfo>()
@@ -283,6 +333,7 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
transformedChangelog = replacePlaceholders(transformedChangelog, EMPTY_MAP, placeholderMap, placeholders, placeholderPrMap, config)
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap, config)
transformedChangelog = cleanupPrPlaceholders(transformedChangelog, placeholders)
transformedChangelog = cleanupPlaceholders(transformedChangelog)
core.info(`️ Filled template`)
core.endGroup()
return transformedChangelog
@@ -322,7 +373,7 @@ function fillAdditionalPlaceholders(
}
function fillPrTemplate(
pr: PullRequestInfo,
pr: PullRequestData,
template: string,
placeholders: Map<string, Placeholder[]> /* placeholders to apply */,
placeholderPrMap: Map<string, string[]> /* map to keep replaced placeholder values with their key */,
@@ -330,6 +381,7 @@ function fillPrTemplate(
): string {
const arrayPlaceholderMap = new Map<string, string>()
fillReviewPlaceholders(arrayPlaceholderMap, 'REVIEWS', pr.reviews || [])
fillChildPrPlaceholders(arrayPlaceholderMap, 'REFERENCED', pr.childPrs || [])
const placeholderMap = new Map<string, string>()
placeholderMap.set('NUMBER', pr.number.toString())
placeholderMap.set('TITLE', pr.title)
@@ -419,6 +471,7 @@ function fillArrayPlaceholders(
key: string,
values: string[]
): void {
if (values.length === 0) return
for (let i = 0; i < values.length; i++) {
placeholderMap.set(`${key}[${i}]`, values[i])
}
@@ -430,6 +483,7 @@ function fillReviewPlaceholders(
parentKey: string,
values: CommentInfo[]
): void {
if (values.length === 0) return
// retrieve the keys from the CommentInfo object
for (const childKey of Object.keys(EMPTY_COMMENT_INFO)) {
for (let i = 0; i < values.length; i++) {
@@ -442,6 +496,24 @@ function fillReviewPlaceholders(
}
}
function fillChildPrPlaceholders(
placeholderMap: Map<string, string> /* placeholderKey and original value */,
parentKey: string,
values: PullRequestInfo[]
): void {
if (values.length === 0) return
// retrieve the keys from the PullRequestInfo object
for (const childKey of Object.keys(EMPTY_PULL_REQUEST_INFO)) {
for (let i = 0; i < values.length; i++) {
placeholderMap.set(`${parentKey}[${i}].${childKey}`, values[i][childKey as keyof PullRequestInfo]?.toLocaleString('en') || '')
}
placeholderMap.set(
`${parentKey}[*].${childKey}`,
values.map(value => value[childKey as keyof PullRequestInfo]?.toLocaleString('en') || '').join(', ')
)
}
}
function replacePrPlaceholders(
template: string,
placeholderPrMap: Map<string, string[]> /* map with all pr related custom placeholder values */,
@@ -467,6 +539,14 @@ function cleanupPrPlaceholders(template: string, placeholders: Map<string, Place
return transformed
}
function cleanupPlaceholders(template: string): string {
let transformed = template
for (const phs of ['REVIEWS', 'REFERENCED', 'ASSIGNEES', 'REVIEWERS', 'APPROVERS']) {
transformed = transformed.replaceAll(new RegExp(`\\$\\{\\{${phs}\\[.+?\\]\\..*?\\}\\}`, 'gu'), '')
}
return transformed
}
function transform(filled: string, transformers: RegexTransformer[]): string {
if (transformers.length === 0) {
return filled