- introduce capability of the action running 2 split
- first collect all pull requests and export to environment - second take exported data and continue generation of changelogs (makes it cheaper, by only collecting PRs once)
This commit is contained in:
+1
-1
@@ -165,7 +165,7 @@ export class Commits {
|
||||
mergeCommitSha: commit.sha,
|
||||
author: commit.author || '',
|
||||
repoName: '',
|
||||
labels: new Set(),
|
||||
labels: [],
|
||||
milestone: '',
|
||||
body: commit.message || '',
|
||||
assignees: [],
|
||||
|
||||
@@ -54,6 +54,8 @@ async function run(): Promise<void> {
|
||||
const fetchReleaseInformation = core.getInput('fetchReleaseInformation') === 'true'
|
||||
const fetchReviews = core.getInput('fetchReviews') === 'true'
|
||||
const commitMode = core.getInput('commitMode') === 'true'
|
||||
const exportCollected = core.getInput('exportCollected') === 'true'
|
||||
const exportOnly = core.getInput('exportOnly') === 'true'
|
||||
|
||||
const result = await new ReleaseNotesBuilder(
|
||||
baseUrl,
|
||||
@@ -70,6 +72,8 @@ async function run(): Promise<void> {
|
||||
fetchReleaseInformation,
|
||||
fetchReviews,
|
||||
commitMode,
|
||||
exportCollected,
|
||||
exportOnly,
|
||||
configuration
|
||||
).build()
|
||||
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ export interface PullRequestInfo {
|
||||
mergeCommitSha: string
|
||||
author: string
|
||||
repoName: string
|
||||
labels: Set<string>
|
||||
labels: string[]
|
||||
milestone: string
|
||||
body: string
|
||||
assignees: string[]
|
||||
@@ -329,8 +329,8 @@ export function retrieveProperty(pr: PullRequestInfo, property: Property, useCas
|
||||
}
|
||||
|
||||
// helper function to add a special open label to prs not merged.
|
||||
function attachSpeciaLabels(status: 'open' | 'merged', labels: Set<string>): Set<string> {
|
||||
labels.add(`--rcba-${status}`)
|
||||
function attachSpeciaLabels(status: 'open' | 'merged', labels: string[]): string[] {
|
||||
labels.push(`--rcba-${status}`)
|
||||
return labels
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ const mapPullRequest = (pr: PullData | Unpacked<PullsListData>, status: 'open' |
|
||||
mergeCommitSha: pr.merge_commit_sha || '',
|
||||
author: pr.user?.login || '',
|
||||
repoName: pr.base.repo.full_name,
|
||||
labels: attachSpeciaLabels(status, new Set(pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || [])),
|
||||
labels: attachSpeciaLabels(status, pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || []),
|
||||
milestone: pr.milestone?.title || '',
|
||||
body: pr.body || '',
|
||||
assignees: pr.assignees?.map(asignee => asignee?.login || '') || [],
|
||||
|
||||
+158
-97
@@ -2,11 +2,13 @@ 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 {checkExportedData, failOrError} from './utils'
|
||||
import {HttpsProxyAgent} from 'https-proxy-agent'
|
||||
import {PullRequestInfo, PullRequests} from './pullRequests'
|
||||
import {Commits, DiffInfo} from './commits'
|
||||
import {buildChangelog} from './transform'
|
||||
import * as fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export interface ReleaseNotesOptions {
|
||||
owner: string // the owner of the repository
|
||||
@@ -43,114 +45,166 @@ export class ReleaseNotesBuilder {
|
||||
private fetchReviewers: boolean = false,
|
||||
private fetchReleaseInformation: boolean = false,
|
||||
private fetchReviews: boolean = false,
|
||||
private commitMode: boolean,
|
||||
private commitMode: boolean = false,
|
||||
private exportCollected: boolean = false,
|
||||
private exportOnly: boolean = false,
|
||||
private configuration: Configuration
|
||||
) {}
|
||||
|
||||
async build(): Promise<string | null> {
|
||||
if (!this.owner) {
|
||||
failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError)
|
||||
return null
|
||||
} else {
|
||||
core.setOutput('owner', this.owner)
|
||||
core.debug(`Resolved 'owner' as ${this.owner}`)
|
||||
}
|
||||
let releaseNotesData = checkExportedData()
|
||||
if (releaseNotesData == null) {
|
||||
if (!this.owner) {
|
||||
failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError)
|
||||
return null
|
||||
} else {
|
||||
core.setOutput('owner', this.owner)
|
||||
core.debug(`Resolved 'owner' as ${this.owner}`)
|
||||
}
|
||||
|
||||
if (!this.repo) {
|
||||
failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError)
|
||||
return null
|
||||
} else {
|
||||
core.setOutput('repo', this.repo)
|
||||
core.debug(`Resolved 'repo' as ${this.repo}`)
|
||||
}
|
||||
core.endGroup()
|
||||
if (!this.repo) {
|
||||
failOrError(`💥 Missing or couldn't resolve 'owner'`, this.failOnError)
|
||||
return null
|
||||
} else {
|
||||
core.setOutput('repo', this.repo)
|
||||
core.debug(`Resolved 'repo' as ${this.repo}`)
|
||||
}
|
||||
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(',')
|
||||
}
|
||||
// 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
|
||||
// load octokit instance
|
||||
const octokit = new Octokit({
|
||||
auth: `token ${this.token || process.env.GITHUB_TOKEN}`,
|
||||
baseUrl: `${this.baseUrl || 'https://api.github.com'}`
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
if (proxy) {
|
||||
const agent = new HttpsProxyAgent(proxy)
|
||||
octokit.hook.before('request', options => {
|
||||
if (noProxyArray.includes(options.request.hostname)) {
|
||||
return
|
||||
}
|
||||
options.request.agent = agent
|
||||
})
|
||||
}
|
||||
|
||||
let thisTag = tagRange.to
|
||||
if (!thisTag) {
|
||||
failOrError(`💥 Missing or couldn't resolve 'toTag'`, this.failOnError)
|
||||
return null
|
||||
// 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
|
||||
}
|
||||
|
||||
releaseNotesData = await pullData(octokit, options, this.exportCollected, this.exportOnly)
|
||||
} else {
|
||||
core.setOutput('toTag', thisTag.name)
|
||||
core.debug(`Resolved 'toTag' as ${thisTag.name}`)
|
||||
}
|
||||
core.info(`ℹ️ Retrieved previously exported collected data`)
|
||||
|
||||
let previousTag = tagRange.from
|
||||
if (previousTag == null) {
|
||||
failOrError(`💥 Unable to retrieve previous tag given ${this.toTag}`, this.failOnError)
|
||||
// merge input with options (in case some data was updated)
|
||||
const diffInfo = releaseNotesData.diffInfo
|
||||
const mergedPullRequests = releaseNotesData.mergedPullRequests
|
||||
const orgOptions = releaseNotesData.options
|
||||
|
||||
// merge fromTag info with provided info || otherwise use cached info
|
||||
const fromTag: TagInfo = orgOptions.fromTag
|
||||
if (this.fromTag != null) {
|
||||
fromTag.name = this.fromTag
|
||||
}
|
||||
const toTag: TagInfo = orgOptions.toTag
|
||||
if (this.toTag != null) {
|
||||
toTag.name = this.toTag
|
||||
}
|
||||
|
||||
// merge provided values with previous options (prefer provided)
|
||||
const options: ReleaseNotesOptions = {
|
||||
owner: this.owner || orgOptions.owner,
|
||||
repo: this.repo || orgOptions.repo,
|
||||
fromTag,
|
||||
toTag,
|
||||
includeOpen: this.includeOpen || orgOptions.includeOpen,
|
||||
failOnError: this.failOnError || orgOptions.failOnError,
|
||||
fetchReviewers: this.fetchReviewers || orgOptions.fetchReviewers,
|
||||
fetchReleaseInformation: this.fetchReleaseInformation || orgOptions.fetchReleaseInformation,
|
||||
fetchReviews: this.fetchReviews || orgOptions.fetchReviews,
|
||||
commitMode: this.commitMode || orgOptions.commitMode,
|
||||
configuration: this.configuration || orgOptions.configuration
|
||||
}
|
||||
|
||||
releaseNotesData = {
|
||||
diffInfo,
|
||||
mergedPullRequests,
|
||||
options
|
||||
}
|
||||
}
|
||||
if (releaseNotesData != null) {
|
||||
return buildChangelog(releaseNotesData.diffInfo, releaseNotesData.mergedPullRequests, releaseNotesData.options)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
const releaseNotesData = await pullData(octokit, options)
|
||||
return buildChangelog(releaseNotesData.diffInfo, releaseNotesData.mergedPullRequests, releaseNotesData.options)
|
||||
}
|
||||
}
|
||||
|
||||
export async function pullData(octokit: Octokit, options: ReleaseNotesOptions): Promise<ReleaseNotesData> {
|
||||
export async function pullData(
|
||||
octokit: Octokit,
|
||||
options: ReleaseNotesOptions,
|
||||
exportCollected: boolean,
|
||||
exportOnly: boolean
|
||||
): Promise<ReleaseNotesData | null> {
|
||||
let mergedPullRequests: PullRequestInfo[]
|
||||
let diffInfo: DiffInfo
|
||||
|
||||
@@ -184,12 +238,19 @@ export async function pullData(octokit: Octokit, options: ReleaseNotesOptions):
|
||||
core.setOutput('changes', diffInfo.changes)
|
||||
core.setOutput('commits', diffInfo.commits)
|
||||
|
||||
const collectAndExport = true
|
||||
if (collectAndExport) {
|
||||
if (exportCollected) {
|
||||
core.info('📦 Exporting collected data')
|
||||
core.exportVariable('_diffInfo', JSON.stringify(diffInfo))
|
||||
core.exportVariable('_mergedPullRequests', JSON.stringify(mergedPullRequests))
|
||||
core.exportVariable('_options', JSON.stringify(options))
|
||||
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()
|
||||
|
||||
+6
-6
@@ -3,7 +3,7 @@ import {Category, Configuration, Placeholder, Property, Transformer} from './con
|
||||
import {CommentInfo, EMPTY_COMMENT_INFO, PullRequestInfo, retrieveProperty, sortPullRequests} from './pullRequests'
|
||||
import {ReleaseNotesOptions} from './releaseNotesBuilder'
|
||||
import {DiffInfo} from './commits'
|
||||
import {createOrSet, haveCommonElements, haveEveryElements} from './utils'
|
||||
import {createOrSet, haveCommonElementsArr, haveEveryElementsArr} from './utils'
|
||||
import {matchesRules, RegexTransformer, validateTransformer} from './regexUtils'
|
||||
|
||||
const EMPTY_MAP = new Map<string, string>()
|
||||
@@ -57,7 +57,7 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
const extracted = extractValues(pr, extractor, 'label_extractor')
|
||||
if (extracted !== null) {
|
||||
for (const label of extracted) {
|
||||
pr.labels.add(label)
|
||||
pr.labels.push(label)
|
||||
}
|
||||
|
||||
if (core.isDebug()) {
|
||||
@@ -100,7 +100,7 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
// bring elements in order
|
||||
for (const [pr, body] of transformedMap) {
|
||||
if (
|
||||
haveCommonElements(
|
||||
haveCommonElementsArr(
|
||||
ignoredLabels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||
pr.labels
|
||||
)
|
||||
@@ -119,7 +119,7 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
// check if any exclude label matches
|
||||
if (category.exclude_labels !== undefined) {
|
||||
if (
|
||||
haveCommonElements(
|
||||
haveCommonElementsArr(
|
||||
category.exclude_labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||
pr.labels
|
||||
)
|
||||
@@ -136,7 +136,7 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
// validate for an exhaustive match (e.g. every provided rule applies)
|
||||
if (category.exhaustive === true && (category.labels !== undefined || category.rules !== undefined)) {
|
||||
if (category.labels !== undefined) {
|
||||
matched = haveEveryElements(
|
||||
matched = haveEveryElementsArr(
|
||||
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||
pr.labels
|
||||
)
|
||||
@@ -152,7 +152,7 @@ export function buildChangelog(diffInfo: DiffInfo, prs: PullRequestInfo[], optio
|
||||
// if not exhaustive, do individual matches
|
||||
if (category.labels !== undefined) {
|
||||
// check if either any of the labels applies
|
||||
matched = haveCommonElements(
|
||||
matched = haveCommonElementsArr(
|
||||
category.labels.map(lbl => lbl.toLocaleLowerCase('en')),
|
||||
pr.labels
|
||||
)
|
||||
|
||||
@@ -2,6 +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'
|
||||
/**
|
||||
* Resolves the repository path, relatively to the GITHUB_WORKSPACE
|
||||
*/
|
||||
@@ -32,6 +36,45 @@ export function failOrError(message: string | Error, failOnError: boolean): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the exported information from a previous run of the `release-changelog-builder-action`.
|
||||
* If available, return a [ReleaseNotesData].
|
||||
*/
|
||||
export function checkExportedData(): ReleaseNotesData | null {
|
||||
const rawDiffInfo = process.env[`RCBA_EXPORT_diffInfo`]
|
||||
const rawMergedPullRequests = process.env[`RCBA_EXPORT_mergedPullRequests`]
|
||||
const rawOptions = process.env[`RCBA_EXPORT_options`]
|
||||
|
||||
if (rawDiffInfo && rawMergedPullRequests && rawOptions) {
|
||||
const diffInfo: DiffInfo = JSON.parse(rawDiffInfo)
|
||||
const mergedPullRequests: PullRequestInfo[] = JSON.parse(rawMergedPullRequests)
|
||||
|
||||
for (const pr of mergedPullRequests) {
|
||||
pr.createdAt = moment(pr.createdAt)
|
||||
if (pr.mergedAt) {
|
||||
pr.mergedAt = moment(pr.mergedAt)
|
||||
}
|
||||
|
||||
if (pr.reviews) {
|
||||
for (const review of pr.reviews) {
|
||||
if (review.submittedAt) {
|
||||
review.submittedAt = moment(review.submittedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options: ReleaseNotesOptions = JSON.parse(rawOptions)
|
||||
return {
|
||||
diffInfo,
|
||||
mergedPullRequests,
|
||||
options
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configuration given the file path, if not found it will fallback to the `DefaultConfiguration`
|
||||
*/
|
||||
@@ -172,6 +215,14 @@ export function haveCommonElements(arr1: string[], arr2: Set<string>): boolean {
|
||||
return arr1.some(item => arr2.has(item))
|
||||
}
|
||||
|
||||
export function haveCommonElementsArr(arr1: string[], arr2: string[]): boolean {
|
||||
return haveCommonElements(arr1, new Set(arr2))
|
||||
}
|
||||
|
||||
export function haveEveryElements(arr1: string[], arr2: Set<string>): boolean {
|
||||
return arr1.every(item => arr2.has(item))
|
||||
}
|
||||
|
||||
export function haveEveryElementsArr(arr1: string[], arr2: string[]): boolean {
|
||||
return haveEveryElements(arr1, new Set(arr2))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user