- introduce new rules for the Category
- the rules can be used to match specified `Properties` with a `RegExp` (uses `test`) - to add them into a category - This allows more complex changelogs to be constructed, which for example want all `OPEN` PRs in one category
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import {buildChangelog} from '../src/transform'
|
import {buildChangelog} from '../src/transform'
|
||||||
import {PullRequestInfo} from '../src/pullRequests'
|
import {PullRequestInfo} from '../src/pullRequests'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import {Configuration, DefaultConfiguration} from '../src/configuration'
|
import {DefaultConfiguration} from '../src/configuration'
|
||||||
import {DefaultDiffInfo} from '../src/commits'
|
import {DefaultDiffInfo} from '../src/commits'
|
||||||
|
|
||||||
jest.setTimeout(180000)
|
jest.setTimeout(180000)
|
||||||
|
|||||||
+23
-3
@@ -20,12 +20,32 @@ export interface Configuration {
|
|||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
title: string // the title of this category
|
title: string // the title of this category
|
||||||
labels: string[] // labels to associate PRs to this category
|
labels?: string[] // labels to associate PRs to this category
|
||||||
exclude_labels?: string[] // if an exclude label is detected, the PR will be excluded from this category
|
exclude_labels?: string[] // if an exclude label is detected, the PR will be excluded from this category
|
||||||
exhaustive?: boolean // requires all labels to be present in the PR
|
rules?: Rule[] // rules to associate PRs to this category
|
||||||
|
exhaustive?: boolean // requires all labels AND/OR rules to be present in the PR
|
||||||
empty_content?: string // if the category has no matching PRs, this content will be used. If not set, the category will be skipped in the changelog.
|
empty_content?: string // if the category has no matching PRs, this content will be used. If not set, the category will be skipped in the changelog.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the properties of the PullRequestInfo useable in different configurations
|
||||||
|
*/
|
||||||
|
export type Property =
|
||||||
|
| 'title'
|
||||||
|
| 'branch'
|
||||||
|
| 'author'
|
||||||
|
| 'labels'
|
||||||
|
| 'milestone'
|
||||||
|
| 'body'
|
||||||
|
| 'assignees'
|
||||||
|
| 'requestedReviewers'
|
||||||
|
| 'approvedReviewers'
|
||||||
|
| 'status'
|
||||||
|
|
||||||
|
export interface Rule extends Regex {
|
||||||
|
on_property?: Property // retrieve the property to apply the rule on
|
||||||
|
}
|
||||||
|
|
||||||
export interface Sort {
|
export interface Sort {
|
||||||
order: 'ASC' | 'DESC' // the sorting order
|
order: 'ASC' | 'DESC' // the sorting order
|
||||||
on_property: 'mergedAt' | 'title' // the property to sort on. (mergedAt falls back to createdAt)
|
on_property: 'mergedAt' | 'title' // the property to sort on. (mergedAt falls back to createdAt)
|
||||||
@@ -41,7 +61,7 @@ export interface Transformer extends Regex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Extractor extends Transformer {
|
export interface Extractor extends Transformer {
|
||||||
on_property?: ('title' | 'author' | 'milestone' | 'body' | 'branch')[] | 'title' | 'author' | 'milestone' | 'body' | 'branch' | undefined // retrieve the property to extract the value from
|
on_property?: Property[] | Property | undefined // retrieve the property to extract the value from
|
||||||
method?: 'replace' | 'match' | undefined // the method to use to extract the value, `match` will not use the `target` property
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
|
|||||||
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
|
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
|
||||||
import {Unpacked} from './utils'
|
import {Unpacked} from './utils'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import {Sort} from './configuration'
|
import {Property, Sort} from './configuration'
|
||||||
|
|
||||||
export interface PullRequestInfo {
|
export interface PullRequestInfo {
|
||||||
number: number
|
number: number
|
||||||
@@ -226,6 +226,22 @@ export function compare(a: PullRequestInfo, b: PullRequestInfo, sort: Sort): num
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to retrieve a property from the PullRequestInfo
|
||||||
|
*/
|
||||||
|
export function retrieveProperty(pr: PullRequestInfo, property: Property, useCase: string): string {
|
||||||
|
let value: string | Set<string> | string[] | undefined = pr[property]
|
||||||
|
if (value === undefined) {
|
||||||
|
core.warning(`⚠️ the provided property '${property}' for \`${useCase}\` is not valid. Fallback to 'body'`)
|
||||||
|
value = pr['body']
|
||||||
|
} else if (value instanceof Set) {
|
||||||
|
value = Array.from(value).join(',') // join into single string
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
value = value.join(',') // join into single string
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// helper function to add a special open label to prs not merged.
|
// helper function to add a special open label to prs not merged.
|
||||||
function attachSpeciaLabels(status: 'open' | 'merged', labels: Set<string>): Set<string> {
|
function attachSpeciaLabels(status: 'open' | 'merged', labels: Set<string>): Set<string> {
|
||||||
labels.add(`--rcba-${status}`)
|
labels.add(`--rcba-${status}`)
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import {Extractor, Property, Regex, Rule, Transformer} from './configuration'
|
||||||
|
import {PullRequestInfo, retrieveProperty} from './pullRequests'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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[]
|
||||||
|
if (exhaustive) {
|
||||||
|
return transformers.every(transformer => {
|
||||||
|
return matches(pr, transformer, 'rule')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return transformers.some(transformer => {
|
||||||
|
return matches(pr, transformer, 'rule')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the configured property results in a positive `test` with the regex.
|
||||||
|
*/
|
||||||
|
function matches(pr: PullRequestInfo, extractor: RegexTransformer, extractor_usecase: string): boolean {
|
||||||
|
if (extractor.pattern == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractor.onProperty !== undefined && extractor.onProperty.length === 1) {
|
||||||
|
const prop = extractor.onProperty[0]
|
||||||
|
const value = retrieveProperty(pr, prop, extractor_usecase)
|
||||||
|
return extractor.pattern.test(value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTransformer(transformer?: Regex): RegexTransformer | null {
|
||||||
|
if (transformer === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let target = undefined
|
||||||
|
if (transformer.hasOwnProperty('target')) {
|
||||||
|
target = (transformer as Transformer).target
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// legacy handling, transform single value input to array
|
||||||
|
if (!Array.isArray(onProperty)) {
|
||||||
|
if (onProperty !== undefined) {
|
||||||
|
onProperty = [onProperty]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildRegex(transformer, target, onProperty, method, onEmpty)
|
||||||
|
} catch (e) {
|
||||||
|
core.warning(`⚠️ Failed to validate transformer: ${transformer.pattern}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the RegExp, providing the configured Regex and additional values
|
||||||
|
*/
|
||||||
|
export function buildRegex(
|
||||||
|
regex: Regex,
|
||||||
|
target: string | undefined,
|
||||||
|
onProperty?: Property[] | undefined,
|
||||||
|
method?: 'replace' | 'match' | undefined,
|
||||||
|
onEmpty?: string | undefined
|
||||||
|
): RegexTransformer | null {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
pattern: new RegExp(regex.pattern.replace('\\\\', '\\'), regex.flags ?? 'gu'),
|
||||||
|
target: target || '',
|
||||||
|
onProperty,
|
||||||
|
method,
|
||||||
|
onEmpty
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
core.warning(`⚠️ Bad replacer regex: ${regex.pattern}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexTransformer {
|
||||||
|
pattern: RegExp | null
|
||||||
|
target: string
|
||||||
|
onProperty?: Property[]
|
||||||
|
method?: 'replace' | 'match'
|
||||||
|
onEmpty?: string
|
||||||
|
}
|
||||||
+1
-1
@@ -5,8 +5,8 @@ import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
|
|||||||
import {SemVer} from 'semver'
|
import {SemVer} from 'semver'
|
||||||
import {TagResolver} from './configuration'
|
import {TagResolver} from './configuration'
|
||||||
import {createCommandManager} from './gitHelper'
|
import {createCommandManager} from './gitHelper'
|
||||||
import {RegexTransformer, validateTransformer} from './transform'
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import {RegexTransformer, validateTransformer} from './regexUtils'
|
||||||
|
|
||||||
export interface TagResult {
|
export interface TagResult {
|
||||||
from: TagInfo | null
|
from: TagInfo | null
|
||||||
|
|||||||
+35
-68
@@ -1,17 +1,10 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {Category, Configuration, Extractor, Placeholder, Transformer} from './configuration'
|
import {Category, Configuration, Placeholder, Property, Transformer} from './configuration'
|
||||||
import {CommentInfo, EMPTY_COMMENT_INFO, PullRequestInfo, sortPullRequests} from './pullRequests'
|
import {CommentInfo, EMPTY_COMMENT_INFO, PullRequestInfo, retrieveProperty, sortPullRequests} from './pullRequests'
|
||||||
import {ReleaseNotesOptions} from './releaseNotes'
|
import {ReleaseNotesOptions} from './releaseNotes'
|
||||||
import {DiffInfo} from './commits'
|
import {DiffInfo} from './commits'
|
||||||
import {createOrSet, haveCommonElements, haveEveryElements} from './utils'
|
import {createOrSet, haveCommonElements, haveEveryElements} from './utils'
|
||||||
|
import {matchesRules, RegexTransformer, validateTransformer} from './regexUtils'
|
||||||
export interface RegexTransformer {
|
|
||||||
pattern: RegExp | null
|
|
||||||
target: string
|
|
||||||
onProperty?: ('title' | 'author' | 'milestone' | 'body' | 'status' | 'branch')[] | undefined
|
|
||||||
method?: 'replace' | 'match' | undefined
|
|
||||||
onEmpty?: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_MAP = new Map<string, string>()
|
const EMPTY_MAP = new Map<string, string>()
|
||||||
|
|
||||||
@@ -128,23 +121,32 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category.exhaustive === true) {
|
if (category.labels !== undefined) {
|
||||||
if (
|
if (category.exhaustive === true) {
|
||||||
haveEveryElements(
|
if (
|
||||||
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
haveEveryElements(
|
||||||
pr.labels
|
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||||
)
|
pr.labels
|
||||||
) {
|
)
|
||||||
pullRequests.push(body)
|
) {
|
||||||
matched = true
|
pullRequests.push(body)
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
haveCommonElements(
|
||||||
|
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||||
|
pr.labels
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
pullRequests.push(body)
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
if (
|
|
||||||
haveCommonElements(
|
if (category.rules !== undefined) {
|
||||||
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
if (matchesRules(category.rules, pr, category.exhaustive === true)) {
|
||||||
pr.labels
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
pullRequests.push(body)
|
pullRequests.push(body)
|
||||||
matched = true
|
matched = true
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,11 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
|||||||
if (!matched) {
|
if (!matched) {
|
||||||
// we allow to have pull requests included in an "uncategorized" category
|
// we allow to have pull requests included in an "uncategorized" category
|
||||||
for (const [category, pullRequests] of categorized) {
|
for (const [category, pullRequests] of categorized) {
|
||||||
if (category.labels.length === 0) {
|
if (
|
||||||
|
(category.labels === undefined || category.labels.length === 0) &&
|
||||||
|
category.rules === undefined &&
|
||||||
|
category.exclude_labels === undefined
|
||||||
|
) {
|
||||||
pullRequests.push(body)
|
pullRequests.push(body)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -458,40 +464,6 @@ function validateTransformers(specifiedTransformers: Transformer[]): RegexTransf
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateTransformer(transformer?: Transformer): RegexTransformer | null {
|
|
||||||
if (transformer === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let onProperty = undefined
|
|
||||||
let method = undefined
|
|
||||||
let onEmpty = undefined
|
|
||||||
if (transformer.hasOwnProperty('on_property')) {
|
|
||||||
onProperty = (transformer as Extractor).on_property
|
|
||||||
method = (transformer as Extractor).method
|
|
||||||
onEmpty = (transformer as Extractor).on_empty
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacy handling, transform single value input to array
|
|
||||||
if (!Array.isArray(onProperty)) {
|
|
||||||
if (onProperty !== undefined) {
|
|
||||||
onProperty = [onProperty]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
pattern: new RegExp(transformer.pattern.replace('\\\\', '\\'), transformer.flags ?? 'gu'),
|
|
||||||
target: transformer.target || '',
|
|
||||||
onProperty,
|
|
||||||
method,
|
|
||||||
onEmpty
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractValues(pr: PullRequestInfo, extractor: RegexTransformer, extractor_usecase: string): string[] | null {
|
function extractValues(pr: PullRequestInfo, extractor: RegexTransformer, extractor_usecase: string): string[] | null {
|
||||||
if (extractor.pattern == null) {
|
if (extractor.pattern == null) {
|
||||||
return null
|
return null
|
||||||
@@ -499,16 +471,11 @@ function extractValues(pr: PullRequestInfo, extractor: RegexTransformer, extract
|
|||||||
|
|
||||||
if (extractor.onProperty !== undefined) {
|
if (extractor.onProperty !== undefined) {
|
||||||
let results: string[] = []
|
let results: string[] = []
|
||||||
const list: ('title' | 'author' | 'milestone' | 'body' | 'status' | 'branch')[] = extractor.onProperty
|
const list: Property[] = extractor.onProperty
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
const prop = list[i]
|
const prop = list[i]
|
||||||
let value: string | undefined = pr[prop]
|
const value = retrieveProperty(pr, prop, extractor_usecase)
|
||||||
if (value === undefined) {
|
|
||||||
core.warning(`⚠️ the provided property '${extractor.onProperty}' for \`${extractor_usecase}\` is not valid`)
|
|
||||||
value = pr['body']
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = extractValuesFromString(value, extractor)
|
const values = extractValuesFromString(value, extractor)
|
||||||
if (values !== null) {
|
if (values !== null) {
|
||||||
results = results.concat(values)
|
results = results.concat(values)
|
||||||
|
|||||||
+1
-2
@@ -2,7 +2,6 @@ import * as core from '@actions/core'
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {Configuration, DefaultConfiguration} from './configuration'
|
import {Configuration, DefaultConfiguration} from './configuration'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the repository path, relatively to the GITHUB_WORKSPACE
|
* Resolves the repository path, relatively to the GITHUB_WORKSPACE
|
||||||
*/
|
*/
|
||||||
@@ -158,7 +157,7 @@ export function writeOutput(githubWorkspacePath: string, outputFile: string, cha
|
|||||||
|
|
||||||
export type Unpacked<T> = T extends (infer U)[] ? U : T
|
export type Unpacked<T> = T extends (infer U)[] ? U : T
|
||||||
|
|
||||||
export function createOrSet<T>(map: Map<String, T[]>, key: string, value: T): void {
|
export function createOrSet<T>(map: Map<string, T[]>, key: string, value: T): void {
|
||||||
const entry = map.get(key)
|
const entry = map.get(key)
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
map.set(key, [value])
|
map.set(key, [value])
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export async function wait(milliseconds: number): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (isNaN(milliseconds)) {
|
|
||||||
throw new Error('milliseconds not a number')
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => resolve('done!'), milliseconds)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user