Refactor transform.ts and others

This commit is contained in:
Bob Loblaw
2024-11-02 09:42:54 +01:00
committed by Mike Penz
parent 2e2eb040a4
commit 0ae6d98e17
12 changed files with 451 additions and 244 deletions
+2 -3
View File
@@ -1,8 +1,8 @@
name: 'CI' name: "CI"
on: on:
push: push:
tags: tags:
- '*' - "*"
pull_request: pull_request:
concurrency: concurrency:
@@ -188,4 +188,3 @@ jobs:
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-b') || contains(github.ref, '-a') }} prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-b') || contains(github.ref, '-a') }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+9 -1
View File
@@ -6,5 +6,13 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"bracketSpacing": false, "bracketSpacing": false,
"arrowParens": "avoid" "arrowParens": "avoid",
"overrides": [
{
"files": ["*.yml", "*.yaml"],
"options": {
"singleQuote": false
}
}
]
} }
+1 -1
View File
@@ -19,7 +19,7 @@ test('missing values should result in failure', () => {
} }
try { try {
cp.execSync(`node ${ip}`, options).toString() cp.execSync(`node ${ip}`, options).toString()
} catch (error: any) { } catch (error: unknown) {
expect(true).toBe(true) expect(true).toBe(true)
} }
}) })
+2 -2
View File
@@ -226,7 +226,7 @@ it('[Github] Uncategorized category', async () => {
}) })
it('[Github] Verify commit based changelog', async () => { it('[Github] Verify commit based changelog', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits.json')) const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits.json'), 'COMMIT')
const releaseNotesBuilder = new ReleaseNotesBuilder( const releaseNotesBuilder = new ReleaseNotesBuilder(
null, null,
githubRepository, githubRepository,
@@ -257,7 +257,7 @@ it('[Github] Verify commit based changelog', async () => {
}) })
it('[Github] Verify commit based changelog, with emoji categorisation', async () => { it('[Github] Verify commit based changelog, with emoji categorisation', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits_emoji.json')) const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_commits_emoji.json'), 'COMMIT')
const releaseNotesBuilder = new ReleaseNotesBuilder( const releaseNotesBuilder = new ReleaseNotesBuilder(
null, null,
githubRepository, githubRepository,
+3 -1
View File
@@ -193,6 +193,7 @@ it('Extract label from title, split regex', async () => {
on_property: 'title' on_property: 'title'
} }
] ]
expect(buildChangelogTest(configuration, mergedPullRequests, repositoryUtils)).toStrictEqual( expect(buildChangelogTest(configuration, mergedPullRequests, repositoryUtils)).toStrictEqual(
`## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n` `## 🚀 Features\n\n- [Feature][AB-1234] - this is a PR 1 title message\n - PR: #1\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n- [Issue][Feature][AB-1234321] - this is a PR 3 title message\n - PR: #3\n\n`
) )
@@ -448,7 +449,8 @@ it('Use empty_content for empty category', async () => {
labels: ['Feature'] labels: ['Feature']
} }
] ]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)).toStrictEqual( const changelog = buildChangelogTest(customConfig, pullRequestsWithLabels, repositoryUtils)
expect(changelog).toStrictEqual(
`## 🚀 Features and 🐛 Issues\n\n- No PRs in this category\n\n## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n` `## 🚀 Features and 🐛 Issues\n\n- No PRs in this category\n\n## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
) )
}) })
+7 -24
View File
@@ -1,11 +1,6 @@
import {Extractor, PullConfiguration, Regex, Rule, Sort} from './pr-collector/types' import {Extractor, PullConfiguration, Regex, Rule} from './pr-collector/types'
export interface Configuration extends PullConfiguration { export interface Configuration extends PullConfiguration {
max_tags_to_fetch: number
max_pull_requests: number
max_back_track_time_days: number
exclude_merge_branches: string[]
sort: Sort | string // "ASC" or "DESC"
template: string template: string
pr_template: string pr_template: string
empty_template: string empty_template: string
@@ -15,8 +10,6 @@ export interface Configuration extends PullConfiguration {
duplicate_filter?: Extractor // extract an identifier from a PR used to detect duplicates, will keep the last match (depends on `sort`) 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. 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: Regex[] transformers: Regex[]
tag_resolver: TagResolver
base_branches: string[]
custom_placeholders?: Placeholder[] custom_placeholders?: Placeholder[]
trim_values: boolean trim_values: boolean
} }
@@ -63,6 +56,8 @@ export interface Placeholder {
transformer: Regex // the transformer to use to transform the original placeholder into the custom placeheolder transformer: Regex // the transformer to use to transform the original placeholder into the custom placeheolder
} }
export class PlaceholderGroup extends Map<string, Placeholder[]> {}
export const DefaultConfiguration: Configuration = { export const DefaultConfiguration: Configuration = {
max_tags_to_fetch: 200, // the amount of tags to fetch from the github API max_tags_to_fetch: 200, // the amount of tags to fetch from the github API
max_pull_requests: 200, // the amount of pull requests to process max_pull_requests: 200, // the amount of pull requests to process
@@ -110,14 +105,8 @@ export const DefaultConfiguration: Configuration = {
} }
export const DefaultCommitConfiguration: Configuration = { export const DefaultCommitConfiguration: Configuration = {
max_tags_to_fetch: DefaultConfiguration.max_tags_to_fetch, ...DefaultConfiguration,
max_pull_requests: DefaultConfiguration.max_pull_requests, pr_template: '- #{{TITLE}}', // the per PR template to pick
max_back_track_time_days: DefaultConfiguration.max_back_track_time_days,
exclude_merge_branches: DefaultConfiguration.exclude_merge_branches,
sort: DefaultConfiguration.sort,
template: '#{{CHANGELOG}}', // the global template to host the changelog
pr_template: '- #{{TITLE}}', // the per PR template to pick for commit based mode
empty_template: DefaultConfiguration.empty_template,
categories: [ categories: [
{ {
title: '## 🚀 Features', title: '## 🚀 Features',
@@ -135,17 +124,11 @@ export const DefaultCommitConfiguration: Configuration = {
title: '## 📦 Other', title: '## 📦 Other',
labels: [] labels: []
} }
], // the categories to support for the ordering ],
ignore_labels: DefaultConfiguration.ignore_labels,
label_extractor: [ label_extractor: [
{ {
pattern: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)', pattern: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)',
target: '$1' target: '$1'
} }
], ]
transformers: DefaultConfiguration.transformers,
tag_resolver: DefaultConfiguration.tag_resolver,
base_branches: DefaultConfiguration.base_branches,
custom_placeholders: DefaultConfiguration.custom_placeholders,
trim_values: DefaultConfiguration.trim_values
} }
+8 -4
View File
@@ -28,6 +28,10 @@ export interface PullRequestInfo {
status: 'open' | 'merged' status: 'open' | 'merged'
} }
export interface PullRequestData extends PullRequestInfo {
childPrs?: PullRequestInfo[]
}
export interface CommentInfo { export interface CommentInfo {
id: number id: number
htmlURL: string htmlURL: string
@@ -79,7 +83,7 @@ export class PullRequests {
) {} ) {}
async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> { async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
return sortPrs(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests)) return sortPrsByMergedAt(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests))
} }
async getBetweenDates( async getBetweenDates(
@@ -89,11 +93,11 @@ export class PullRequests {
toDate: moment.Moment, toDate: moment.Moment,
maxPullRequests: number maxPullRequests: number
): Promise<PullRequestInfo[]> { ): Promise<PullRequestInfo[]> {
return sortPrs(await this.repositoryUtils.getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests)) return sortPrsByMergedAt(await this.repositoryUtils.getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests))
} }
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> { async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
return sortPrs(await this.repositoryUtils.getOpen(owner, repo, maxPullRequests)) return sortPrsByMergedAt(await this.repositoryUtils.getOpen(owner, repo, maxPullRequests))
} }
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> { async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
@@ -221,7 +225,7 @@ export class PullRequests {
} }
} }
function sortPrs(pullRequests: PullRequestInfo[]): PullRequestInfo[] { function sortPrsByMergedAt(pullRequests: PullRequestInfo[]): PullRequestInfo[] {
return sortPullRequests(pullRequests, { return sortPullRequests(pullRequests, {
order: 'ASC', order: 'ASC',
on_property: 'mergedAt' on_property: 'mergedAt'
+17
View File
@@ -58,3 +58,20 @@ export interface RegexTransformer {
method?: 'replace' | 'replaceAll' | 'match' | 'regexr' method?: 'replace' | 'replaceAll' | 'match' | 'regexr'
onEmpty?: string onEmpty?: string
} }
export class GroupedTemplateContext extends Map<string, string[]> {}
export class TemplateContext extends Map<string, string> {}
export type ChangelogStrings = {
categorized: string
uncategorized: string
open: string
ignored: string
}
export type PrStrings = {
categorizedList: string[]
uncategorizedList: string[]
openList: string[]
ignoredList: string[]
}
+2 -2
View File
@@ -6,8 +6,8 @@ import {validateRegex} from './pr-collector/regexUtils'
/** /**
* Checks if any of the rules match the given PR * Checks if any of the rules match the given PR
*/ */
export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: Boolean): boolean { export function matchesRules(rules: Rule[], pr: PullRequestInfo, exhaustive: boolean): boolean {
const transformers: RegexTransformer[] = rules.map(rule => validateRegex(rule)).filter(t => t !== null) as RegexTransformer[] const transformers: RegexTransformer[] = rules.map(rule => validateRegex(rule)).filter(t => t !== null)
if (exhaustive) { if (exhaustive) {
return transformers.every(transformer => { return transformers.every(transformer => {
return matches(pr, transformer, 'rule') return matches(pr, transformer, 'rule')
+2 -2
View File
@@ -1,12 +1,12 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Configuration} from './configuration' import {Configuration} from './configuration'
import {checkExportedData, writeCacheData} from './utils' import {checkExportedData, writeCacheData} from './utils'
import {buildChangelog, PullRequestData} from './transform' import {buildChangelog} from './transform'
import {PullRequestCollector} from './pr-collector/prCollector' import {PullRequestCollector} from './pr-collector/prCollector'
import {failOrError} from './pr-collector/utils' import {failOrError} from './pr-collector/utils'
import {TagInfo} from './pr-collector/tags' import {TagInfo} from './pr-collector/tags'
import {DiffInfo} from './pr-collector/commits' import {DiffInfo} from './pr-collector/commits'
import {PullRequestInfo} from './pr-collector/pullRequests' import {PullRequestData, PullRequestInfo} from './pr-collector/pullRequests'
import {BaseRepository} from './repositories/BaseRepository' import {BaseRepository} from './repositories/BaseRepository'
export interface ReleaseNotesOptions { export interface ReleaseNotesOptions {
+366 -197
View File
@@ -1,38 +1,34 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Category, Configuration, Placeholder, Property} from './configuration' import {Category, Configuration, Placeholder, PlaceholderGroup, Property} from './configuration'
import {createOrSet, haveCommonElementsArr, haveEveryElementsArr} from './utils' import {createOrSet, groupPlaceholders, haveCommonElementsArr, haveEveryElementsArr, mergeMaps} from './utils'
import { import {
CommentInfo, CommentInfo,
EMPTY_COMMENT_INFO, EMPTY_COMMENT_INFO,
EMPTY_PULL_REQUEST_INFO, EMPTY_PULL_REQUEST_INFO,
PullRequestData,
PullRequestInfo, PullRequestInfo,
retrieveProperty, retrieveProperty,
sortPullRequests sortPullRequests
} from './pr-collector/pullRequests' } from './pr-collector/pullRequests'
import {DiffInfo} from './pr-collector/commits' import {DiffInfo} from './pr-collector/commits'
import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils' import {transformStringToOptionalValue, transformStringToValues, validateRegex} from './pr-collector/regexUtils'
import {Regex, RegexTransformer} from './pr-collector/types' import {ChangelogStrings, GroupedTemplateContext, PrStrings, Regex, RegexTransformer, TemplateContext} from './pr-collector/types'
import {ReleaseNotesOptions} from './releaseNotesBuilder' import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {matchesRules} from './regexUtils' import {matchesRules} from './regexUtils'
const EMPTY_MAP = new Map<string, string>()
let CLEAR = false let CLEAR = false
export function clear(): void { export function clear(): void {
CLEAR = true CLEAR = true
} }
export interface PullRequestData extends PullRequestInfo {
childPrs?: PullRequestInfo[]
}
export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], options: ReleaseNotesOptions): string { export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], options: ReleaseNotesOptions): string {
core.startGroup('📦 Build changelog') core.startGroup('📦 Build changelog')
let prs: PullRequestData[] = origPrs let prs: PullRequestData[] = origPrs
if (prs.length === 0) { if (prs.length === 0) {
core.warning(`⚠️ No pull requests found`) core.warning(`⚠️ No pull requests found`)
const result = replaceEmptyTemplate(options.configuration.empty_template, options) const result = renderEmptyChangelogTemplate(options.configuration.empty_template, options)
core.endGroup() core.endGroup()
return result return result
} }
@@ -123,31 +119,32 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
} }
} }
// keep reference for the placeholder values const groupedPlaceholders = groupPlaceholders(config.custom_placeholders || [])
const placeholders = new Map<string, Placeholder[]>()
for (const ph of config.custom_placeholders || []) { const customPlaceholdersTemplateContext = new GroupedTemplateContext()
createOrSet(placeholders, ph.source, ph)
}
const placeholderPrMap = new Map<string, string[]>()
const validatedTransformers = validateTransformers(config.transformers) const validatedTransformers = validateTransformers(config.transformers)
const transformedMap = new Map<PullRequestInfo, string>()
// convert PRs to their text representation
for (const pr of prs) {
transformedMap.set(pr, transform(fillPrTemplate(pr, config.pr_template, placeholders, placeholderPrMap, config), validatedTransformers))
}
core.info(`️ Used ${validatedTransformers.length} transformers to adjust message`) core.info(`️ Used ${validatedTransformers.length} transformers to adjust message`)
core.info(`✒️ Wrote messages for ${prs.length} pull requests`)
for (const pr of prs) {
const prAsObject = pr as unknown as Record<string, unknown>
transformObject(prAsObject, validatedTransformers)
}
core.info(`✒️ Wrote messages for ${prs.length} commits`)
const prInfoMap = buildInfoMapAndFillPlaceholderContext(
prs,
config.pr_template,
groupedPlaceholders,
customPlaceholdersTemplateContext,
config
)
// bring PRs into the order of categories // bring PRs into the order of categories
const categories = config.categories const categories = config.categories
const ignoredLabels = config.ignore_labels
const flatCategories = flatten(config.categories) const flatCategories = flatten(config.categories)
const categorizedPrs: string[] = []
const ignoredPrs: string[] = []
const openPrs: string[] = []
const uncategorizedPrs: string[] = []
// set-up the category object // set-up the category object
for (const category of flatCategories) { for (const category of flatCategories) {
@@ -156,8 +153,109 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
} }
} }
const prStrings = buildPrStringsAndFillCategoryEntries(prInfoMap, config.ignore_labels, categories, flatCategories)
core.info(`️ Ordered all pull requests into ${categories.length} categories`)
// serialize and provide the categorized content as json
const transformedCategorized = buildCategorizedOutput(flatCategories)
core.setOutput('categorized', JSON.stringify(transformedCategorized))
// construct final changelog
const changelogStrings = buildChangelogStrings(flatCategories, prStrings)
core.info(`✒️ Wrote ${changelogStrings.categorized.length} categorized pull requests down`)
core.info(`✒️ Wrote ${changelogStrings.uncategorized.length} non categorized pull requests down`)
core.info(`✒️ Wrote ${changelogStrings.open.length} open pull requests down`)
core.info(`✒️ Wrote ${changelogStrings.ignored.length} ignored pull requests down`)
core.setOutput('categorized_prs', changelogStrings.categorized.length)
core.setOutput('uncategorized_prs', changelogStrings.uncategorized.length)
core.setOutput('open_prs', changelogStrings.open.length)
core.setOutput('ignored_prs', changelogStrings.ignored.length)
// collect all contributors
const contributorsSet: Set<string> = new Set(prs.map(pr => `@${pr.author}`))
const contributorsArray = Array.from(contributorsSet)
const contributorsString = contributorsArray.join(', ')
const externalContributorString = contributorsArray.filter(value => value !== options.owner).join(', ')
core.setOutput('contributors', JSON.stringify(contributorsSet))
const releaseNotesTemplateContext = buildReleaseNotesTemplateContext(
changelogStrings,
contributorsString,
externalContributorString,
prStrings,
diffInfo,
options
)
let renderedReleaseNotesTemplate = renderTemplateAndFillPlaceholderContext(
config.template,
releaseNotesTemplateContext,
groupedPlaceholders,
customPlaceholdersTemplateContext,
config
)
renderedReleaseNotesTemplate = renderTemplateWithContext(renderedReleaseNotesTemplate, customPlaceholdersTemplateContext, config)
renderedReleaseNotesTemplate = cleanupPrPlaceholders(renderedReleaseNotesTemplate, groupedPlaceholders)
renderedReleaseNotesTemplate = cleanupPlaceholders(renderedReleaseNotesTemplate)
core.info(`️ Filled template`)
core.endGroup()
return renderedReleaseNotesTemplate
}
function buildInfoMapAndFillPlaceholderContext(
prData: PullRequestData[],
template: string,
groupedPlaceholders: Map<string, Placeholder[]>,
customPlaceholdersTemplateContext: GroupedTemplateContext,
config: Configuration
): Map<PullRequestInfo, string> {
const infoMap = new Map<PullRequestInfo, string>()
for (const pr of prData) {
const [prTemplateContext, prArrayTemplateContext] = buildPrTemplateContext(pr)
let renderedTemplate = template
renderedTemplate = renderTemplateAndFillPlaceholderContext(
renderedTemplate,
prArrayTemplateContext,
groupedPlaceholders,
customPlaceholdersTemplateContext,
config
)
renderedTemplate = renderTemplateAndFillPlaceholderContext(
renderedTemplate,
prTemplateContext,
groupedPlaceholders,
customPlaceholdersTemplateContext,
config
)
infoMap.set(pr, renderedTemplate)
}
return infoMap
}
function buildPrStringsAndFillCategoryEntries(
prInfoMap: Map<PullRequestInfo, string>,
ignoredLabels: string[],
categories: Category[],
flatCategories: Category[]
): PrStrings {
const categorizedPrs: string[] = []
const ignoredPrs: string[] = []
const openPrs: string[] = []
const uncategorizedPrs: string[] = []
// bring elements in order // bring elements in order
prLoop: for (const [pr, body] of transformedMap) { prLoop: for (const [pr, body] of prInfoMap) {
if ( if (
haveCommonElementsArr( haveCommonElementsArr(
ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')), ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')),
@@ -184,7 +282,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
if (!matchedOnce) { if (!matchedOnce) {
// 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 of flatCategories) { for (const category of flatCategories) {
const pullRequests = category.entries || [] category.entries = category.entries || []
if ((category.labels === undefined || category.labels.length === 0) && category.rules === undefined) { if ((category.labels === undefined || category.labels.length === 0) && category.rules === undefined) {
// check if any exclude label matches for the "uncategorized" category // check if any exclude label matches for the "uncategorized" category
if (category.exclude_labels !== undefined) { if (category.exclude_labels !== undefined) {
@@ -194,7 +292,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
pr.labels pr.labels
) )
) { ) {
pullRequests.push(body) category.entries.push(body)
} else if (core.isDebug()) { } else if (core.isDebug()) {
const excludeLabels = JSON.stringify(category.exclude_labels) const excludeLabels = JSON.stringify(category.exclude_labels)
core.debug( core.debug(
@@ -202,7 +300,7 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
) )
} }
} else { } else {
pullRequests.push(body) category.entries.push(body)
} }
break break
@@ -215,106 +313,126 @@ export function buildChangelog(diffInfo: DiffInfo, origPrs: PullRequestInfo[], o
categorizedPrs.push(body) categorizedPrs.push(body)
} }
} }
core.info(`️ Ordered all pull requests into ${categories.length} categories`)
// serialize and provide the categorized content as json const prStrings: PrStrings = {
const transformedCategorized = {} categorizedList: categorizedPrs,
for (const category of flatCategories) { uncategorizedList: uncategorizedPrs,
Object.assign(transformedCategorized, {[category.key || category.title]: category.entries}) openList: openPrs,
ignoredList: ignoredPrs
} }
core.setOutput('categorized', JSON.stringify(transformedCategorized))
// construct final changelog return prStrings
let changelog = '' }
function buildChangelogStrings(flatCategories: Category[], prStrings: PrStrings): ChangelogStrings {
const {categorizedList, uncategorizedList, openList, ignoredList} = prStrings
let changelogCategorized = ''
for (const category of flatCategories) { for (const category of flatCategories) {
const pullRequests = category.entries || [] const pullRequests = category.entries || []
changelog = attachCategoryChangelog(changelog, category, pullRequests) changelogCategorized += buildCategorizedChangelogString(category, pullRequests)
} }
core.info(`✒️ Wrote ${categorizedPrs.length} categorized pull requests down`)
if (core.isDebug()) { if (core.isDebug()) {
for (const pr of categorizedPrs) { for (const pr of categorizedList) {
core.debug(` ${pr}`) core.debug(` ${pr}`)
} }
} }
core.setOutput('categorized_prs', categorizedPrs.length)
let changelogUncategorized = '' let changelogUncategorized = ''
for (const pr of uncategorizedPrs) { for (const pr of uncategorizedList) {
changelogUncategorized = `${changelogUncategorized + pr}\n` changelogUncategorized = `${changelogUncategorized + pr}\n`
} }
core.info(`✒️ Wrote ${uncategorizedPrs.length} non categorized pull requests down`)
if (core.isDebug()) { if (core.isDebug()) {
for (const pr of uncategorizedPrs) { for (const pr of uncategorizedList) {
core.debug(` ${pr}`) core.debug(` ${pr}`)
} }
} }
core.setOutput('uncategorized_prs', uncategorizedPrs.length)
let changelogOpen = '' let changelogOpen = ''
if (openPrs.length > 0) { if (openList.length > 0) {
for (const pr of openPrs) { for (const pr of openList) {
changelogOpen = `${changelogOpen + pr}\n` changelogOpen = `${changelogOpen + pr}\n`
} }
core.info(`✒️ Wrote ${openPrs.length} open pull requests down`)
if (core.isDebug()) { if (core.isDebug()) {
for (const pr of openPrs) { for (const pr of openList) {
core.debug(` ${pr}`) core.debug(` ${pr}`)
} }
} }
core.setOutput('open_prs', openPrs.length)
} }
let changelogIgnored = '' let changelogIgnored = ''
for (const pr of ignoredPrs) { for (const pr of ignoredList) {
changelogIgnored = `${changelogIgnored + pr}\n` changelogIgnored = `${changelogIgnored + pr}\n`
} }
if (core.isDebug()) { if (core.isDebug()) {
for (const pr of ignoredPrs) { for (const pr of ignoredList) {
core.debug(` ${pr}`) core.debug(` ${pr}`)
} }
} }
core.info(`✒️ Wrote ${ignoredPrs.length} ignored pull requests down`)
// collect all contributors const changelogStrings: ChangelogStrings = {
const contributorsSet: Set<String> = new Set() categorized: changelogCategorized,
for (const pr of prs) { uncategorized: changelogUncategorized,
contributorsSet.add(`@${pr.author}`) open: changelogOpen,
ignored: changelogIgnored
} }
const contributorsArray = Array.from(contributorsSet)
const contributorsString = contributorsArray.join(', ')
const externalContributorString = contributorsArray.filter(value => value !== options.owner).join(', ')
core.setOutput('contributors', JSON.stringify(contributorsSet))
// fill template return changelogStrings
const placeholderMap = new Map<string, string>() }
placeholderMap.set('CHANGELOG', changelog)
placeholderMap.set('UNCATEGORIZED', changelogUncategorized) function buildReleaseNotesTemplateContext(
placeholderMap.set('OPEN', changelogOpen) changelogStrings: ChangelogStrings,
placeholderMap.set('IGNORED', changelogIgnored) contributorsString: string,
externalContributorString: string,
prStrings: PrStrings,
diffInfo: DiffInfo,
options: ReleaseNotesOptions
): TemplateContext {
const {
categorized: changelogCategorized,
uncategorized: changelogUncategorized,
open: changelogOpen,
ignored: changelogIgnored
} = changelogStrings
const {categorizedList, uncategorizedList, openList, ignoredList} = prStrings
let releaseNotesTemplateContext = new TemplateContext()
releaseNotesTemplateContext.set('CHANGELOG', changelogCategorized)
releaseNotesTemplateContext.set('UNCATEGORIZED', changelogUncategorized)
releaseNotesTemplateContext.set('OPEN', changelogOpen)
releaseNotesTemplateContext.set('IGNORED', changelogIgnored)
// fill special collected contributors // fill special collected contributors
placeholderMap.set('CONTRIBUTORS', contributorsString) releaseNotesTemplateContext.set('CONTRIBUTORS', contributorsString)
placeholderMap.set('EXTERNAL_CONTRIBUTORS', externalContributorString) releaseNotesTemplateContext.set('EXTERNAL_CONTRIBUTORS', externalContributorString)
// fill other placeholders // fill other placeholders
placeholderMap.set('CATEGORIZED_COUNT', categorizedPrs.length.toString()) releaseNotesTemplateContext.set('CATEGORIZED_COUNT', categorizedList.length.toString())
placeholderMap.set('UNCATEGORIZED_COUNT', uncategorizedPrs.length.toString()) releaseNotesTemplateContext.set('UNCATEGORIZED_COUNT', uncategorizedList.length.toString())
placeholderMap.set('OPEN_COUNT', openPrs.length.toString()) releaseNotesTemplateContext.set('OPEN_COUNT', openList.length.toString())
placeholderMap.set('IGNORED_COUNT', ignoredPrs.length.toString()) releaseNotesTemplateContext.set('IGNORED_COUNT', ignoredList.length.toString())
// code change placeholders // code change placeholders
placeholderMap.set('CHANGED_FILES', diffInfo.changedFiles.toString()) releaseNotesTemplateContext.set('CHANGED_FILES', diffInfo.changedFiles.toString())
placeholderMap.set('ADDITIONS', diffInfo.additions.toString()) releaseNotesTemplateContext.set('ADDITIONS', diffInfo.additions.toString())
placeholderMap.set('DELETIONS', diffInfo.deletions.toString()) releaseNotesTemplateContext.set('DELETIONS', diffInfo.deletions.toString())
placeholderMap.set('CHANGES', diffInfo.changes.toString()) releaseNotesTemplateContext.set('CHANGES', diffInfo.changes.toString())
placeholderMap.set('COMMITS', diffInfo.commits.toString()) releaseNotesTemplateContext.set('COMMITS', diffInfo.commits.toString())
fillAdditionalPlaceholders(options, placeholderMap)
let transformedChangelog = config.template const coreReleasesNotesContext = buildCoreReleaseNotesTemplateContext(options)
transformedChangelog = replacePlaceholders(transformedChangelog, EMPTY_MAP, placeholderMap, placeholders, placeholderPrMap, config)
transformedChangelog = replacePrPlaceholders(transformedChangelog, placeholderPrMap, config) releaseNotesTemplateContext = mergeMaps(releaseNotesTemplateContext, coreReleasesNotesContext)
transformedChangelog = cleanupPrPlaceholders(transformedChangelog, placeholders)
transformedChangelog = cleanupPlaceholders(transformedChangelog) return releaseNotesTemplateContext
core.info(`️ Filled template`) }
core.endGroup()
return transformedChangelog function buildCategorizedOutput(flatCategories: Category[]): Record<string, string[]> {
const transformedCategorized = {}
for (const category of flatCategories) {
Object.assign(transformedCategorized, {[category.key || category.title]: category.entries})
}
return transformedCategorized
} }
function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: string): boolean[] { function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: string): boolean[] {
@@ -334,10 +452,11 @@ function recursiveCategorizePr(category: Category, pr: PullRequestInfo, body: st
// if consumed we don't handle it anymore, as it was matched in a child, don't handle anymore // if consumed we don't handle it anymore, as it was matched in a child, don't handle anymore
if (!consumed && !matched) { if (!consumed && !matched) {
const pullRequests = category.entries || [] category.entries = category.entries || []
matched = matchesParent matched = matchesParent
if (matched) { if (matched) {
pullRequests.push(body) // if matched add the PR to the list category.entries.push(body) // if matched add the PR to the list
} }
} }
if (matched && category.consume) { if (matched && category.consume) {
@@ -401,156 +520,185 @@ function categorizePr(category: Category, pr: PullRequestInfo): boolean {
return matched return matched
} }
function attachCategoryChangelog(changelog: string, category: Category, pullRequests: string[]): string { function buildCategorizedChangelogString(category: Category, pullRequests: string[]): string {
let categorizedString = ''
if (pullRequests.length > 0 || hasChildWithEntries(category)) { if (pullRequests.length > 0 || hasChildWithEntries(category)) {
if (category.title) { if (category.title) {
changelog = `${changelog + category.title}\n\n` categorizedString = `${categorizedString + category.title}\n\n`
} }
for (const pr of pullRequests) { for (const pr of pullRequests) {
changelog = `${changelog + pr}\n` categorizedString = `${categorizedString + pr}\n`
} }
changelog = `${changelog}\n` // add space between sections categorizedString = `${categorizedString}\n` // add space between sections
} else if (category.empty_content !== undefined) { } else if (category.empty_content !== undefined) {
if (category.title) { if (category.title) {
changelog = `${changelog + category.title}\n\n` categorizedString = `${categorizedString + category.title}\n\n`
} }
changelog = `${changelog + category.empty_content}\n\n` categorizedString = `${categorizedString + category.empty_content}\n\n`
} }
return changelog return categorizedString
} }
export function replaceEmptyTemplate(template: string, options: ReleaseNotesOptions): string { export function renderEmptyChangelogTemplate(template: string, options: ReleaseNotesOptions): string {
const placeholders = new Map<string, Placeholder[]>() const placeholders = new Map<string, Placeholder[]>()
for (const ph of options.configuration.custom_placeholders || []) { for (const ph of options.configuration.custom_placeholders || []) {
createOrSet(placeholders, ph.source, ph) createOrSet(placeholders, ph.source, ph)
} }
const placeholderMap = new Map<string, string>()
fillAdditionalPlaceholders(options, placeholderMap) const releaseNotesTemplateContext = buildCoreReleaseNotesTemplateContext(options)
return replacePlaceholders(template, EMPTY_MAP, placeholderMap, placeholders, undefined, options.configuration)
const renderedEmptyChangelogTemplate = renderTemplateAndFillPlaceholderContext(
template,
releaseNotesTemplateContext,
placeholders,
undefined,
options.configuration
)
return renderedEmptyChangelogTemplate
} }
function fillAdditionalPlaceholders( function buildCoreReleaseNotesTemplateContext(options: ReleaseNotesOptions): TemplateContext {
options: ReleaseNotesOptions, const templateContext = new TemplateContext()
placeholderMap: Map<string, string> /* placeholderKey and original value */
): void { templateContext.set('OWNER', options.owner)
placeholderMap.set('OWNER', options.owner) templateContext.set('REPO', options.repo)
placeholderMap.set('REPO', options.repo) templateContext.set('FROM_TAG', options.fromTag.name)
placeholderMap.set('FROM_TAG', options.fromTag.name) templateContext.set('FROM_TAG_DATE', options.fromTag.date?.toISOString() || '')
placeholderMap.set('FROM_TAG_DATE', options.fromTag.date?.toISOString() || '') templateContext.set('TO_TAG', options.toTag.name)
placeholderMap.set('TO_TAG', options.toTag.name) templateContext.set('TO_TAG_DATE', options.toTag.date?.toISOString() || '')
placeholderMap.set('TO_TAG_DATE', options.toTag.date?.toISOString() || '')
const fromDate = options.fromTag.date const fromDate = options.fromTag.date
const toDate = options.toTag.date const toDate = options.toTag.date
if (fromDate !== undefined && toDate !== undefined) { if (fromDate !== undefined && toDate !== undefined) {
placeholderMap.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '') templateContext.set('DAYS_SINCE', toDate.diff(fromDate, 'days').toString() || '')
} else { } else {
placeholderMap.set('DAYS_SINCE', '') templateContext.set('DAYS_SINCE', '')
} }
placeholderMap.set( templateContext.set(
'RELEASE_DIFF', 'RELEASE_DIFF',
`${options.repositoryUtils.homeUrl}/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}` `${options.repositoryUtils.homeUrl}/${options.owner}/${options.repo}/compare/${options.fromTag.name}...${options.toTag.name}`
) )
return templateContext
} }
function fillPrTemplate( function buildPrTemplateContext(pr: PullRequestData): [TemplateContext, TemplateContext] {
pr: PullRequestData, const prTemplateContext = new TemplateContext()
template: string,
placeholders: Map<string, Placeholder[]> /* placeholders to apply */, prTemplateContext.set('NUMBER', pr.number.toString())
placeholderPrMap: Map<string, string[]> /* map to keep replaced placeholder values with their key */, prTemplateContext.set('TITLE', pr.title)
configuration: Configuration prTemplateContext.set('URL', pr.htmlURL)
): string { prTemplateContext.set('STATUS', pr.status)
const arrayPlaceholderMap = new Map<string, string>() prTemplateContext.set('CREATED_AT', pr.createdAt.toISOString())
fillReviewPlaceholders(arrayPlaceholderMap, 'REVIEWS', pr.reviews || []) prTemplateContext.set('MERGED_AT', pr.mergedAt?.toISOString() || '')
fillChildPrPlaceholders(arrayPlaceholderMap, 'REFERENCED', pr.childPrs || []) prTemplateContext.set('MERGE_SHA', pr.mergeCommitSha)
const placeholderMap = new Map<string, string>() prTemplateContext.set('AUTHOR', pr.author)
placeholderMap.set('NUMBER', pr.number.toString()) prTemplateContext.set('AUTHOR_NAME', pr.authorName || '')
placeholderMap.set('TITLE', pr.title) prTemplateContext.set('LABELS', [...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || '')
placeholderMap.set('URL', pr.htmlURL) prTemplateContext.set('MILESTONE', pr.milestone || '')
placeholderMap.set('STATUS', pr.status) prTemplateContext.set('BODY', pr.body)
placeholderMap.set('CREATED_AT', pr.createdAt.toISOString()) prTemplateContext.set('ASSIGNEES', pr.assignees?.join(', ') || '')
placeholderMap.set('MERGED_AT', pr.mergedAt?.toISOString() || '') prTemplateContext.set('REVIEWERS', pr.requestedReviewers?.join(', ') || '')
placeholderMap.set('MERGE_SHA', pr.mergeCommitSha) prTemplateContext.set('APPROVERS', pr.approvedReviewers?.join(', ') || '')
placeholderMap.set('AUTHOR', pr.author) prTemplateContext.set('BRANCH', pr.branch || '')
placeholderMap.set('AUTHOR_NAME', pr.authorName || '') prTemplateContext.set('BASE_BRANCH', pr.baseBranch)
placeholderMap.set('LABELS', [...pr.labels]?.filter(l => !l.startsWith('--rcba-'))?.join(', ') || '')
placeholderMap.set('MILESTONE', pr.milestone || '') const prArrayTemplateContext = new TemplateContext()
placeholderMap.set('BODY', pr.body) fillReviewPlaceholders(prArrayTemplateContext, 'REVIEWS', pr.reviews || [])
fillArrayPlaceholders(arrayPlaceholderMap, 'ASSIGNEES', pr.assignees || []) fillChildPrPlaceholders(prArrayTemplateContext, 'REFERENCED', pr.childPrs || [])
placeholderMap.set('ASSIGNEES', pr.assignees?.join(', ') || '') fillArrayPlaceholders(prArrayTemplateContext, 'ASSIGNEES', pr.assignees || [])
fillArrayPlaceholders(arrayPlaceholderMap, 'REVIEWERS', pr.requestedReviewers || []) fillArrayPlaceholders(prArrayTemplateContext, 'REVIEWERS', pr.requestedReviewers || [])
placeholderMap.set('REVIEWERS', pr.requestedReviewers?.join(', ') || '') fillArrayPlaceholders(prArrayTemplateContext, 'APPROVERS', pr.approvedReviewers || [])
fillArrayPlaceholders(arrayPlaceholderMap, 'APPROVERS', pr.approvedReviewers || [])
placeholderMap.set('APPROVERS', pr.approvedReviewers?.join(', ') || '') return [prTemplateContext, prArrayTemplateContext]
placeholderMap.set('BRANCH', pr.branch || '')
placeholderMap.set('BASE_BRANCH', pr.baseBranch)
return replacePlaceholders(template, arrayPlaceholderMap, placeholderMap, placeholders, placeholderPrMap, configuration)
} }
function replacePlaceholders( function renderTemplateAndFillPlaceholderContext(
template: string, template: string,
arrayPlaceholderMap: Map<string, string> /* arrayPlaceholderKey and original value */, templateContext: TemplateContext /* placeholderKey and original value */,
placeholderMap: Map<string, string> /* placeholderKey and original value */, customPlaceholders: PlaceholderGroup /* placeholders to apply */,
placeholders: Map<string, Placeholder[]> /* placeholders to apply */, customPlaceholdersTemplateContext: GroupedTemplateContext | undefined /* map to keep replaced placeholder values with their key */,
placeholderPrMap: Map<string, string[]> | undefined /* map to keep replaced placeholder values with their key */,
configuration: Configuration configuration: Configuration
): string { ): string {
let transformed = template let transformed = template
// replace array placeholders first const trimValues = configuration.trim_values
for (const [key, value] of arrayPlaceholderMap) {
transformed = handlePlaceholder(transformed, key, value, placeholders, placeholderPrMap, configuration)
}
// replace traditional placeholders // replace traditional placeholders
for (const [key, value] of placeholderMap) { for (const [key, value] of templateContext) {
transformed = handlePlaceholder(transformed, key, value, placeholders, placeholderPrMap, configuration) transformed = transformed.replaceAll(`#{{${key}}}`, trimValues ? value.trim() : value)
const extractedValues = extractPlaceholderValuesAndFillPlaceholderContext(
key,
value,
customPlaceholders,
customPlaceholdersTemplateContext
)
for (const [placeholderName, extractedValue] of extractedValues) {
transformed = transformed.replaceAll(`#{{${placeholderName}}}`, trimValues ? extractedValue.trim() : extractedValue)
}
} }
return transformed return transformed
} }
function handlePlaceholder( function extractPlaceholderValuesAndFillPlaceholderContext(
template: string,
key: string, key: string,
value: string, value: string,
placeholders: Map<string, Placeholder[]> /* placeholders to apply */, customPlaceholders: PlaceholderGroup,
placeholderPrMap: Map<string, string[]> | undefined /* map to keep replaced placeholder values with their key */, customPlaceholdersTemplateContext: GroupedTemplateContext | undefined
configuration: Configuration ): TemplateContext {
): string { // Replace custom placeholders
let transformed = template.replaceAll(`#{{${key}}}`, configuration.trim_values ? value.trim() : value) const placeholdersForKey = customPlaceholders.get(key)
// replace custom placeholders
const phs = placeholders.get(key)
if (phs) {
for (const placeholder of phs) {
const transformer = validateRegex(placeholder.transformer)
if (transformer?.pattern) {
const extractedValue = transformStringToOptionalValue(value, transformer)
// note: `.replace` will return the full string again if there was no match
// note: This is mostly backwards compatibility
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
if (placeholderPrMap) {
createOrSet(placeholderPrMap, placeholder.name, extractedValue)
}
transformed = transformed.replaceAll(
`#{{${placeholder.name}}}`,
configuration.trim_values ? extractedValue.trim() : extractedValue
)
if (core.isDebug()) { const extractedValues = new TemplateContext()
core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`)
} if (!placeholdersForKey) {
} else if (core.isDebug() && extractedValue === value) { return extractedValues
core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`) }
}
for (const placeholder of placeholdersForKey) {
const extractedValue = extractTransformedValue(value, placeholder)
if (extractedValue) {
extractedValues.set(placeholder.name, extractedValue)
if (customPlaceholdersTemplateContext) {
createOrSet(customPlaceholdersTemplateContext, placeholder.name, extractedValue)
} }
} }
} }
return transformed
return extractedValues
}
function extractTransformedValue(value: string, placeholder: Placeholder): string | undefined {
const transformer = validateRegex(placeholder.transformer)
if (!transformer?.pattern) {
return undefined
}
const extractedValue = transformStringToOptionalValue(value, transformer)
if (extractedValue && ((transformer.method && transformer.method !== 'replace') || extractedValue !== value)) {
if (core.isDebug()) {
core.debug(` Custom Placeholder successfully matched data - ${extractedValue} (${placeholder.name})`)
}
return extractedValue
}
if (core.isDebug() && extractedValue === value) {
core.debug(` Custom Placeholder did result in the full original value returned. Skipping. (${placeholder.name})`)
}
return undefined
} }
function fillArrayPlaceholders( function fillArrayPlaceholders(
placeholderMap: Map<string, string> /* placeholderKey and original value */, placeholderMap: TemplateContext /* placeholderKey and original value */,
key: string, key: string,
values: string[] values: string[]
): void { ): void {
@@ -562,7 +710,7 @@ function fillArrayPlaceholders(
} }
function fillReviewPlaceholders( function fillReviewPlaceholders(
placeholderMap: Map<string, string> /* placeholderKey and original value */, placeholderMap: TemplateContext /* placeholderKey and original value */,
parentKey: string, parentKey: string,
values: CommentInfo[] values: CommentInfo[]
): void { ): void {
@@ -580,7 +728,7 @@ function fillReviewPlaceholders(
} }
function fillChildPrPlaceholders( function fillChildPrPlaceholders(
placeholderMap: Map<string, string> /* placeholderKey and original value */, placeholderMap: TemplateContext /* placeholderKey and original value */,
parentKey: string, parentKey: string,
values: PullRequestInfo[] values: PullRequestInfo[]
): void { ): void {
@@ -597,13 +745,13 @@ function fillChildPrPlaceholders(
} }
} }
function replacePrPlaceholders( function renderTemplateWithContext(
template: string, template: string,
placeholderPrMap: Map<string, string[]> /* map with all pr related custom placeholder values */, templateContext: GroupedTemplateContext /* map with all pr related custom placeholder values */,
configuration: Configuration configuration: Configuration
): string { ): string {
let transformed = template let transformed = template
for (const [key, values] of placeholderPrMap) { for (const [key, values] of templateContext) {
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
transformed = transformed.replaceAll(`#{{${key}[${i}]}}`, configuration.trim_values ? values[i].trim() : values[i]) transformed = transformed.replaceAll(`#{{${key}[${i}]}}`, configuration.trim_values ? values[i].trim() : values[i])
} }
@@ -612,7 +760,7 @@ function replacePrPlaceholders(
return transformed return transformed
} }
function cleanupPrPlaceholders(template: string, placeholders: Map<string, Placeholder[]>): string { function cleanupPrPlaceholders(template: string, placeholders: PlaceholderGroup): string {
let transformed = template let transformed = template
for (const [, phs] of placeholders) { for (const [, phs] of placeholders) {
for (const ph of phs) { for (const ph of phs) {
@@ -630,6 +778,27 @@ function cleanupPlaceholders(template: string): string {
return transformed return transformed
} }
function transformObject(obj: Record<string, unknown>, transformers: RegexTransformer[]): void {
for (const [key, value] of Object.entries(obj)) {
if (value === undefined) {
continue
}
if (Array.isArray(value)) {
if (value.every(item => typeof item === 'string')) {
// If the array contains only strings, apply the transformation to each string
obj[key] = value.map(item => transform(item, transformers))
} else {
// If the array contains objects, recursively apply the transformation to each object
for (const child of value) {
transformObject(child as Record<string, unknown>, transformers)
}
}
} else if (typeof value === 'string') {
obj[key] = transform(value, transformers)
}
}
}
function transform(filled: string, transformers: RegexTransformer[]): string { function transform(filled: string, transformers: RegexTransformer[]): string {
if (transformers.length === 0) { if (transformers.length === 0) {
return filled return filled
+27 -2
View File
@@ -1,7 +1,7 @@
import * as core from '@actions/core' 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, DefaultCommitConfiguration, DefaultConfiguration} from './configuration' import {Configuration, DefaultCommitConfiguration, DefaultConfiguration, Placeholder} from './configuration'
import moment from 'moment' import moment from 'moment'
import {DiffInfo} from './pr-collector/commits' import {DiffInfo} from './pr-collector/commits'
import {PullRequestInfo} from './pr-collector/pullRequests' import {PullRequestInfo} from './pr-collector/pullRequests'
@@ -123,7 +123,7 @@ export function resolveMode(mode: string | undefined, commitMode: boolean): 'PR'
} }
if (mode !== undefined) { if (mode !== undefined) {
const upperCaseMode = mode.toUpperCase(); const upperCaseMode = mode.toUpperCase()
if (upperCaseMode === 'COMMIT') { if (upperCaseMode === 'COMMIT') {
return 'COMMIT' return 'COMMIT'
} else if (upperCaseMode === 'HYBRID') { } else if (upperCaseMode === 'HYBRID') {
@@ -243,6 +243,27 @@ export function createOrSet<T>(map: Map<string, T[]>, key: string, value: T): vo
} }
} }
/**
* Groups an array of Placeholder objects by their source attribute.
*
* @param {Placeholder[]} placeholders - An array of Placeholder objects to be grouped.
* @returns {Map<string, Placeholder[]>} A map where the key is the source attribute of the Placeholder objects,
* and the value is an array of Placeholder objects that share the same source attribute.
*/
export function groupPlaceholders(placeholders: Placeholder[]): Map<string, Placeholder[]> {
const map = new Map<string, Placeholder[]>()
for (const ph of placeholders) {
const key = ph.source
const entry = map.get(key)
if (entry === undefined) {
map.set(key, [ph])
} else {
entry.push(ph)
}
}
return map
}
export function haveCommonElements(arr1: string[], arr2: Set<string>): boolean { export function haveCommonElements(arr1: string[], arr2: Set<string>): boolean {
return arr1.some(item => arr2.has(item)) return arr1.some(item => arr2.has(item))
} }
@@ -258,3 +279,7 @@ export function haveEveryElements(arr1: string[], arr2: Set<string>): boolean {
export function haveEveryElementsArr(arr1: string[], arr2: string[]): boolean { export function haveEveryElementsArr(arr1: string[], arr2: string[]): boolean {
return haveEveryElements(arr1, new Set(arr2)) return haveEveryElements(arr1, new Set(arr2))
} }
export function mergeMaps<T, U>(map1: Map<T, U>, map2: Map<T, U>): Map<T, U> {
return new Map<T, U>([...map1, ...map2])
}