style: code formatting

This commit is contained in:
Ankio
2023-11-01 11:05:11 +08:00
parent 08cf89bc11
commit e08045ee77
13 changed files with 520 additions and 516 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import {Rule, Extractor, Regex, Transformer, Sort, PullConfiguration} from './pr-collector/types' import {Extractor, PullConfiguration, Regex, Rule, Sort, Transformer} from './pr-collector/types'
export interface Configuration extends PullConfiguration { export interface Configuration extends PullConfiguration {
max_tags_to_fetch: number max_tags_to_fetch: number
+118 -120
View File
@@ -1,155 +1,153 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import moment from 'moment' import moment from 'moment'
import {failOrError} from './utils' import {failOrError} from './utils'
import {PullRequestInfo} from './pullRequests' import {PullRequestInfo} from './pullRequests'
import {Options} from './prCollector' import {Options} from './prCollector'
import {BaseRepository} from "../repositories/BaseRepository"; import {BaseRepository} from '../repositories/BaseRepository'
export interface DiffInfo { export interface DiffInfo {
changedFiles: number changedFiles: number
additions: number additions: number
deletions: number deletions: number
changes: number changes: number
commits: number commits: number
commitInfo: CommitInfo[] commitInfo: CommitInfo[]
} }
export const DefaultDiffInfo: DiffInfo = { export const DefaultDiffInfo: DiffInfo = {
changedFiles: 0, changedFiles: 0,
additions: 0, additions: 0,
deletions: 0, deletions: 0,
changes: 0, changes: 0,
commits: 0, commits: 0,
commitInfo: [] commitInfo: []
} }
export interface CommitInfo { export interface CommitInfo {
sha: string sha: string
summary: string summary: string
message: string message: string
author: string author: string
authorDate: moment.Moment authorDate: moment.Moment
committer: string committer: string
commitDate: moment.Moment commitDate: moment.Moment
} }
export class Commits { export class Commits {
constructor(private repositoryUtils: BaseRepository) { constructor(private repositoryUtils: BaseRepository) {}
async getDiff(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> {
const diff: DiffInfo = await this.getDiffRemote(owner, repo, base, head)
diff.commitInfo = this.sortCommits(diff.commitInfo)
return diff
}
private async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> {
return this.repositoryUtils.getDiffRemote(owner, repo, base, head)
}
private sortCommits(commits: CommitInfo[]): CommitInfo[] {
const commitsResult = []
const shas: {[key: string]: boolean} = {}
for (const commit of commits) {
if (shas[commit.sha]) {
continue
}
shas[commit.sha] = true
commitsResult.push(commit)
} }
async getDiff(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> { commitsResult.sort((a, b) => {
const diff: DiffInfo = await this.getDiffRemote(owner, repo, base, head) if (a.commitDate.isBefore(b.commitDate)) {
diff.commitInfo = this.sortCommits(diff.commitInfo) return -1
return diff } else if (b.commitDate.isBefore(a.commitDate)) {
return 1
}
return 0
})
return commitsResult
}
async getCommitHistory(options: Options): Promise<DiffInfo> {
const {owner, repo, fromTag, toTag, failOnError} = options
core.info(`️ Comparing ${owner}/${repo} - '${fromTag.name}...${toTag.name}'`)
const commitsApi = new Commits(this.repositoryUtils)
let diffInfo: DiffInfo
try {
diffInfo = await commitsApi.getDiff(owner, repo, fromTag.name, toTag.name)
} catch (error) {
failOrError(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError)
return DefaultDiffInfo
}
if (diffInfo.commitInfo.length === 0) {
core.warning(`⚠️ No commits found between - ${fromTag.name}...${toTag.name}`)
return DefaultDiffInfo
} }
private async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> { return diffInfo
return this.repositoryUtils.getDiffRemote(owner, repo, base, head) }
async generateCommitPRs(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, configuration} = options
const diffInfo = await this.getCommitHistory(options)
const commits = diffInfo.commitInfo
if (commits.length === 0) {
return [diffInfo, []]
} }
private sortCommits(commits: CommitInfo[]): CommitInfo[] { const prCommits = filterCommits(commits, configuration.exclude_merge_branches)
const commitsResult = []
const shas: { [key: string]: boolean } = {}
for (const commit of commits) { core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`)
if (shas[commit.sha]) {
continue
}
shas[commit.sha] = true
commitsResult.push(commit)
}
commitsResult.sort((a, b) => { const prs = prCommits.map(function (commit): PullRequestInfo {
if (a.commitDate.isBefore(b.commitDate)) { return {
return -1 number: 0,
} else if (b.commitDate.isBefore(a.commitDate)) { title: commit.summary,
return 1 htmlURL: '',
} baseBranch: '',
return 0 createdAt: commit.commitDate,
}) mergedAt: commit.commitDate,
mergeCommitSha: commit.sha,
return commitsResult author: commit.author || '',
} repoName: '',
labels: [],
async getCommitHistory(options: Options): Promise<DiffInfo> { milestone: '',
const {owner, repo, fromTag, toTag, failOnError} = options body: commit.message || '',
core.info(`️ Comparing ${owner}/${repo} - '${fromTag.name}...${toTag.name}'`) assignees: [],
requestedReviewers: [],
const commitsApi = new Commits(this.repositoryUtils) approvedReviewers: [],
let diffInfo: DiffInfo status: 'merged'
try { }
diffInfo = await commitsApi.getDiff(owner, repo, fromTag.name, toTag.name) })
} catch (error) { return [diffInfo, prs]
failOrError(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`, failOnError) }
return DefaultDiffInfo
}
if (diffInfo.commitInfo.length === 0) {
core.warning(`⚠️ No commits found between - ${fromTag.name}...${toTag.name}`)
return DefaultDiffInfo
}
return diffInfo
}
async generateCommitPRs(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
const {owner, repo, configuration} = options
const diffInfo = await this.getCommitHistory(options)
const commits = diffInfo.commitInfo
if (commits.length === 0) {
return [diffInfo, []]
}
const prCommits = filterCommits(commits, configuration.exclude_merge_branches)
core.info(`️ Retrieved ${prCommits.length} commits for ${owner}/${repo}`)
const prs = prCommits.map(function (commit): PullRequestInfo {
return {
number: 0,
title: commit.summary,
htmlURL: '',
baseBranch: '',
createdAt: commit.commitDate,
mergedAt: commit.commitDate,
mergeCommitSha: commit.sha,
author: commit.author || '',
repoName: '',
labels: [],
milestone: '',
body: commit.message || '',
assignees: [],
requestedReviewers: [],
approvedReviewers: [],
status: 'merged'
}
})
return [diffInfo, prs]
}
} }
/** /**
* Filters out all commits which match the exclude pattern * Filters out all commits which match the exclude pattern
*/ */
export function filterCommits(commits: CommitInfo[], excludeMergeBranches: string[]): CommitInfo[] { export function filterCommits(commits: CommitInfo[], excludeMergeBranches: string[]): CommitInfo[] {
const filteredCommits = [] const filteredCommits = []
for (const commit of commits) { for (const commit of commits) {
if (excludeMergeBranches) { if (excludeMergeBranches) {
let matched = false let matched = false
for (const excludeMergeBranch of excludeMergeBranches) { for (const excludeMergeBranch of excludeMergeBranches) {
if (commit.summary.includes(excludeMergeBranch)) { if (commit.summary.includes(excludeMergeBranch)) {
matched = true matched = true
break break
}
}
if (matched) {
continue
}
} }
filteredCommits.push(commit) }
if (matched) {
continue
}
} }
filteredCommits.push(commit)
}
return filteredCommits return filteredCommits
} }
+1 -1
View File
@@ -39,7 +39,7 @@ class GitCommandManager {
return result return result
} }
async execGit(args: string[], allowAllExitCodes = false, silent = false): Promise<GitOutput> { async execGit(args: string[], allowAllExitCodes = false, silent = false): Promise<GitOutput> {
directoryExistsSync(this.workingDirectory, true) directoryExistsSync(this.workingDirectory, true)
const result = new GitOutput() const result = new GitOutput()
+5 -7
View File
@@ -1,12 +1,10 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {PullConfiguration} from './types' import {PullConfiguration} from './types'
import {Octokit} from '@octokit/rest'
import {TagInfo, Tags} from './tags' import {TagInfo, Tags} from './tags'
import {failOrError} from './utils' import {failOrError} from './utils'
import {HttpsProxyAgent} from 'https-proxy-agent'
import {PullRequestInfo, PullRequests} from './pullRequests' import {PullRequestInfo, PullRequests} from './pullRequests'
import {Commits, DiffInfo} from './commits' import {Commits, DiffInfo} from './commits'
import {BaseRepository} from "../repositories/BaseRepository"; import {BaseRepository} from '../repositories/BaseRepository'
export interface Options { export interface Options {
owner: string // the owner of the repository owner: string // the owner of the repository
@@ -33,7 +31,7 @@ export interface Data {
export class PullRequestCollector { export class PullRequestCollector {
constructor( constructor(
private baseUrl: string | null, private baseUrl: string | null,
private repositoryUtils: BaseRepository , private repositoryUtils: BaseRepository,
private repositoryPath: string, private repositoryPath: string,
private owner: string, private owner: string,
private repo: string, private repo: string,
@@ -47,7 +45,7 @@ export class PullRequestCollector {
private fetchReleaseInformation = false, private fetchReleaseInformation = false,
private fetchReviews = false, private fetchReviews = false,
private commitMode = false, private commitMode = false,
private configuration: PullConfiguration, private configuration: PullConfiguration
) {} ) {}
async build(): Promise<Data | null> { async build(): Promise<Data | null> {
@@ -93,7 +91,7 @@ export class PullRequestCollector {
core.endGroup() core.endGroup()
return await pullData( this.repositoryUtils, { return await pullData(this.repositoryUtils, {
owner: this.owner, owner: this.owner,
repo: this.repo, repo: this.repo,
fromTag: previousTag, fromTag: previousTag,
@@ -110,7 +108,7 @@ export class PullRequestCollector {
} }
} }
export async function pullData( repositoryUtils: BaseRepository , options: Options): Promise<Data | null> { export async function pullData(repositoryUtils: BaseRepository, options: Options): Promise<Data | null> {
let mergedPullRequests: PullRequestInfo[] let mergedPullRequests: PullRequestInfo[]
let diffInfo: DiffInfo let diffInfo: DiffInfo
+12 -11
View File
@@ -1,11 +1,10 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest' import {RestEndpointMethodTypes} from '@octokit/rest'
import {Unpacked} from './utils'
import moment from 'moment' import moment from 'moment'
import {Property, Sort} from './types' import {Property, Sort} from './types'
import {Commits, DiffInfo, filterCommits} from './commits' import {Commits, DiffInfo, filterCommits} from './commits'
import {Options} from './prCollector' import {Options} from './prCollector'
import {BaseRepository} from "../repositories/BaseRepository"; import {BaseRepository} from '../repositories/BaseRepository'
export interface PullRequestInfo { export interface PullRequestInfo {
number: number number: number
@@ -65,7 +64,7 @@ export const EMPTY_COMMENT_INFO: CommentInfo = {
state: undefined state: undefined
} }
export type PullData = RestEndpointMethodTypes['pulls']['get']['response']['data'] export type PullData = RestEndpointMethodTypes['pulls']['get']['response']['data']
export type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data'] export type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
@@ -77,17 +76,22 @@ export class PullRequests {
private commits: Commits private commits: Commits
) {} ) {}
async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> { async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
return sortPrs(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests)) return sortPrs(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests))
} }
async getBetweenDates(owner: string, repo: string, fromDate: moment.Moment, toDate: moment.Moment, maxPullRequests: number): Promise<PullRequestInfo[]> { async getBetweenDates(
return sortPrs(await this.repositoryUtils.getBetweenDates(owner, repo,fromDate,toDate,maxPullRequests)) owner: string,
repo: string,
fromDate: moment.Moment,
toDate: moment.Moment,
maxPullRequests: number
): Promise<PullRequestInfo[]> {
return sortPrs(await this.repositoryUtils.getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests))
} }
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> { async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
return sortPrs(await this.repositoryUtils.getOpen(owner, repo,maxPullRequests)) return sortPrs(await this.repositoryUtils.getOpen(owner, repo, maxPullRequests))
} }
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> { async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
@@ -215,8 +219,6 @@ export class PullRequests {
} }
} }
function sortPrs(pullRequests: PullRequestInfo[]): PullRequestInfo[] { function sortPrs(pullRequests: PullRequestInfo[]): PullRequestInfo[] {
return sortPullRequests(pullRequests, { return sortPullRequests(pullRequests, {
order: 'ASC', order: 'ASC',
@@ -281,4 +283,3 @@ export function retrieveProperty(pr: PullRequestInfo, property: Property, useCas
} }
return value return value
} }
+3 -3
View File
@@ -6,7 +6,7 @@ import {RegexTransformer, TagResolver, Transformer} from './types'
import {createCommandManager} from './gitHelper' import {createCommandManager} from './gitHelper'
import moment from 'moment' import moment from 'moment'
import {validateTransformer} from './regexUtils' import {validateTransformer} from './regexUtils'
import {BaseRepository} from "../repositories/BaseRepository"; import {BaseRepository} from '../repositories/BaseRepository'
export interface TagResult { export interface TagResult {
from: TagInfo | null from: TagInfo | null
@@ -28,11 +28,11 @@ export class Tags {
constructor(private repositoryUtils: BaseRepository) {} constructor(private repositoryUtils: BaseRepository) {}
async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> { async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
return this.repositoryUtils.getTags(owner,repo,maxTagsToFetch) return this.repositoryUtils.getTags(owner, repo, maxTagsToFetch)
} }
async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> { async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> {
return this.repositoryUtils.fillTagInformation(repositoryPath,owner,repo,tagInfo) return this.repositoryUtils.fillTagInformation(repositoryPath, owner, repo, tagInfo)
} }
async findPredecessorTag( async findPredecessorTag(
+35 -35
View File
@@ -1,63 +1,63 @@
export interface PullConfiguration { export interface PullConfiguration {
max_tags_to_fetch: number max_tags_to_fetch: number
max_pull_requests: number max_pull_requests: number
max_back_track_time_days: number max_back_track_time_days: number
exclude_merge_branches: string[] exclude_merge_branches: string[]
sort: Sort | string // "ASC" or "DESC" sort: Sort | string // "ASC" or "DESC"
tag_resolver: TagResolver tag_resolver: TagResolver
base_branches: string[] base_branches: string[]
} }
/** /**
* Defines the properties of the PullRequestInfo useable in different configurations * Defines the properties of the PullRequestInfo useable in different configurations
*/ */
export type Property = export type Property =
| 'number' | 'number'
| 'title' | 'title'
| 'branch' | 'branch'
| 'author' | 'author'
| 'labels' | 'labels'
| 'milestone' | 'milestone'
| 'body' | 'body'
| 'assignees' | 'assignees'
| 'requestedReviewers' | 'requestedReviewers'
| 'approvedReviewers' | 'approvedReviewers'
| 'status' | 'status'
export interface Rule extends Regex { export interface Rule extends Regex {
on_property?: Property // retrieve the property to apply the rule on 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)
} }
export interface TagResolver { export interface TagResolver {
method: string // semver, sort method: string // semver, sort
filter?: Regex // the regex to filter the tags, prior to sorting filter?: Regex // the regex to filter the tags, prior to sorting
transformer?: Transformer | Transformer[] // transforms the tag name using the regex, run after the filter transformer?: Transformer | Transformer[] // transforms the tag name using the regex, run after the filter
} }
export interface Regex { export interface Regex {
pattern: string // the regex pattern to match pattern: string // the regex pattern to match
flags?: string // the regex flag to use for RegExp flags?: string // the regex flag to use for RegExp
} }
export interface Transformer extends Regex { export interface Transformer extends Regex {
target?: string // the target string to transform the source string using the regex to target?: string // the target string to transform the source string using the regex to
} }
export interface Extractor extends Transformer { export interface Extractor extends Transformer {
on_property?: Property[] | Property | 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)
} }
export interface RegexTransformer { export interface RegexTransformer {
pattern: RegExp | null pattern: RegExp | null
target: string target: string
onProperty?: Property[] onProperty?: Property[]
method?: 'replace' | 'match' method?: 'replace' | 'match'
onEmpty?: string onEmpty?: string
} }
+1 -1
View File
@@ -1,5 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Rule, RegexTransformer} from './pr-collector/types' import {RegexTransformer, Rule} from './pr-collector/types'
import {PullRequestInfo, retrieveProperty} from './pr-collector/pullRequests' import {PullRequestInfo, retrieveProperty} from './pr-collector/pullRequests'
import {validateTransformer} from './pr-collector/regexUtils' import {validateTransformer} from './pr-collector/regexUtils'
+1 -2
View File
@@ -1,13 +1,12 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Configuration} from './configuration' import {Configuration} from './configuration'
import {checkExportedData, writeCacheData} from './utils' import {checkExportedData, writeCacheData} from './utils'
import {PullRequestData, buildChangelog} from './transform' import {buildChangelog, PullRequestData} from './transform'
import {PullRequestCollector} from './pr-collector/prCollector' import {PullRequestCollector} from './pr-collector/prCollector'
import {failOrError} from './pr-collector/utils' import {failOrError} from './pr-collector/utils'
import {TagInfo} from './pr-collector/tags' import {TagInfo} from './pr-collector/tags'
import {DiffInfo} from './pr-collector/commits' import {DiffInfo} from './pr-collector/commits'
import {PullRequestInfo} from './pr-collector/pullRequests' import {PullRequestInfo} from './pr-collector/pullRequests'
import * as fs from 'fs'
import {BaseRepository} from './repositories/BaseRepository' import {BaseRepository} from './repositories/BaseRepository'
export interface ReleaseNotesOptions { export interface ReleaseNotesOptions {
+51 -45
View File
@@ -1,60 +1,66 @@
import {TagInfo} from "../pr-collector/tags"; import {TagInfo} from '../pr-collector/tags'
import {DiffInfo} from "../pr-collector/commits"; import {DiffInfo} from '../pr-collector/commits'
import {Options} from "../pr-collector/prCollector"; import moment from 'moment/moment'
import moment from "moment/moment"; import {PullRequestInfo} from '../pr-collector/pullRequests'
import {PullRequestInfo} from "../pr-collector/pullRequests"; import * as core from '@actions/core'
import * as core from "@actions/core"; import {createCommandManager} from '../pr-collector/gitHelper'
import {createCommandManager} from "../pr-collector/gitHelper";
export abstract class BaseRepository { export abstract class BaseRepository {
proxy?: string; proxy?: string
noProxyArray: string[] noProxyArray: string[]
// Define an abstract getter for the default URL // Define an abstract getter for the default URL
abstract get defaultUrl(): string; abstract get defaultUrl(): string
abstract get homeUrl():string; abstract get homeUrl(): string
protected constructor(protected token: string, protected url: string | undefined,protected repositoryPath:string) { protected constructor(
this.proxy = process.env.https_proxy || process.env.HTTPS_PROXY protected token: string,
const noProxy = process.env.no_proxy || process.env.NO_PROXY protected url: string | undefined,
this.noProxyArray = [] protected repositoryPath: string
if (noProxy) { ) {
this.noProxyArray = noProxy.split(',') this.proxy = process.env.https_proxy || process.env.HTTPS_PROXY
} const noProxy = process.env.no_proxy || process.env.NO_PROXY
this.noProxyArray = []
if (noProxy) {
this.noProxyArray = noProxy.split(',')
} }
}
abstract getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> abstract getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]>
abstract fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> abstract fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo>
abstract getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> abstract getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo>
abstract getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]>
abstract getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> abstract getBetweenDates(
owner: string,
repo: string,
fromDate: moment.Moment,
toDate: moment.Moment,
maxPullRequests: number
): Promise<PullRequestInfo[]>
abstract getBetweenDates(owner: string, repo: string, fromDate: moment.Moment, toDate: moment.Moment, maxPullRequests: number): Promise<PullRequestInfo[]> abstract getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]>
abstract getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> abstract getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void>
abstract getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> protected async getTagByCreateTime(repositoryPath: string, tagInfo: TagInfo) {
core.info(`⚠️ No release information found for ${tagInfo.name}, trying to retrieve tag creation time as fallback.`)
const gitHelper = await createCommandManager(repositoryPath)
protected async getTagByCreateTime(repositoryPath: string, tagInfo: TagInfo) { const creationTimeString = await gitHelper.tagCreation(tagInfo.name)
core.info(`⚠️ No release information found for ${tagInfo.name}, trying to retrieve tag creation time as fallback.`) const creationTime = moment(creationTimeString)
const gitHelper = await createCommandManager(repositoryPath) if (creationTimeString !== null && creationTime.isValid()) {
const creationTimeString = await gitHelper.tagCreation(tagInfo.name) tagInfo.date = creationTime
const creationTime = moment(creationTimeString) core.info(
if (creationTimeString !== null && creationTime.isValid()) { `️ Resolved tag creation time (${creationTimeString}) from 'git for-each-ref --format="%(creatordate:rfc)" "refs/tags/${tagInfo.name}`
tagInfo.date = creationTime )
core.info( } else {
`️ Resolved tag creation time (${creationTimeString}) from 'git for-each-ref --format="%(creatordate:rfc)" "refs/tags/${tagInfo.name}` core.info(
) `⚠️ Could not retrieve tag creation time via git cli 'git for-each-ref --format="%(creatordate:rfc)" "refs/tags/${tagInfo.name}'`
} else { )
core.info(
`⚠️ Could not retrieve tag creation time via git cli 'git for-each-ref --format="%(creatordate:rfc)" "refs/tags/${tagInfo.name}'`
)
}
return tagInfo
} }
return tagInfo
}
} }
+287 -286
View File
@@ -1,302 +1,303 @@
import {BaseRepository} from "./BaseRepository"; import {BaseRepository} from './BaseRepository'
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest"; import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import {HttpsProxyAgent} from "https-proxy-agent"; import {HttpsProxyAgent} from 'https-proxy-agent'
import * as core from "@actions/core"; import * as core from '@actions/core'
import {TagInfo} from "../pr-collector/tags"; import {TagInfo} from '../pr-collector/tags'
import moment from "moment/moment"; import moment from 'moment/moment'
import {DiffInfo} from "../pr-collector/commits"; import {DiffInfo} from '../pr-collector/commits'
import { import {CommentInfo, PullData, PullRequestInfo, PullReviewsData, PullsListData} from '../pr-collector/pullRequests'
CommentInfo, PullData, import {Unpacked} from '../pr-collector/utils'
PullRequestInfo,
PullReviewsData, PullsListData
} from "../pr-collector/pullRequests";
import {Unpacked} from "../pr-collector/utils";
export class GithubRepository extends BaseRepository { export class GithubRepository extends BaseRepository {
async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> {
let changedFilesCount = 0
let additionCount = 0
let deletionCount = 0
let changeCount = 0
let commitCount = 0
async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> { // Fetch comparisons recursively until we don't find any commits
let changedFilesCount = 0 // This is because the GitHub API limits the number of commits returned in a single response.
let additionCount = 0 let commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = []
let deletionCount = 0 let compareHead = head
let changeCount = 0 // eslint-disable-next-line no-constant-condition
let commitCount = 0 while (true) {
const compareResult = await this.octokit.repos.compareCommits({
// Fetch comparisons recursively until we don't find any commits owner,
// This is because the GitHub API limits the number of commits returned in a single response. repo,
let commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = [] base,
let compareHead = head head: compareHead
// eslint-disable-next-line no-constant-condition })
while (true) { if (compareResult.data.total_commits === 0) {
const compareResult = await this.octokit.repos.compareCommits({ break
owner, }
repo, changedFilesCount += compareResult.data.files?.length ?? 0
base, const files = compareResult.data.files
head: compareHead if (files !== undefined) {
}) for (const file of files) {
if (compareResult.data.total_commits === 0) { additionCount += file.additions
break deletionCount += file.deletions
} changeCount += file.changes
changedFilesCount += compareResult.data.files?.length ?? 0
const files = compareResult.data.files
if (files !== undefined) {
for (const file of files) {
additionCount += file.additions
deletionCount += file.deletions
changeCount += file.changes
}
}
commitCount += compareResult.data.commits.length
commits = compareResult.data.commits.concat(commits)
compareHead = `${commits[0].sha}^`
}
core.info(`️ Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`)
return {
changedFiles: changedFilesCount,
additions: additionCount,
deletions: deletionCount,
changes: changeCount,
commits: commitCount,
commitInfo: commits
.filter(commit => commit.sha)
.map(commit => ({
sha: commit.sha || '',
summary: commit.commit.message.split('\n')[0],
message: commit.commit.message,
author: commit.author?.login || '',
authorDate: moment(commit.commit.author?.date),
committer: commit.committer?.login || '',
commitDate: moment(commit.commit.committer?.date),
prNumber: undefined
}))
} }
}
commitCount += compareResult.data.commits.length
commits = compareResult.data.commits.concat(commits)
compareHead = `${commits[0].sha}^`
} }
async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> { core.info(`️ Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`)
const mergedPRs: PullRequestInfo[] = []
const options = this.octokit.repos.listPullRequestsAssociatedWithCommit.endpoint.merge({
owner,
repo,
commit_sha,
per_page: `${Math.min(10, maxPullRequests)}`,
direction: 'desc'
})
for await (const response of this.octokit.paginate.iterator(options)) { return {
const prs: PullsListData = response.data as PullsListData changedFiles: changedFilesCount,
additions: additionCount,
for (const pr of prs) { deletions: deletionCount,
mergedPRs.push(this.mapPullRequest(pr, pr.merged_at ? 'merged' : 'open')) changes: changeCount,
} commits: commitCount,
} commitInfo: commits
return mergedPRs .filter(commit => commit.sha)
.map(commit => ({
sha: commit.sha || '',
summary: commit.commit.message.split('\n')[0],
message: commit.commit.message,
author: commit.author?.login || '',
authorDate: moment(commit.commit.author?.date),
committer: commit.committer?.login || '',
commitDate: moment(commit.commit.committer?.date),
prNumber: undefined
}))
} }
}
async getBetweenDates(owner: string, repo: string, fromDate: moment.Moment, toDate: moment.Moment, maxPullRequests: number): Promise<PullRequestInfo[]> { async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
const mergedPRs: PullRequestInfo[] = [] const mergedPRs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({ const options = this.octokit.repos.listPullRequestsAssociatedWithCommit.endpoint.merge({
owner, owner,
repo, repo,
state: 'closed', commit_sha,
sort: 'merged', per_page: `${Math.min(10, maxPullRequests)}`,
per_page: `${Math.min(100, maxPullRequests)}`, direction: 'desc'
direction: 'desc'
})
for await (const response of this.octokit.paginate.iterator(options)) {
const prs: PullsListData = response.data as PullsListData
for (const pr of prs.filter(p => !!p.merged_at)) {
mergedPRs.push(this.mapPullRequest(pr, 'merged'))
}
if (mergedPRs.length >= maxPullRequests) {
core.warning(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests} (1)`)
break // bail out early to not keep iterating forever
} else if (prs.length > 0) {
if (this.fetchedEnough(prs, fromDate)) {
return mergedPRs // bail out early to not keep iterating on PRs super old
}
} else {
core.debug(`⚠️ No more PRs retrieved from API. Fetched so far: ${mergedPRs.length}`)
break
}
}
return mergedPRs
}
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
const openPrs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({
owner,
repo,
state: 'open',
sort: 'created',
per_page: '100',
direction: 'desc'
})
for await (const response of this.octokit.paginate.iterator(options)) {
const prs: PullsListData = response.data as PullsListData
for (const pr of prs) {
openPrs.push(this.mapPullRequest(pr, 'open'))
}
const firstPR = prs[0]
if (firstPR === undefined || openPrs.length >= maxPullRequests) {
if (openPrs.length >= maxPullRequests) {
core.warning(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests} (2)`)
}
break // bail out early to not keep iterating forever
}
}
return openPrs
}
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
const options = this.octokit.pulls.listReviews.endpoint.merge({
owner,
repo,
pull_number: pr.number,
sort: 'created',
direction: 'desc'
})
const prReviews: CommentInfo[] = []
for await (const response of this.octokit.paginate.iterator(options)) {
const comments: PullReviewsData = response.data as PullReviewsData
for (const comment of comments) {
prReviews.push(this.mapComment(comment))
}
}
pr.reviews = prReviews
}
get defaultUrl(): string {
return "https://api.github.com";
}
private octokit: Octokit
constructor(token: string, url: string|undefined,repositoryPath:string) {
super(token, url,repositoryPath);
this.url = url || this.defaultUrl
// load octokit instance
this.octokit = new Octokit({
auth: `token ${this.token}`,
baseUrl: this.url
})
if (this.proxy) {
const agent = new HttpsProxyAgent(this.proxy)
this.octokit.hook.before('request', options => {
if (this.noProxyArray.includes(options.request.hostname)) {
return
}
options.request.agent = agent
})
}
}
async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
const tagsInfo: TagInfo[] = []
const options = this.octokit.repos.listTags.endpoint.merge({
owner,
repo,
direction: 'desc',
per_page: 100
})
for await (const response of this.octokit.paginate.iterator(options)) {
type TagsListData = RestEndpointMethodTypes['repos']['listTags']['response']['data']
const tags: TagsListData = response.data as TagsListData
for (const tag of tags) {
tagsInfo.push({
name: tag.name,
commit: tag.commit.sha
})
}
// for performance only fetch newest maxTagsToFetch tags!!
if (tagsInfo.length >= maxTagsToFetch) {
break
}
}
core.info(`️ Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`)
return tagsInfo
}
async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> {
const options = this.octokit.repos.getReleaseByTag.endpoint.merge({
owner,
repo,
tag: tagInfo.name
})
try {
const response = await this.octokit.request(options)
type ReleaseInformation = RestEndpointMethodTypes['repos']['getReleaseByTag']['response']['data']
const release: ReleaseInformation = response.data as ReleaseInformation
tagInfo.date = moment(release.created_at)
core.info(`️ Retrieved information about the release associated with ${tagInfo.name} from the GitHub API`)
} catch (error) {
tagInfo = await this.getTagByCreateTime(repositoryPath, tagInfo)
}
return tagInfo
}
// helper function to add a special open label to prs not merged.
private attachSpecialLabels(status: 'open' | 'merged', labels: string[]): string[] {
labels.push(`--rcba-${status}`)
return labels
}
private mapPullRequest = (pr: PullData | Unpacked<PullsListData>, status: 'open' | 'merged' = 'open'): PullRequestInfo => ({
number: pr.number,
title: pr.title,
htmlURL: pr.html_url,
baseBranch: pr.base.ref,
branch: pr.head.ref,
createdAt: moment(pr.created_at),
mergedAt: pr.merged_at ? moment(pr.merged_at) : undefined,
mergeCommitSha: pr.merge_commit_sha || '',
author: pr.user?.login || '',
repoName: pr.base.repo.full_name,
labels: this.attachSpecialLabels(status, pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || []),
milestone: pr.milestone?.title || '',
body: pr.body || '',
assignees: pr.assignees?.map(asignee => asignee?.login || '') || [],
requestedReviewers: pr.requested_reviewers?.map(reviewer => reviewer?.login || '') || [],
approvedReviewers: [],
reviews: undefined,
status
}) })
private mapComment = (comment: Unpacked<PullReviewsData>): CommentInfo => ({ for await (const response of this.octokit.paginate.iterator(options)) {
id: comment.id, const prs: PullsListData = response.data as PullsListData
htmlURL: comment.html_url,
submittedAt: comment.submitted_at ? moment(comment.submitted_at) : undefined, for (const pr of prs) {
author: comment.user?.login || '', mergedPRs.push(this.mapPullRequest(pr, pr.merged_at ? 'merged' : 'open'))
body: comment.body, }
state: comment.state }
return mergedPRs
}
async getBetweenDates(
owner: string,
repo: string,
fromDate: moment.Moment,
toDate: moment.Moment,
maxPullRequests: number
): Promise<PullRequestInfo[]> {
const mergedPRs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({
owner,
repo,
state: 'closed',
sort: 'merged',
per_page: `${Math.min(100, maxPullRequests)}`,
direction: 'desc'
}) })
private fetchedEnough(pullRequests: PullsListData, fromDate: moment.Moment): boolean { for await (const response of this.octokit.paginate.iterator(options)) {
for (let i = 0; i < Math.min(pullRequests.length, 3); i++) { const prs: PullsListData = response.data as PullsListData
const firstPR = pullRequests[i]
if (!firstPR.merged_at) { for (const pr of prs.filter(p => !!p.merged_at)) {
// no merged_at timestamp -> look for the next mergedPRs.push(this.mapPullRequest(pr, 'merged'))
} else if (fromDate.isAfter(moment(firstPR.merged_at))) { }
return true if (mergedPRs.length >= maxPullRequests) {
} else { core.warning(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests} (1)`)
break // not enough PRs yet, go further break // bail out early to not keep iterating forever
} } else if (prs.length > 0) {
if (this.fetchedEnough(prs, fromDate)) {
return mergedPRs // bail out early to not keep iterating on PRs super old
} }
return false } else {
core.debug(`⚠️ No more PRs retrieved from API. Fetched so far: ${mergedPRs.length}`)
break
}
} }
get homeUrl(): string { return mergedPRs
return "https://github.com"; }
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
const openPrs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({
owner,
repo,
state: 'open',
sort: 'created',
per_page: '100',
direction: 'desc'
})
for await (const response of this.octokit.paginate.iterator(options)) {
const prs: PullsListData = response.data as PullsListData
for (const pr of prs) {
openPrs.push(this.mapPullRequest(pr, 'open'))
}
const firstPR = prs[0]
if (firstPR === undefined || openPrs.length >= maxPullRequests) {
if (openPrs.length >= maxPullRequests) {
core.warning(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests} (2)`)
}
break // bail out early to not keep iterating forever
}
} }
return openPrs
}
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
const options = this.octokit.pulls.listReviews.endpoint.merge({
owner,
repo,
pull_number: pr.number,
sort: 'created',
direction: 'desc'
})
const prReviews: CommentInfo[] = []
for await (const response of this.octokit.paginate.iterator(options)) {
const comments: PullReviewsData = response.data as PullReviewsData
for (const comment of comments) {
prReviews.push(this.mapComment(comment))
}
}
pr.reviews = prReviews
}
get defaultUrl(): string {
return 'https://api.github.com'
}
private octokit: Octokit
constructor(token: string, url: string | undefined, repositoryPath: string) {
super(token, url, repositoryPath)
this.url = url || this.defaultUrl
// load octokit instance
this.octokit = new Octokit({
auth: `token ${this.token}`,
baseUrl: this.url
})
if (this.proxy) {
const agent = new HttpsProxyAgent(this.proxy)
this.octokit.hook.before('request', options => {
if (this.noProxyArray.includes(options.request.hostname)) {
return
}
options.request.agent = agent
})
}
}
async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
const tagsInfo: TagInfo[] = []
const options = this.octokit.repos.listTags.endpoint.merge({
owner,
repo,
direction: 'desc',
per_page: 100
})
for await (const response of this.octokit.paginate.iterator(options)) {
type TagsListData = RestEndpointMethodTypes['repos']['listTags']['response']['data']
const tags: TagsListData = response.data as TagsListData
for (const tag of tags) {
tagsInfo.push({
name: tag.name,
commit: tag.commit.sha
})
}
// for performance only fetch newest maxTagsToFetch tags!!
if (tagsInfo.length >= maxTagsToFetch) {
break
}
}
core.info(`️ Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`)
return tagsInfo
}
async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> {
const options = this.octokit.repos.getReleaseByTag.endpoint.merge({
owner,
repo,
tag: tagInfo.name
})
try {
const response = await this.octokit.request(options)
type ReleaseInformation = RestEndpointMethodTypes['repos']['getReleaseByTag']['response']['data']
const release: ReleaseInformation = response.data as ReleaseInformation
tagInfo.date = moment(release.created_at)
core.info(`️ Retrieved information about the release associated with ${tagInfo.name} from the GitHub API`)
} catch (error) {
tagInfo = await this.getTagByCreateTime(repositoryPath, tagInfo)
}
return tagInfo
}
// helper function to add a special open label to prs not merged.
private attachSpecialLabels(status: 'open' | 'merged', labels: string[]): string[] {
labels.push(`--rcba-${status}`)
return labels
}
private mapPullRequest = (pr: PullData | Unpacked<PullsListData>, status: 'open' | 'merged' = 'open'): PullRequestInfo => ({
number: pr.number,
title: pr.title,
htmlURL: pr.html_url,
baseBranch: pr.base.ref,
branch: pr.head.ref,
createdAt: moment(pr.created_at),
mergedAt: pr.merged_at ? moment(pr.merged_at) : undefined,
mergeCommitSha: pr.merge_commit_sha || '',
author: pr.user?.login || '',
repoName: pr.base.repo.full_name,
labels: this.attachSpecialLabels(status, pr.labels?.map(lbl => lbl.name?.toLocaleLowerCase('en') || '') || []),
milestone: pr.milestone?.title || '',
body: pr.body || '',
assignees: pr.assignees?.map(asignee => asignee?.login || '') || [],
requestedReviewers: pr.requested_reviewers?.map(reviewer => reviewer?.login || '') || [],
approvedReviewers: [],
reviews: undefined,
status
})
private mapComment = (comment: Unpacked<PullReviewsData>): CommentInfo => ({
id: comment.id,
htmlURL: comment.html_url,
submittedAt: comment.submitted_at ? moment(comment.submitted_at) : undefined,
author: comment.user?.login || '',
body: comment.body,
state: comment.state
})
private fetchedEnough(pullRequests: PullsListData, fromDate: moment.Moment): boolean {
for (let i = 0; i < Math.min(pullRequests.length, 3); i++) {
const firstPR = pullRequests[i]
if (!firstPR.merged_at) {
// no merged_at timestamp -> look for the next
} else if (fromDate.isAfter(moment(firstPR.merged_at))) {
return true
} else {
break // not enough PRs yet, go further
}
}
return false
}
get homeUrl(): string {
return 'https://github.com'
}
} }
+1 -1
View File
@@ -11,7 +11,7 @@ import {
} from './pr-collector/pullRequests' } from './pr-collector/pullRequests'
import {DiffInfo} from './pr-collector/commits' import {DiffInfo} from './pr-collector/commits'
import {validateTransformer} from './pr-collector/regexUtils' import {validateTransformer} from './pr-collector/regexUtils'
import {Transformer, RegexTransformer} from './pr-collector/types' import {RegexTransformer, Transformer} from './pr-collector/types'
import {ReleaseNotesOptions} from './releaseNotesBuilder' import {ReleaseNotesOptions} from './releaseNotesBuilder'
import {matchesRules} from './regexUtils' import {matchesRules} from './regexUtils'
+1
View File
@@ -7,6 +7,7 @@ import {DiffInfo} from './pr-collector/commits'
import {PullRequestInfo} from './pr-collector/pullRequests' import {PullRequestInfo} from './pr-collector/pullRequests'
import {Data, ReleaseNotesOptions} from './releaseNotesBuilder' import {Data, ReleaseNotesOptions} from './releaseNotesBuilder'
import {env} from 'process' import {env} from 'process'
/** /**
* Resolves the repository path, relatively to the GITHUB_WORKSPACE * Resolves the repository path, relatively to the GITHUB_WORKSPACE
*/ */