- move pr collection into npm module

(publish without any furhter notes or docs)
- refactor action to use npm dependency
This commit is contained in:
Mike Penz
2023-06-03 11:22:52 +00:00
committed by GitHub
parent 4a9ea3cd6a
commit e7dc26611c
28 changed files with 3529 additions and 1808 deletions
+11 -1
View File
@@ -33,10 +33,20 @@ jobs:
node-version: 16.x
- name: Install dependencies
run: npm ci
run: |
cd pr-collector
npm ci
cd ..
npm ci
- name: Rebuild the dist/ directory
run: |
cd pr-collector
npm run build
npm run package
cd ..
npm run build
npm run package
+4
View File
@@ -28,6 +28,10 @@ jobs:
- name: Run NPM
run: |
cd pr-collector
npm run all
cd ..
npm run all
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3
View File
@@ -97,3 +97,6 @@ Thumbs.db
# Ignore built ts files
__tests__/runner/*
lib/**/*
lib
pr-collector/dist
+3 -1
View File
@@ -13,5 +13,7 @@ it('Configurations are merged correctly', async () => {
const mergedConfiguration = mergeConfiguration(configurationJson, configurationFile)
console.log(mergedConfiguration)
expect(JSON.stringify(mergedConfiguration)).toEqual(`{\"max_tags_to_fetch\":200,\"max_pull_requests\":1000,\"max_back_track_time_days\":1000,\"exclude_merge_branches\":[],\"sort\":\"DESC\",\"template\":\"$\{\{CHANGELOG}}\",\"pr_template\":\"- $\{\{TITLE}}\\n - PR: #$\{\{NUMBER}}\",\"empty_template\":\"- no magic changes\",\"categories\":[{\"title\":\"## 🚀 Features\",\"labels\":[\"feature\"]},{\"title\":\"## 🐛 Fixes\",\"labels\":[\"fix\"]},{\"title\":\"## 🧪 Tests\",\"labels\":[\"test\"]}],\"ignore_labels\":[\"ignore\"],\"label_extractor\":[],\"transformers\":[],\"tag_resolver\":{\"method\":\"semver\"},\"base_branches\":[],\"custom_placeholders\":[],\"trim_values\":true}`)
expect(JSON.stringify(mergedConfiguration)).toEqual(
`{\"max_tags_to_fetch\":200,\"max_pull_requests\":1000,\"max_back_track_time_days\":1000,\"exclude_merge_branches\":[],\"sort\":\"DESC\",\"template\":\"$\{\{CHANGELOG}}\",\"pr_template\":\"- $\{\{TITLE}}\\n - PR: #$\{\{NUMBER}}\",\"empty_template\":\"- no magic changes\",\"categories\":[{\"title\":\"## 🚀 Features\",\"labels\":[\"feature\"]},{\"title\":\"## 🐛 Fixes\",\"labels\":[\"fix\"]},{\"title\":\"## 🧪 Tests\",\"labels\":[\"test\"]}],\"ignore_labels\":[\"ignore\"],\"label_extractor\":[],\"transformers\":[],\"tag_resolver\":{\"method\":\"semver\"},\"base_branches\":[],\"custom_placeholders\":[],\"trim_values\":true}`
)
})
+71 -21
View File
@@ -1,7 +1,7 @@
import {mergeConfiguration, resolveConfiguration} from '../src/utils'
import {pullData} from '../src/releaseNotesBuilder'
import {Octokit} from '@octokit/rest'
import { buildChangelog } from '../src/transform'
import {buildChangelog} from '../src/transform'
jest.setTimeout(180000)
@@ -13,7 +13,9 @@ const octokit = new Octokit({
it('Should have empty changelog (tags)', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs/configuration.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v0.0.1'},
@@ -25,7 +27,10 @@ it('Should have empty changelog (tags)', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -34,7 +39,9 @@ it('Should have empty changelog (tags)', async () => {
it('Should match generated changelog (tags)', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs/configuration.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v0.0.1'},
@@ -46,7 +53,10 @@ it('Should match generated changelog (tags)', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -60,7 +70,9 @@ it('Should match generated changelog (tags)', async () => {
it('Should match generated changelog (refs)', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_all_placeholders.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: '5ec7a2d86fe9f43fdd38d5e254a1117c8a51b4c3'},
@@ -72,7 +84,10 @@ it('Should match generated changelog (refs)', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -94,7 +109,9 @@ nhoelzl
it('Should match generated changelog and replace all occurrences (refs)', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_replace_all_placeholders.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: '5ec7a2d86fe9f43fdd38d5e254a1117c8a51b4c3'},
@@ -106,7 +123,10 @@ it('Should match generated changelog and replace all occurrences (refs)', async
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -130,7 +150,9 @@ nhoelzl
it('Should match ordered ASC', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_asc.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v0.3.0'},
@@ -142,7 +164,10 @@ it('Should match ordered ASC', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -151,7 +176,9 @@ it('Should match ordered ASC', async () => {
it('Should match ordered DESC', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_desc.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v0.3.0'},
@@ -163,7 +190,10 @@ it('Should match ordered DESC', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -172,7 +202,9 @@ it('Should match ordered DESC', async () => {
it('Should match ordered by title ASC', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_sort_title_asc.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v0.3.0'},
@@ -184,7 +216,10 @@ it('Should match ordered by title ASC', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -195,7 +230,9 @@ it('Should match ordered by title ASC', async () => {
it('Should match ordered by title DESC', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_sort_title_desc.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v0.3.0'},
@@ -207,7 +244,10 @@ it('Should match ordered by title DESC', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -218,7 +258,9 @@ it('Should match ordered by title DESC', async () => {
it('Should ignore PRs not merged into develop branch', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_base_branches_develop.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v1.3.1'},
@@ -230,7 +272,10 @@ it('Should ignore PRs not merged into develop branch', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
@@ -239,7 +284,9 @@ it('Should ignore PRs not merged into develop branch', async () => {
it('Should ignore PRs not merged into main branch', async () => {
const configuration = mergeConfiguration(undefined, resolveConfiguration('', 'configs_test/configuration_base_branches_main.json'))
const data = await pullData(octokit, {
const data = await pullData(
octokit,
{
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: {name: 'v1.3.1'},
@@ -251,7 +298,10 @@ it('Should ignore PRs not merged into main branch', async () => {
fetchReviews: false,
commitMode: false,
configuration
}, false, false)
},
false,
false
)
const changeLog = buildChangelog(data!.diffInfo, data!.mergedPullRequests, data!.options)
console.log(changeLog)
+3 -9
View File
@@ -23,9 +23,7 @@ it('Should order tags correctly using semver', async () => {
})
.join(',')
expect(sorted).toStrictEqual(
`2020.4.0,2020.4.0-rc02,2020.3.2,v2020.3.1,2020.3.1-rc03,2020.3.1-rc01,2020.3.1-b01,v2020.3.0`
)
expect(sorted).toStrictEqual(`2020.4.0,2020.4.0-rc02,2020.3.2,v2020.3.1,2020.3.1-rc03,2020.3.1-rc01,2020.3.1-b01,v2020.3.0`)
})
it('Should order tags correctly using semver', async () => {
@@ -54,9 +52,7 @@ it('Should order tags correctly using semver', async () => {
})
.join(',')
expect(sorted).toStrictEqual(
`1000.0.0,100.0.0,20.0.2,10.1.0,10.1.0-2,10.0.0,2.0.0,1.0.0,1.0.0-a01,0.1.0,0.1.0-b01,0.0.1,0.0.1-rc01`
)
expect(sorted).toStrictEqual(`1000.0.0,100.0.0,20.0.2,10.1.0,10.1.0-2,10.0.0,2.0.0,1.0.0,1.0.0-a01,0.1.0,0.1.0-b01,0.0.1,0.0.1-rc01`)
})
it('Should order tags alphabetical', async () => {
@@ -85,9 +81,7 @@ it('Should order tags alphabetical', async () => {
})
.join(',')
expect(sorted).toStrictEqual(
`a,20.0.2,2.0.0,1000.0.0,10.1.0,10.1.0-2,10.0.0,1.0.0,1.0.0-a01,v1,0.1.0-b01,0.0.1,0.0.1-rc01`
)
expect(sorted).toStrictEqual(`a,20.0.2,2.0.0,1000.0.0,10.1.0,10.1.0-2,10.0.0,1.0.0,1.0.0-a01,v1,0.1.0-b01,0.0.1,0.0.1-rc01`)
})
it('Should filter tags correctly using the regex', async () => {
+17 -21
View File
@@ -329,8 +329,8 @@ openPullRequestsWithLabels.push(
requestedReviewers: [],
approvedReviewers: [],
status: 'open'
},
{
},
{
number: 7,
title: 'Still pending open pull request',
htmlURL: '',
@@ -347,7 +347,7 @@ openPullRequestsWithLabels.push(
requestedReviewers: [],
approvedReviewers: [],
status: 'open'
}
}
)
it('Match multiple labels exhaustive for category', async () => {
@@ -457,9 +457,7 @@ it('Release Diff', async () => {
configuration: customConfig
})
expect(resultChangelog).toStrictEqual(
`https://github.com/mikepenz/release-changelog-builder-action/compare/v2.8.0...v2.8.1\n`
)
expect(resultChangelog).toStrictEqual(`https://github.com/mikepenz/release-changelog-builder-action/compare/v2.8.0...v2.8.1\n`)
})
it('Use exclude labels to not include a PR within a category.', async () => {
@@ -532,7 +530,6 @@ it('Extract custom placeholder from PR body and replace in global template', asy
)
})
it('Use Rules to include a PR within a Category.', async () => {
const customConfig = Object.assign({}, DefaultConfiguration)
customConfig.categories = [
@@ -542,12 +539,12 @@ it('Use Rules to include a PR within a Category.', async () => {
exclude_labels: ['Fix'],
rules: [
{
pattern: "\[ABC-1234\]",
on_property: "title"
pattern: '[ABC-1234]',
on_property: 'title'
},
{
pattern: "merged",
on_property: "status"
pattern: 'merged',
on_property: 'status'
}
],
exhaustive: true
@@ -568,8 +565,8 @@ it('Use Rules to get all open PRs in a Category.', async () => {
title: '## Open PRs only',
rules: [
{
pattern: "open",
on_property: "status"
pattern: 'open',
on_property: 'status'
}
]
}
@@ -577,7 +574,6 @@ it('Use Rules to get all open PRs in a Category.', async () => {
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 current open PR and merged categorised.', async () => {
let prs = Array.from(pullRequestsWithLabels)
prs = prs.concat(Array.from(openPullRequestsWithLabels))
@@ -599,7 +595,8 @@ it('Use Rules to get current open PR and merged categorised.', async () => {
],
exhaustive: true,
exhaustive_rules: false
},{
},
{
title: '## 🐛 Issues',
labels: ['Issue'],
rules: [
@@ -627,8 +624,8 @@ it('Use Rules to get all open PRs in one Category and merged categorised.', asyn
title: '## Open PRs only',
rules: [
{
pattern: "open",
on_property: "status"
pattern: 'open',
on_property: 'status'
}
]
},
@@ -637,11 +634,11 @@ it('Use Rules to get all open PRs in one Category and merged categorised.', asyn
labels: ['Feature', 'Issue'],
rules: [
{
pattern: "merged",
on_property: "status"
pattern: 'merged',
on_property: 'status'
}
],
exhaustive: true,
exhaustive: true
}
]
expect(buildChangelogTest(customConfig, prs)).toStrictEqual(
@@ -649,7 +646,6 @@ it('Use Rules to get all open PRs in one Category and merged categorised.', asyn
)
})
function buildChangelogTest(config: Configuration, prs: PullRequestInfo[]): string {
return buildChangelog(DefaultDiffInfo, prs, {
owner: 'mikepenz',
Generated Vendored
+1511 -1382
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+3
View File
@@ -541,6 +541,9 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
github-pr-collector
Apache 2.0
has-flag
MIT
MIT License
+28 -6
View File
@@ -13,16 +13,11 @@
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@octokit/rest": "^19.0.11",
"https-proxy-agent": "^7.0.0",
"moment": "^2.29.4",
"semver": "^7.5.1",
"tunnel": "^0.0.6",
"webpack": "^5.85.0"
"github-pr-collector": "file:pr-collector"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@types/node": "^20.2.5",
"@types/semver": "^7.5.0",
"@typescript-eslint/parser": "^5.59.8",
"@vercel/ncc": "^0.36.1",
"eslint": "^8.42.0",
@@ -3768,6 +3763,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/github-pr-collector": {
"resolved": "pr-collector",
"link": true
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -7063,6 +7062,29 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"pr-collector": {
"name": "github-pr-collector",
"version": "v1.0.0",
"license": "Apache 2.0",
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@octokit/rest": "^19.0.11",
"https-proxy-agent": "^7.0.0",
"moment": "^2.29.4",
"semver": "^7.5.1",
"webpack": "^5.85.0"
},
"devDependencies": {
"@types/node": "^20.2.5",
"@types/semver": "^7.5.0",
"@vercel/ncc": "^0.36.1",
"js-yaml": "^4.1.0",
"prettier": "2.8.8",
"typescript": "^5.1.3"
}
}
}
}
+1 -6
View File
@@ -36,16 +36,11 @@
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@octokit/rest": "^19.0.11",
"https-proxy-agent": "^7.0.0",
"moment": "^2.29.4",
"semver": "^7.5.1",
"tunnel": "^0.0.6",
"webpack": "^5.85.0"
"github-pr-collector": "file:pr-collector"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@types/node": "^20.2.5",
"@types/semver": "^7.5.0",
"@typescript-eslint/parser": "^5.59.8",
"@vercel/ncc": "^0.36.1",
"eslint": "^8.42.0",
+1322
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
{
"name": "github-pr-collector",
"version": "v1.0.0",
"description": "Library to fetch GitHub pull request between 2 tags/sha1 hashes.",
"main": "lib/prCollector.js",
"types": "lib/prCollector.d.ts",
"scripts": {
"build": "tsc",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts",
"package": "ncc build --source-map --license licenses.txt",
"test": "jest --passWithNoTests",
"all": "npm run build && npm run format && npm run lint && npm run package && npm test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mikepenz/release-changelog-builder.git"
},
"keywords": [
"github",
"actions",
"changelog",
"release-notes",
"release",
"notes",
"change",
"release-automation",
"pull-requests",
"issues",
"labels"
],
"author": "Mike Penz",
"license": "Apache 2.0",
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@octokit/rest": "^19.0.11",
"https-proxy-agent": "^7.0.0",
"moment": "^2.29.4",
"semver": "^7.5.1",
"webpack": "^5.85.0"
},
"devDependencies": {
"@types/node": "^20.2.5",
"@types/semver": "^7.5.0",
"@vercel/ncc": "^0.36.1",
"js-yaml": "^4.1.0",
"prettier": "2.8.8",
"typescript": "^5.1.3"
}
}
@@ -2,8 +2,8 @@ import * as core from '@actions/core'
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import moment from 'moment'
import {failOrError} from './utils'
import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {PullRequestInfo} from './pullRequests'
import {Options} from './prCollector'
export interface DiffInfo {
changedFiles: number
@@ -121,7 +121,7 @@ export class Commits {
return commitsResult
}
async getCommitHistory(options: ReleaseNotesOptions): Promise<DiffInfo> {
async getCommitHistory(options: Options): Promise<DiffInfo> {
const {owner, repo, fromTag, toTag, failOnError} = options
core.info(`️ Comparing ${owner}/${repo} - '${fromTag.name}...${toTag.name}'`)
@@ -141,7 +141,7 @@ export class Commits {
return diffInfo
}
async generateCommitPRs(options: ReleaseNotesOptions): Promise<[DiffInfo, PullRequestInfo[]]> {
async generateCommitPRs(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, configuration} = options
const diffInfo = await this.getCommitHistory(options)
+55
View File
@@ -0,0 +1,55 @@
export interface Configuration {
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"
tag_resolver: TagResolver
base_branches: string[]
}
/**
* Defines the properties of the PullRequestInfo useable in different configurations
*/
export type Property =
| 'number'
| '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 {
order: 'ASC' | 'DESC' // the sorting order
on_property: 'mergedAt' | 'title' // the property to sort on. (mergedAt falls back to createdAt)
}
export interface TagResolver {
method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting
transformer?: Transformer // transforms the tag name using the regex, run after the filter
}
export interface Regex {
pattern: string // the regex pattern to match
flags?: string // the regex flag to use for RegExp
}
export interface Transformer extends Regex {
target?: string // the target string to transform the source string using the regex to
}
export interface Extractor extends Transformer {
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
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)
}
+159
View File
@@ -0,0 +1,159 @@
import * as core from '@actions/core'
import {Configuration} from './configuration'
import {Octokit} from '@octokit/rest'
import {TagInfo, Tags} from './tags'
import {failOrError} from './utils'
import {HttpsProxyAgent} from 'https-proxy-agent'
import {PullRequestInfo, PullRequests} from './pullRequests'
import {Commits, DiffInfo} from './commits'
export interface Options {
owner: string // the owner of the repository
repo: string // the repository
fromTag: TagInfo // the tag/ref to start from
toTag: TagInfo // the tag/ref up to
includeOpen: boolean // defines if we should also fetch open pull requests
failOnError: boolean // defines if we should fail the action in case of an error
fetchReviewers: boolean // defines if the action should fetch the reviewers for PRs - approved reviewers are not included in the default PR listing
fetchReleaseInformation: boolean // defines if the action should fetch the release information for the from and to tag - e.g. the creation date for the associated release
fetchReviews: boolean // defines if the action should fetch the reviews for the PR.
commitMode: boolean // defines if we use the alternative commit based mode. note: this is only partially supported
configuration: Configuration // the configuration as defined in `configuration.ts`
}
export interface Data {
diffInfo: DiffInfo
mergedPullRequests: PullRequestInfo[]
options: Options
}
export class PullRequestCollector {
constructor(
private baseUrl: string | null,
private token: string | null,
private repositoryPath: string,
private owner: string,
private repo: string,
private fromTag: string | null,
private toTag: string | null,
private includeOpen: boolean = false,
private failOnError: boolean,
private ignorePreReleases: boolean,
private fetchReviewers: boolean = false,
private fetchReleaseInformation: boolean = false,
private fetchReviews: boolean = false,
private commitMode: boolean = false,
private configuration: Configuration
) {}
async build(): Promise<Data | null> {
// check proxy setup for GHES environments
const proxy = process.env.https_proxy || process.env.HTTPS_PROXY
const noProxy = process.env.no_proxy || process.env.NO_PROXY
let noProxyArray: string[] = []
if (noProxy) {
noProxyArray = noProxy.split(',')
}
// load octokit instance
const octokit = new Octokit({
auth: `token ${this.token || process.env.GITHUB_TOKEN}`,
baseUrl: `${this.baseUrl || 'https://api.github.com'}`
})
if (proxy) {
const agent = new HttpsProxyAgent(proxy)
octokit.hook.before('request', options => {
if (noProxyArray.includes(options.request.hostname)) {
return
}
options.request.agent = agent
})
}
// ensure proper from <-> to tag range
core.startGroup(`🔖 Resolve tags`)
const tagsApi = new Tags(octokit)
const tagRange = await tagsApi.retrieveRange(
this.repositoryPath,
this.owner,
this.repo,
this.fromTag,
this.toTag,
this.ignorePreReleases,
this.configuration.max_tags_to_fetch,
this.configuration.tag_resolver
)
let thisTag = tagRange.to
if (!thisTag) {
failOrError(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError)
return null
} else {
core.setOutput('toTag', thisTag.name)
core.debug(`Resolved 'toTag' as ${thisTag.name}`)
}
let previousTag = tagRange.from
if (previousTag == null) {
failOrError(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError)
return null
}
core.setOutput('fromTag', previousTag.name)
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
if (this.fetchReleaseInformation) {
// load release information from the GitHub API
core.info(`️ Fetching release information was enabled`)
thisTag = await tagsApi.fillTagInformation(this.repositoryPath, this.owner, this.repo, thisTag)
previousTag = await tagsApi.fillTagInformation(this.repositoryPath, this.owner, this.repo, previousTag)
} else {
core.debug(`️ Fetching release information was disabled`)
}
core.endGroup()
const options = {
owner: this.owner,
repo: this.repo,
fromTag: previousTag,
toTag: thisTag,
includeOpen: this.includeOpen,
failOnError: this.failOnError,
fetchReviewers: this.fetchReviewers,
fetchReleaseInformation: this.fetchReleaseInformation,
fetchReviews: this.fetchReviews,
commitMode: this.commitMode,
configuration: this.configuration
}
return await pullData(octokit, options)
}
}
export async function pullData(octokit: Octokit, options: Options): Promise<Data | null> {
let mergedPullRequests: PullRequestInfo[]
let diffInfo: DiffInfo
const commitsApi = new Commits(octokit)
if (!options.commitMode) {
core.startGroup(`🚀 Load pull requests`)
const pullRequestsApi = new PullRequests(octokit, commitsApi)
const [info, prs] = await pullRequestsApi.getMergedPullRequests(options)
mergedPullRequests = prs
diffInfo = info
} else {
core.startGroup(`🚀 Load commit history`)
core.info(`⚠️ Executing experimental commit mode`)
const [info, prs] = await commitsApi.generateCommitPRs(options)
mergedPullRequests = prs
diffInfo = info
}
core.endGroup()
return {
diffInfo,
mergedPullRequests,
options
}
}
@@ -4,7 +4,7 @@ import {Unpacked} from './utils'
import moment from 'moment'
import {Property, Sort} from './configuration'
import {Commits, DiffInfo, filterCommits} from './commits'
import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {Options} from './prCollector'
export interface PullRequestInfo {
number: number
@@ -162,7 +162,7 @@ export class PullRequests {
pr.reviews = prReviews
}
async getMergedPullRequests(options: ReleaseNotesOptions): Promise<[DiffInfo, PullRequestInfo[]]> {
async getMergedPullRequests(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, includeOpen, fetchReviewers, fetchReviews, configuration} = options
const diffInfo = await this.commits.getCommitHistory(options)
+49
View File
@@ -0,0 +1,49 @@
import * as core from '@actions/core'
import * as fs from 'fs'
/**
* Will automatically either report the message to the log, or mark the action as failed. Additionally defining the output failed, allowing it to be read in by other actions
*/
export function failOrError(message: string | Error, failOnError: boolean): void {
// if we report any failure, consider the action to have failed, may not make the build fail
core.setOutput('failed', true)
if (failOnError) {
core.setFailed(message)
} else {
core.error(message)
}
}
/**
* Checks if a given directory exists
*/
export function directoryExistsSync(inputPath: string, required?: boolean): boolean {
if (!inputPath) {
throw new Error("Arg 'path' must not be empty")
}
let stats: fs.Stats
try {
stats = fs.statSync(inputPath)
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
if (error.code === 'ENOENT') {
if (!required) {
return false
}
throw new Error(`Directory '${inputPath}' does not exist`)
}
throw new Error(`Encountered an error when checking whether path '${inputPath}' exists: ${error.message}`)
}
if (stats.isDirectory()) {
return true
} else if (!required) {
return false
}
throw new Error(`Directory '${inputPath}' does not exist`)
}
export type Unpacked<T> = T extends (infer U)[] ? U : T
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"lib": [ "ES2021.String" ], /* Enable custom `ES2021.String` extension in typescript for `replaceAll` */
"declaration": true
},
"exclude": ["node_modules", "**/*.test.ts", "lib"]
}
+2 -24
View File
@@ -1,3 +1,5 @@
import {Extractor, Regex, Rule, Sort, Transformer} from 'github-pr-collector/lib/configuration'
export interface Configuration {
max_tags_to_fetch: number
max_pull_requests: number
@@ -45,30 +47,6 @@ export type Property =
| 'approvedReviewers'
| 'status'
export interface Rule extends Regex {
on_property?: Property // retrieve the property to apply the rule on
}
export interface Sort {
order: 'ASC' | 'DESC' // the sorting order
on_property: 'mergedAt' | 'title' // the property to sort on. (mergedAt falls back to createdAt)
}
export interface Regex {
pattern: string // the regex pattern to match
flags?: string // the regex flag to use for RegExp
}
export interface Transformer extends Regex {
target?: string // the target string to transform the source string using the regex to
}
export interface Extractor extends Transformer {
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
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)
}
export interface TagResolver {
method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting
+60 -131
View File
@@ -1,12 +1,12 @@
import * as core from '@actions/core'
import {Configuration} from './configuration'
import {Octokit} from '@octokit/rest'
import {TagInfo, Tags} from './tags'
import {checkExportedData, failOrError} from './utils'
import {HttpsProxyAgent} from 'https-proxy-agent'
import {PullRequestInfo, PullRequests} from './pullRequests'
import {Commits, DiffInfo} from './commits'
import {checkExportedData} from './utils'
import {buildChangelog} from './transform'
import {PullRequestCollector} from 'github-pr-collector'
import {failOrError} from 'github-pr-collector/lib/utils'
import {TagInfo} from 'github-pr-collector/lib/tags'
import {DiffInfo} from 'github-pr-collector/lib/commits'
import {PullRequestInfo} from 'github-pr-collector/lib/pullRequests'
export interface ReleaseNotesOptions {
owner: string // the owner of the repository
@@ -22,12 +22,11 @@ export interface ReleaseNotesOptions {
configuration: Configuration // the configuration as defined in `configuration.ts`
}
export interface ReleaseNotesData {
export interface Data {
diffInfo: DiffInfo
mergedPullRequests: PullRequestInfo[]
options: ReleaseNotesOptions
}
export class ReleaseNotesBuilder {
constructor(
private baseUrl: string | null,
@@ -69,77 +68,34 @@ export class ReleaseNotesBuilder {
}
core.endGroup()
// check proxy setup for GHES environments
const proxy = process.env.https_proxy || process.env.HTTPS_PROXY
const noProxy = process.env.no_proxy || process.env.NO_PROXY
let noProxyArray: string[] = []
if (noProxy) {
noProxyArray = noProxy.split(',')
}
// load octokit instance
const octokit = new Octokit({
auth: `token ${this.token || process.env.GITHUB_TOKEN}`,
baseUrl: `${this.baseUrl || 'https://api.github.com'}`
})
if (proxy) {
const agent = new HttpsProxyAgent(proxy)
octokit.hook.before('request', options => {
if (noProxyArray.includes(options.request.hostname)) {
return
}
options.request.agent = agent
})
}
// ensure proper from <-> to tag range
core.startGroup(`🔖 Resolve tags`)
const tagsApi = new Tags(octokit)
const tagRange = await tagsApi.retrieveRange(
const prData = await new PullRequestCollector(
this.baseUrl,
this.token,
this.repositoryPath,
this.owner,
this.repo,
this.fromTag,
this.toTag,
this.includeOpen,
this.failOnError,
this.ignorePreReleases,
this.configuration.max_tags_to_fetch,
this.configuration.tag_resolver
)
this.fetchReviewers,
this.fetchReleaseInformation,
this.fetchReviews,
this.commitMode,
this.configuration
).build()
let thisTag = tagRange.to
if (!thisTag) {
failOrError(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError)
return null
} else {
core.setOutput('toTag', thisTag.name)
core.debug(`Resolved 'toTag' as ${thisTag.name}`)
}
let previousTag = tagRange.from
if (previousTag == null) {
failOrError(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError)
if (prData == null) {
return null
}
core.setOutput('fromTag', previousTag.name)
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
if (this.fetchReleaseInformation) {
// load release information from the GitHub API
core.info(`️ Fetching release information was enabled`)
thisTag = await tagsApi.fillTagInformation(this.repositoryPath, this.owner, this.repo, thisTag)
previousTag = await tagsApi.fillTagInformation(this.repositoryPath, this.owner, this.repo, previousTag)
} else {
core.debug(`️ Fetching release information was disabled`)
}
core.endGroup()
const options = {
const resolvedOptions = prData.options
const options: ReleaseNotesOptions = {
owner: this.owner,
repo: this.repo,
fromTag: previousTag,
toTag: thisTag,
fromTag: resolvedOptions.fromTag,
toTag: resolvedOptions.toTag,
includeOpen: this.includeOpen,
failOnError: this.failOnError,
fetchReviewers: this.fetchReviewers,
@@ -148,8 +104,44 @@ export class ReleaseNotesBuilder {
commitMode: this.commitMode,
configuration: this.configuration
}
const mergedPullRequests = prData.mergedPullRequests
const diffInfo = prData.diffInfo
releaseNotesData = await pullData(octokit, options, this.exportCollected, this.exportOnly)
// define the included PRs within this release as output
core.setOutput(
'pull_requests',
mergedPullRequests
.map(pr => {
return pr.number
})
.join(',')
)
core.setOutput('changed_files', diffInfo.changedFiles)
core.setOutput('additions', diffInfo.additions)
core.setOutput('deletions', diffInfo.deletions)
core.setOutput('changes', diffInfo.changes)
core.setOutput('commits', diffInfo.commits)
if (this.exportCollected) {
core.info('📦 Exporting collected data')
core.exportVariable(`RCBA_EXPORT_diffInfo`, JSON.stringify(diffInfo))
//fs.writeFileSync(path.resolve('diffInfo.json'), JSON.stringify(diffInfo))
core.exportVariable(`RCBA_EXPORT_mergedPullRequests`, JSON.stringify(mergedPullRequests))
//fs.writeFileSync(path.resolve('mergedPullRequests.json'), JSON.stringify(mergedPullRequests))
core.exportVariable(`RCBA_EXPORT_options`, JSON.stringify(options))
//fs.writeFileSync(path.resolve('options.json'), JSON.stringify(options))
if (this.exportOnly) {
core.endGroup()
return null
}
}
releaseNotesData = {
mergedPullRequests,
diffInfo,
options
}
} else {
core.info(`️ Retrieved previously exported collected data`)
@@ -196,66 +188,3 @@ export class ReleaseNotesBuilder {
}
}
}
export async function pullData(
octokit: Octokit,
options: ReleaseNotesOptions,
exportCollected: boolean,
exportOnly: boolean
): Promise<ReleaseNotesData | null> {
let mergedPullRequests: PullRequestInfo[]
let diffInfo: DiffInfo
const commitsApi = new Commits(octokit)
if (!options.commitMode) {
core.startGroup(`🚀 Load pull requests`)
const pullRequestsApi = new PullRequests(octokit, commitsApi)
const [info, prs] = await pullRequestsApi.getMergedPullRequests(options)
mergedPullRequests = prs
diffInfo = info
} else {
core.startGroup(`🚀 Load commit history`)
core.info(`⚠️ Executing experimental commit mode`)
const [info, prs] = await commitsApi.generateCommitPRs(options)
mergedPullRequests = prs
diffInfo = info
}
// define the included PRs within this release as output
core.setOutput(
'pull_requests',
mergedPullRequests
.map(pr => {
return pr.number
})
.join(',')
)
core.setOutput('changed_files', diffInfo.changedFiles)
core.setOutput('additions', diffInfo.additions)
core.setOutput('deletions', diffInfo.deletions)
core.setOutput('changes', diffInfo.changes)
core.setOutput('commits', diffInfo.commits)
if (exportCollected) {
core.info('📦 Exporting collected data')
core.exportVariable(`RCBA_EXPORT_diffInfo`, JSON.stringify(diffInfo))
//fs.writeFileSync(path.resolve('diffInfo.json'), JSON.stringify(diffInfo))
core.exportVariable(`RCBA_EXPORT_mergedPullRequests`, JSON.stringify(mergedPullRequests))
//fs.writeFileSync(path.resolve('mergedPullRequests.json'), JSON.stringify(mergedPullRequests))
core.exportVariable(`RCBA_EXPORT_options`, JSON.stringify(options))
//fs.writeFileSync(path.resolve('options.json'), JSON.stringify(options))
if (exportOnly) {
core.endGroup()
return null
}
}
core.endGroup()
return {
diffInfo,
mergedPullRequests,
options
}
}
+6 -5
View File
@@ -1,10 +1,11 @@
import * as core from '@actions/core'
import {Category, Configuration, Placeholder, Property, Transformer} from './configuration'
import {CommentInfo, EMPTY_COMMENT_INFO, PullRequestInfo, retrieveProperty, sortPullRequests} from './pullRequests'
import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {DiffInfo} from './commits'
import {Category, Configuration, Placeholder, Property} from './configuration'
import {createOrSet, haveCommonElementsArr, haveEveryElementsArr} from './utils'
import {matchesRules, RegexTransformer, validateTransformer} from './regexUtils'
import {CommentInfo, EMPTY_COMMENT_INFO, PullRequestInfo, retrieveProperty, sortPullRequests} from 'github-pr-collector/lib/pullRequests'
import {DiffInfo} from 'github-pr-collector/lib/commits'
import {RegexTransformer, matchesRules, validateTransformer} from 'github-pr-collector/lib/regexUtils'
import {Transformer} from 'github-pr-collector/lib/configuration'
import {ReleaseNotesOptions} from './releaseNotesBuilder'
const EMPTY_MAP = new Map<string, string>()
+4 -51
View File
@@ -2,10 +2,10 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as path from 'path'
import {Configuration, DefaultConfiguration} from './configuration'
import {ReleaseNotesData, ReleaseNotesOptions} from './releaseNotesBuilder'
import {DiffInfo} from './commits'
import {PullRequestInfo} from './pullRequests'
import moment from 'moment'
import {DiffInfo} from 'github-pr-collector/lib/commits'
import {PullRequestInfo} from 'github-pr-collector/lib/pullRequests'
import {Data, ReleaseNotesOptions} from './releaseNotesBuilder'
/**
* Resolves the repository path, relatively to the GITHUB_WORKSPACE
*/
@@ -23,24 +23,11 @@ export function retrieveRepositoryPath(providedPath: string): string {
return repositoryPath
}
/**
* Will automatically either report the message to the log, or mark the action as failed. Additionally defining the output failed, allowing it to be read in by other actions
*/
export function failOrError(message: string | Error, failOnError: boolean): void {
// if we report any failure, consider the action to have failed, may not make the build fail
core.setOutput('failed', true)
if (failOnError) {
core.setFailed(message)
} else {
core.error(message)
}
}
/**
* Retrieves the exported information from a previous run of the `release-changelog-builder-action`.
* If available, return a [ReleaseNotesData].
*/
export function checkExportedData(): ReleaseNotesData | null {
export function checkExportedData(): Data | null {
const rawDiffInfo = process.env[`RCBA_EXPORT_diffInfo`]
const rawMergedPullRequests = process.env[`RCBA_EXPORT_mergedPullRequests`]
const rawOptions = process.env[`RCBA_EXPORT_options`]
@@ -153,38 +140,6 @@ export function mergeConfiguration(jc?: Configuration, fc?: Configuration): Conf
}
}
/**
* Checks if a given directory exists
*/
export function directoryExistsSync(inputPath: string, required?: boolean): boolean {
if (!inputPath) {
throw new Error("Arg 'path' must not be empty")
}
let stats: fs.Stats
try {
stats = fs.statSync(inputPath)
} catch (error: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
if (error.code === 'ENOENT') {
if (!required) {
return false
}
throw new Error(`Directory '${inputPath}' does not exist`)
}
throw new Error(`Encountered an error when checking whether path '${inputPath}' exists: ${error.message}`)
}
if (stats.isDirectory()) {
return true
} else if (!required) {
return false
}
throw new Error(`Directory '${inputPath}' does not exist`)
}
/**
* Writes the changelog to the given the file
*/
@@ -200,8 +155,6 @@ export function writeOutput(githubWorkspacePath: string, outputFile: string, cha
}
}
export type Unpacked<T> = T extends (infer U)[] ? U : T
export function createOrSet<T>(map: Map<string, T[]>, key: string, value: T): void {
const entry = map.get(key)
if (!entry) {
+1 -1
View File
@@ -9,5 +9,5 @@
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"lib": [ "ES2021.String" ] /* Enable custom `ES2021.String` extension in typescript for `replaceAll` */
},
"exclude": ["node_modules", "**/*.test.ts"]
"exclude": ["node_modules", "**/*.test.ts", "pr-collector"]
}