Merge pull request #997 from mikepenz/feature/category_rules

Introduce `Rule` for categorisations
This commit is contained in:
Mike Penz
2023-01-06 11:12:23 +01:00
committed by GitHub
11 changed files with 479 additions and 326 deletions
+14 -3
View File
@@ -219,7 +219,14 @@ This configuration is a `JSON` in the following format. (The below shocases *exa
"labels": ["test", "magic"], "labels": ["test", "magic"],
"exclude_labels": ["no-magic"], "exclude_labels": ["no-magic"],
"exhaustive": true, "exhaustive": true,
"empty_content": "- no matching PRs" "empty_content": "- no matching PRs",
"rules": [
{
"pattern": "open",
"on_property": "status",
"flags": "gu"
}
]
} }
], ],
"ignore_labels": [ "ignore_labels": [
@@ -381,10 +388,14 @@ Table of descriptions for the `configuration.json` options to configure the resu
|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| categories | An array of `category` specifications, offering a flexible way to group changes into categories. | | categories | An array of `category` specifications, offering a flexible way to group changes into categories. |
| category.title | The display name of a category in the changelog. | | category.title | The display name of a category in the changelog. |
| category.labels | An array of labels, to match pull request labels against. If any PR label matches any category label, the pull request will show up under this category. | | category.labels | An array of labels, to match pull request labels against. If any PR label matches any category label, the pull request will show up under this category. (See `exhaustive` to change this) |
| category.exclude_labels | Similar to `labels`, an array of labels to match PRs against, but if a match occurs the PR is excluded from this category. | | category.exclude_labels | Similar to `labels`, an array of labels to match PRs against, but if a match occurs the PR is excluded from this category. |
| category.exhaustive | Will require all labels defined within this category to be present on the matching PR. | | category.exhaustive | Will require all labels defined within this category to be present on the matching PR. |
| category.empty_content | If the category has no matching PRs, this content will be used. When not set, the category will be skipped in the changelog. | | category.empty_content | If the category has no matching PRs, this content will be used. When not set, the category will be skipped in the changelog. |
| category.rules | An array of `rules` used to match PRs against. Any match will include the PR. (See `exhaustive` to change this) |
| category.rules.pattern | A `regex` pattern to match the property value towards. Uses `RegExp.test("val")` |
| category.rules.flags | Defines the regex flags specified for the pattern. Default: `gu`. |
| category.rules.on_property | The PR property to match against. [Possible values](https://github.com/mikepenz/release-changelog-builder-action/blob/feature/category_rules/src/configuration.ts#L33-L43). |
| ignore_labels | An array of labels, to match pull request labels against. If any PR label overlaps, the pull request will be ignored from the changelog. This takes precedence over category labels | | ignore_labels | An array of labels, to match pull request labels against. If any PR label overlaps, the pull request will be ignored from the changelog. This takes precedence over category labels |
| sort | A `sort` specification, offering the ability to define sort order and property. | | sort | A `sort` specification, offering the ability to define sort order and property. |
| sort.order | The sort order. Allowed values: `ASC`, `DESC` | | sort.order | The sort order. Allowed values: `ASC`, `DESC` |
@@ -397,7 +408,7 @@ Table of descriptions for the `configuration.json` options to configure the resu
| 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.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.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.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. | | 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. |
| 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 |
+118 -182
View File
@@ -122,6 +122,25 @@ const pullRequestWithLabelInBody: PullRequestInfo = {
status: 'merged' status: 'merged'
} }
const openPullRequest: PullRequestInfo = {
number: 6,
title: 'Still pending open pull request',
htmlURL: '',
baseBranch: '',
createdAt: moment(),
mergedAt: moment(),
mergeCommitSha: 'sha1',
author: 'Mike',
repoName: 'test-repo',
labels: new Set<string>(),
milestone: '',
body: 'Some fancy body message',
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'open'
}
it('Extract label from title, combined regex', async () => { it('Extract label from title, combined regex', async () => {
configuration.label_extractor = [ configuration.label_extractor = [
{ {
@@ -130,22 +149,7 @@ it('Extract label from title, combined regex', async () => {
on_property: 'title' on_property: 'title'
} }
] ]
expect(buildChangelogTest(configuration, mergedPullRequests)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration
})
expect(resultChangelog).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\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\n`
) )
}) })
@@ -161,21 +165,7 @@ it('Extract label from title and body, combined regex', async () => {
let prs = Array.from(mergedPullRequests) let prs = Array.from(mergedPullRequests)
prs.push(pullRequestWithLabelInBody) prs.push(pullRequestWithLabelInBody)
const resultChangelog = buildChangelog(DefaultDiffInfo, prs, { expect(buildChangelogTest(configuration, prs)).toStrictEqual(
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration
})
expect(resultChangelog).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- label in body\n - PR: #5\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\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- label in body\n - PR: #5\n\n## 🐛 Fixes\n\n- [Issue][AB-4321] - this is a PR 2 title message\n - PR: #2\n\n`
) )
}) })
@@ -193,22 +183,7 @@ it('Extract label from title, split regex', async () => {
on_property: 'title' on_property: 'title'
} }
] ]
expect(buildChangelogTest(configuration, mergedPullRequests)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration
})
expect(resultChangelog).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`
) )
}) })
@@ -226,22 +201,7 @@ it('Extract label from title, match', async () => {
method: 'match' method: 'match'
} }
] ]
expect(buildChangelogTest(configuration, mergedPullRequests)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration
})
expect(resultChangelog).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`
) )
}) })
@@ -254,22 +214,7 @@ it('Extract label from title, match multiple', async () => {
method: 'match' method: 'match'
} }
] ]
expect(buildChangelogTest(configuration, mergedPullRequests)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration
})
expect(resultChangelog).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`
) )
}) })
@@ -283,22 +228,7 @@ it('Extract label from title, match multiple, custon non matching label', async
on_empty: '[Other]' on_empty: '[Other]'
} }
] ]
expect(buildChangelogTest(configuration, mergedPullRequests)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration
})
expect(resultChangelog).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## 🧪 Others\n\n- [AB-404] - not found label\n - PR: #4\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## 🧪 Others\n\n- [AB-404] - not found label\n - PR: #4\n\n`
) )
}) })
@@ -399,22 +329,7 @@ it('Match multiple labels exhaustive for category', async () => {
exhaustive: true exhaustive: true
} }
] ]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, pullRequestsWithLabels, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration: customConfig
})
expect(resultChangelog).toStrictEqual(
`## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n` `## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
) )
}) })
@@ -426,22 +341,7 @@ it('Deduplicate duplicated PRs', async () => {
on_property: 'title', on_property: 'title',
method: 'match' method: 'match'
} }
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, pullRequestsWithLabels, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration: customConfig
})
expect(resultChangelog).toStrictEqual(
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n` `## 🚀 Features\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
) )
}) })
@@ -454,22 +354,7 @@ it('Deduplicate duplicated PRs DESC', async () => {
on_property: 'title', on_property: 'title',
method: 'match' method: 'match'
} }
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, pullRequestsWithLabels, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration: customConfig
})
expect(resultChangelog).toStrictEqual(
`## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n\n` `## 🚀 Features\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n## 🐛 Fixes\n\n- [ABC-4321] - this is a PR 2 title message\n - PR: #2\n\n`
) )
}) })
@@ -487,22 +372,7 @@ it('Use empty_content for empty category', async () => {
labels: ['Feature'] labels: ['Feature']
} }
] ]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, pullRequestsWithLabels, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration: customConfig
})
expect(resultChangelog).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`
) )
}) })
@@ -572,22 +442,7 @@ it('Use exclude labels to not include a PR within a category.', async () => {
exclude_labels: ['Fix'] exclude_labels: ['Fix']
} }
] ]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
const resultChangelog = buildChangelog(DefaultDiffInfo, pullRequestsWithLabels, {
owner: 'mikepenz',
repo: 'test-repo',
fromTag: {name: '1.0.0'},
toTag: {name: '2.0.0'},
includeOpen: false,
failOnError: false,
fetchReviewers: false,
fetchReleaseInformation: false,
fetchReviews: false,
commitMode: false,
configuration: customConfig
})
expect(resultChangelog).toStrictEqual(
`## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🚀 Features and/or 🐛 Issues But No 🐛 Fixes\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n` `## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n## 🚀 Features and/or 🐛 Issues But No 🐛 Fixes\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n`
) )
}) })
@@ -632,7 +487,92 @@ it('Extract custom placeholder from PR body and replace in global template', asy
'${{CHANGELOG}}\n\n${{C_PLACEHOLER_2[2]}}\n\n${{C_PLACEHOLER_2[*]}}${{C_PLACEHOLDER_1[7]}}${{C_PLACEHOLER_2[1493]}}${{C_PLACEHOLER_4[*]}}${{C_PLACEHOLER_4[0]}}${{C_PLACEHOLER_3[1]}}' '${{CHANGELOG}}\n\n${{C_PLACEHOLER_2[2]}}\n\n${{C_PLACEHOLER_2[*]}}${{C_PLACEHOLDER_1[7]}}${{C_PLACEHOLER_2[1493]}}${{C_PLACEHOLER_4[*]}}${{C_PLACEHOLER_4[0]}}${{C_PLACEHOLER_3[1]}}'
customConfig.pr_template = '${{BODY}} ----> ${{C_PLACEHOLDER_1}}${{C_PLACEHOLER_3}}' customConfig.pr_template = '${{BODY}} ----> ${{C_PLACEHOLDER_1}}${{C_PLACEHOLER_3}}'
const resultChangelog = buildChangelog(DefaultDiffInfo, mergedPullRequests, { expect(buildChangelogTest(customConfig, mergedPullRequests)).toStrictEqual(
`## 🚀 Features\n\nno magic body1 for this matter ----> - body1body1\nno magic body3 for this matter ----> - body3\n\n## 🐛 Fixes\n\nno magic body2 for this matter ----> - body2\nno magic body3 for this matter ----> - body3\n\n## 🧪 Others\n\nno magic body4 for this matter ----> - body4\n\n\n\n\n- ody3\n\n\n- ody1\n- ody2\n- ody3\n- ody4`
)
})
it('Use Rules to include a PR within a Category.', async () => {
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.categories = [
{
title: '## 🚀 Features But No 🐛 Fixes and only merged with a title containing `[ABC-1234]`',
labels: ['Feature'],
exclude_labels: ['Fix'],
rules: [
{
pattern: "\[ABC-1234\]",
on_property: "title"
},
{
pattern: "merged",
on_property: "status"
}
],
exhaustive: true
}
]
expect(buildChangelogTest(customConfig, pullRequestsWithLabels)).toStrictEqual(
`## 🚀 Features But No 🐛 Fixes and only merged with a title containing \`[ABC-1234]\`\n\n- [ABC-1234] - this is a PR 1 title message\n - PR: #1\n\n`
)
})
it('Use Rules to get all open PRs in a Category.', async () => {
let prs = Array.from(pullRequestsWithLabels)
prs.push(openPullRequest)
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.categories = [
{
title: '## Open PRs only',
rules: [
{
pattern: "open",
on_property: "status"
}
]
}
]
expect(buildChangelogTest(customConfig, prs)).toStrictEqual(
`## Open PRs only\n\n- Still pending open pull request\n - PR: #6\n\n`
)
})
it('Use Rules to get all open PRs in one Category and merged categorised.', async () => {
let prs = Array.from(pullRequestsWithLabels)
prs.push(openPullRequest)
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.categories = [
{
title: '## Open PRs only',
rules: [
{
pattern: "open",
on_property: "status"
}
]
},
{
title: '## 🚀 Features and 🐛 Issues',
labels: ['Feature', 'Issue'],
rules: [
{
pattern: "merged",
on_property: "status"
}
],
exhaustive: true,
}
]
expect(buildChangelogTest(customConfig, prs)).toStrictEqual(
`## Open PRs only\n\n- Still pending open pull request\n - PR: #6\n\n## 🚀 Features and 🐛 Issues\n\n- [ABC-1234] - this is a PR 3 title message\n - PR: #3\n\n`
)
})
function buildChangelogTest(config: Configuration, prs: PullRequestInfo[]): string {
return buildChangelog(DefaultDiffInfo, prs, {
owner: 'mikepenz', owner: 'mikepenz',
repo: 'test-repo', repo: 'test-repo',
fromTag: {name: '1.0.0'}, fromTag: {name: '1.0.0'},
@@ -643,10 +583,6 @@ it('Extract custom placeholder from PR body and replace in global template', asy
fetchReleaseInformation: false, fetchReleaseInformation: false,
fetchReviews: false, fetchReviews: false,
commitMode: false, commitMode: false,
configuration: customConfig configuration: config
})
expect(resultChangelog).toStrictEqual(
`## 🚀 Features\n\nno magic body1 for this matter ----> - body1body1\nno magic body3 for this matter ----> - body3\n\n## 🐛 Fixes\n\nno magic body2 for this matter ----> - body2\nno magic body3 for this matter ----> - body3\n\n## 🧪 Others\n\nno magic body4 for this matter ----> - body4\n\n\n\n\n- ody3\n\n\n- ody1\n- ody2\n- ody3\n- ody4`
)
}) })
}
Generated Vendored
+172 -56
View File
@@ -491,7 +491,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.compare = exports.sortPullRequests = exports.PullRequests = exports.EMPTY_COMMENT_INFO = void 0; exports.retrieveProperty = exports.compare = exports.sortPullRequests = exports.PullRequests = exports.EMPTY_COMMENT_INFO = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const moment_1 = __importDefault(__nccwpck_require__(9623)); const moment_1 = __importDefault(__nccwpck_require__(9623));
exports.EMPTY_COMMENT_INFO = { exports.EMPTY_COMMENT_INFO = {
@@ -738,6 +738,24 @@ function compare(a, b, sort) {
} }
} }
exports.compare = compare; exports.compare = compare;
/**
* Helper function to retrieve a property from the PullRequestInfo
*/
function retrieveProperty(pr, property, useCase) {
let value = 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;
}
exports.retrieveProperty = retrieveProperty;
// 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, labels) { function attachSpeciaLabels(status, labels) {
labels.add(`--rcba-${status}`); labels.add(`--rcba-${status}`);
@@ -779,6 +797,127 @@ const mapComment = (comment) => {
}; };
/***/ }),
/***/ 2364:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.buildRegex = exports.validateTransformer = exports.matchesRules = void 0;
const core = __importStar(__nccwpck_require__(2186));
const pullRequests_1 = __nccwpck_require__(4217);
/**
* Checks if any of the rules match the given PR
*/
function matchesRules(rules, pr, exhaustive) {
const transformers = rules.map(rule => validateTransformer(rule)).filter(t => t !== null);
if (exhaustive) {
return transformers.every(transformer => {
return matches(pr, transformer, 'rule');
});
}
else {
return transformers.some(transformer => {
return matches(pr, transformer, 'rule');
});
}
}
exports.matchesRules = matchesRules;
/**
* Checks if the configured property results in a positive `test` with the regex.
*/
function matches(pr, extractor, extractor_usecase) {
if (extractor.pattern == null) {
return false;
}
if (extractor.onProperty !== undefined && extractor.onProperty.length === 1) {
const prop = extractor.onProperty[0];
const value = (0, pullRequests_1.retrieveProperty)(pr, prop, extractor_usecase);
return extractor.pattern.test(value);
}
return false;
}
function validateTransformer(transformer) {
if (transformer === undefined) {
return null;
}
try {
let target = undefined;
if (transformer.hasOwnProperty('target')) {
target = transformer.target;
}
let onProperty = undefined;
let method = undefined;
let onEmpty = undefined;
if (transformer.hasOwnProperty('method')) {
method = transformer.method;
onEmpty = transformer.on_empty;
onProperty = transformer.on_property;
}
else if (transformer.hasOwnProperty('on_property')) {
onProperty = transformer.on_property;
}
// 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;
}
}
exports.validateTransformer = validateTransformer;
/**
* Constructs the RegExp, providing the configured Regex and additional values
*/
function buildRegex(regex, target, onProperty, method, onEmpty) {
var _a;
try {
return {
pattern: new RegExp(regex.pattern.replace('\\\\', '\\'), (_a = regex.flags) !== null && _a !== void 0 ? _a : 'gu'),
target: target || '',
onProperty,
method,
onEmpty
};
}
catch (e) {
core.warning(`⚠️ Bad replacer regex: ${regex.pattern}`);
return null;
}
}
exports.buildRegex = buildRegex;
/***/ }), /***/ }),
/***/ 5882: /***/ 5882:
@@ -1228,8 +1367,8 @@ const github = __importStar(__nccwpck_require__(5438));
const semver = __importStar(__nccwpck_require__(1383)); const semver = __importStar(__nccwpck_require__(1383));
const semver_1 = __nccwpck_require__(1383); const semver_1 = __nccwpck_require__(1383);
const gitHelper_1 = __nccwpck_require__(353); const gitHelper_1 = __nccwpck_require__(353);
const transform_1 = __nccwpck_require__(1644);
const moment_1 = __importDefault(__nccwpck_require__(9623)); const moment_1 = __importDefault(__nccwpck_require__(9623));
const regexUtils_1 = __nccwpck_require__(2364);
class Tags { class Tags {
constructor(octokit) { constructor(octokit) {
this.octokit = octokit; this.octokit = octokit;
@@ -1353,7 +1492,7 @@ class Tags {
// retrieve the tags from the API // retrieve the tags from the API
yield this.getTags(owner, repo, maxTagsToFetch), tagResolver); yield this.getTags(owner, repo, maxTagsToFetch), tagResolver);
// check if a transformer was defined // check if a transformer was defined
const tagTransformer = (0, transform_1.validateTransformer)(tagResolver.transformer); const tagTransformer = (0, regexUtils_1.validateTransformer)(tagResolver.transformer);
let transformedTags; let transformedTags;
if (tagTransformer != null) { if (tagTransformer != null) {
core.debug(`️ Using configured tagTransformer`); core.debug(`️ Using configured tagTransformer`);
@@ -1566,10 +1705,11 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.validateTransformer = exports.replaceEmptyTemplate = exports.buildChangelog = void 0; exports.replaceEmptyTemplate = exports.buildChangelog = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const pullRequests_1 = __nccwpck_require__(4217); const pullRequests_1 = __nccwpck_require__(4217);
const utils_1 = __nccwpck_require__(918); const utils_1 = __nccwpck_require__(918);
const regexUtils_1 = __nccwpck_require__(2364);
const EMPTY_MAP = new Map(); const EMPTY_MAP = new Map();
function buildChangelog(diffInfo, prs, options) { function buildChangelog(diffInfo, prs, options) {
// sort to target order // sort to target order
@@ -1579,7 +1719,7 @@ function buildChangelog(diffInfo, prs, options) {
core.info(`️ Sorted all pull requests ascending: ${JSON.stringify(sort)}`); core.info(`️ Sorted all pull requests ascending: ${JSON.stringify(sort)}`);
// drop duplicate pull requests // drop duplicate pull requests
if (config.duplicate_filter !== undefined) { if (config.duplicate_filter !== undefined) {
const extractor = validateTransformer(config.duplicate_filter); const extractor = (0, regexUtils_1.validateTransformer)(config.duplicate_filter);
if (extractor != null) { if (extractor != null) {
core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``); core.info(`️ Remove duplicated pull requests using \`duplicate_filter\``);
const deduplicatedMap = new Map(); const deduplicatedMap = new Map();
@@ -1650,8 +1790,9 @@ function buildChangelog(diffInfo, prs, options) {
if (pr.status === 'open') { if (pr.status === 'open') {
openPrs.push(body); openPrs.push(body);
} }
let matched = false; let matchedOnce = false; // in case we matched once at least, the PR can't be uncategorized
for (const [category, pullRequests] of categorized) { for (const [category, pullRequests] of categorized) {
let matched = false; // check if we matched within the given category
// check if any exclude label matches // check if any exclude label matches
if (category.exclude_labels !== undefined) { if (category.exclude_labels !== undefined) {
if ((0, utils_1.haveCommonElements)(category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) { if ((0, utils_1.haveCommonElements)(category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) {
@@ -1664,23 +1805,36 @@ function buildChangelog(diffInfo, prs, options) {
continue; // one of the exclude labels matched, skip the PR for this category continue; // one of the exclude labels matched, skip the PR for this category
} }
} }
if (category.exhaustive === true) { // in case we have exhaustive matching enabled, and have labels and/or rules
if ((0, utils_1.haveEveryElements)(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) { // validate for an exhaustive match (e.g. every provided rule applies)
pullRequests.push(body); if (category.exhaustive === true && (category.labels !== undefined || category.rules !== undefined)) {
matched = true; if (category.labels !== undefined) {
matched = (0, utils_1.haveEveryElements)(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels);
}
if (matched && category.rules !== undefined) {
matched = (0, regexUtils_1.matchesRules)(category.rules, pr, true);
} }
} }
else { else {
if ((0, utils_1.haveCommonElements)(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels)) { // if not exhaustive, do individual matches
pullRequests.push(body); if (category.labels !== undefined) {
matched = true; // check if either any of the labels applies
matched = (0, utils_1.haveCommonElements)(category.labels.map(lbl => lbl.toLocaleLowerCase('en')), pr.labels);
}
if (!matched && category.rules !== undefined) {
// if no label did apply, check if any rule applies
matched = (0, regexUtils_1.matchesRules)(category.rules, pr, false);
} }
} }
if (matched) {
pullRequests.push(body); // if matched add the PR to the list
} }
if (!matched) { matchedOnce = matchedOnce || matched;
}
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, 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) {
pullRequests.push(body); pullRequests.push(body);
break; break;
} }
@@ -1846,7 +2000,7 @@ function handlePlaceholder(template, key, value, placeholders /* placeholders to
const phs = placeholders.get(key); const phs = placeholders.get(key);
if (phs) { if (phs) {
for (const placeholder of phs) { for (const placeholder of phs) {
const transformer = validateTransformer(placeholder.transformer); const transformer = (0, regexUtils_1.validateTransformer)(placeholder.transformer);
if (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) { if (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) {
const extractedValue = value.replace(transformer.pattern, transformer.target); const extractedValue = value.replace(transformer.pattern, transformer.target);
// note: `.replace` will return the full string again if there was no match // note: `.replace` will return the full string again if there was no match
@@ -1918,47 +2072,13 @@ function validateTransformers(specifiedTransformers) {
const transformers = specifiedTransformers; const transformers = specifiedTransformers;
return transformers return transformers
.map(transformer => { .map(transformer => {
return validateTransformer(transformer); return (0, regexUtils_1.validateTransformer)(transformer);
}) })
.filter(transformer => (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) != null) .filter(transformer => (transformer === null || transformer === void 0 ? void 0 : transformer.pattern) != null)
.map(transformer => { .map(transformer => {
return transformer; return transformer;
}); });
} }
function validateTransformer(transformer) {
var _a;
if (transformer === undefined) {
return null;
}
try {
let onProperty = undefined;
let method = undefined;
let onEmpty = undefined;
if (transformer.hasOwnProperty('on_property')) {
onProperty = transformer.on_property;
method = transformer.method;
onEmpty = transformer.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('\\\\', '\\'), (_a = transformer.flags) !== null && _a !== void 0 ? _a : 'gu'),
target: transformer.target || '',
onProperty,
method,
onEmpty
};
}
catch (e) {
core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`);
return null;
}
}
exports.validateTransformer = validateTransformer;
function extractValues(pr, extractor, extractor_usecase) { function extractValues(pr, extractor, extractor_usecase) {
if (extractor.pattern == null) { if (extractor.pattern == null) {
return null; return null;
@@ -1969,11 +2089,7 @@ function extractValues(pr, extractor, extractor_usecase) {
// 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 = pr[prop]; const value = (0, pullRequests_1.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);
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+23 -3
View File
@@ -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
View File
@@ -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}`)
+101
View File
@@ -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
View File
@@ -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
+29 -66
View File
@@ -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>()
@@ -108,8 +101,9 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
openPrs.push(body) openPrs.push(body)
} }
let matched = false let matchedOnce = false // in case we matched once at least, the PR can't be uncategorized
for (const [category, pullRequests] of categorized) { for (const [category, pullRequests] of categorized) {
let matched = false // check if we matched within the given category
// check if any exclude label matches // check if any exclude label matches
if (category.exclude_labels !== undefined) { if (category.exclude_labels !== undefined) {
if ( if (
@@ -128,38 +122,46 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
} }
} }
if (category.exhaustive === true) { // in case we have exhaustive matching enabled, and have labels and/or rules
if ( // validate for an exhaustive match (e.g. every provided rule applies)
haveEveryElements( if (category.exhaustive === true && (category.labels !== undefined || category.rules !== undefined)) {
if (category.labels !== undefined) {
matched = haveEveryElements(
category.labels.map(lbl => lbl.toLocaleLowerCase('en')), category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels pr.labels
) )
) { }
pullRequests.push(body) if (matched && category.rules !== undefined) {
matched = true matched = matchesRules(category.rules, pr, true)
} }
} else { } else {
if ( // if not exhaustive, do individual matches
haveCommonElements( if (category.labels !== undefined) {
// check if either any of the labels applies
matched = haveCommonElements(
category.labels.map(lbl => lbl.toLocaleLowerCase('en')), category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
pr.labels pr.labels
) )
) { }
pullRequests.push(body) if (!matched && category.rules !== undefined) {
matched = true // if no label did apply, check if any rule applies
matched = matchesRules(category.rules, pr, false)
} }
} }
if (matched) {
pullRequests.push(body) // if matched add the PR to the list
}
matchedOnce = matchedOnce || matched
} }
if (!matched) { 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, 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) {
pullRequests.push(body) pullRequests.push(body)
break break
} }
} }
uncategorizedPrs.push(body) uncategorizedPrs.push(body)
} else { } else {
categorizedPrs.push(body) categorizedPrs.push(body)
@@ -458,40 +460,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 +467,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)
+3 -4
View File
@@ -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])
@@ -167,10 +166,10 @@ export function createOrSet<T>(map: Map<String, T[]>, key: string, value: T): vo
} }
} }
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))
} }
export function haveEveryElements(arr1: string[], arr2: Set<string>): Boolean { export function haveEveryElements(arr1: string[], arr2: Set<string>): boolean {
return arr1.every(item => arr2.has(item)) return arr1.every(item => arr2.has(item))
} }
-9
View File
@@ -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)
})
}