refactor: Extract github-related calls
This commit is contained in:
+1
-1
@@ -99,4 +99,4 @@ __tests__/runner/*
|
|||||||
lib/**/*
|
lib/**/*
|
||||||
|
|
||||||
lib
|
lib
|
||||||
pr-collector/dist
|
src/pr-collector/dist
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
|
|
||||||
import moment from 'moment'
|
|
||||||
import {failOrError} from './utils'
|
|
||||||
import {PullRequestInfo} from './pullRequests'
|
|
||||||
import {Options} from './prCollector'
|
|
||||||
|
|
||||||
export interface DiffInfo {
|
|
||||||
changedFiles: number
|
|
||||||
additions: number
|
|
||||||
deletions: number
|
|
||||||
changes: number
|
|
||||||
commits: number
|
|
||||||
commitInfo: CommitInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultDiffInfo: DiffInfo = {
|
|
||||||
changedFiles: 0,
|
|
||||||
additions: 0,
|
|
||||||
deletions: 0,
|
|
||||||
changes: 0,
|
|
||||||
commits: 0,
|
|
||||||
commitInfo: []
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommitInfo {
|
|
||||||
sha: string
|
|
||||||
summary: string
|
|
||||||
message: string
|
|
||||||
author: string
|
|
||||||
authorDate: moment.Moment
|
|
||||||
committer: string
|
|
||||||
commitDate: moment.Moment
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Commits {
|
|
||||||
constructor(private octokit: Octokit) {}
|
|
||||||
|
|
||||||
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> {
|
|
||||||
let changedFilesCount = 0
|
|
||||||
let additionCount = 0
|
|
||||||
let deletionCount = 0
|
|
||||||
let changeCount = 0
|
|
||||||
let commitCount = 0
|
|
||||||
|
|
||||||
// Fetch comparisons recursively until we don't find any commits
|
|
||||||
// This is because the GitHub API limits the number of commits returned in a single response.
|
|
||||||
let commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = []
|
|
||||||
let compareHead = head
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
const compareResult = await this.octokit.repos.compareCommits({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
base,
|
|
||||||
head: compareHead
|
|
||||||
})
|
|
||||||
if (compareResult.data.total_commits === 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
commitsResult.sort((a, b) => {
|
|
||||||
if (a.commitDate.isBefore(b.commitDate)) {
|
|
||||||
return -1
|
|
||||||
} 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.octokit)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
export function filterCommits(commits: CommitInfo[], excludeMergeBranches: string[]): CommitInfo[] {
|
|
||||||
const filteredCommits = []
|
|
||||||
|
|
||||||
for (const commit of commits) {
|
|
||||||
if (excludeMergeBranches) {
|
|
||||||
let matched = false
|
|
||||||
for (const excludeMergeBranch of excludeMergeBranches) {
|
|
||||||
if (commit.summary.includes(excludeMergeBranch)) {
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matched) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filteredCommits.push(commit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredCommits
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
export interface PullConfiguration {
|
|
||||||
max_tags_to_fetch: number
|
|
||||||
max_pull_requests: number
|
|
||||||
max_back_track_time_days: number
|
|
||||||
exclude_merge_branches: string[]
|
|
||||||
sort: Sort | string // "ASC" or "DESC"
|
|
||||||
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 | 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegexTransformer {
|
|
||||||
pattern: RegExp | null
|
|
||||||
target: string
|
|
||||||
onProperty?: Property[]
|
|
||||||
method?: 'replace' | 'match'
|
|
||||||
onEmpty?: string
|
|
||||||
}
|
|
||||||
+22
-2
@@ -3,13 +3,31 @@ import * as github from '@actions/github'
|
|||||||
import {mergeConfiguration, parseConfiguration, resolveConfiguration, retrieveRepositoryPath, writeOutput} from './utils'
|
import {mergeConfiguration, parseConfiguration, resolveConfiguration, retrieveRepositoryPath, writeOutput} from './utils'
|
||||||
import {ReleaseNotesBuilder} from './releaseNotesBuilder'
|
import {ReleaseNotesBuilder} from './releaseNotesBuilder'
|
||||||
import {Configuration} from './configuration'
|
import {Configuration} from './configuration'
|
||||||
|
import {GithubRepository} from "./repositories/GithubRepository";
|
||||||
|
import {GiteaRepository} from "./repositories/GiteaRepository";
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
|
|
||||||
|
const supportedPlatform = {
|
||||||
|
github: GithubRepository,
|
||||||
|
gitea: GiteaRepository,
|
||||||
|
};
|
||||||
|
function isSupportedPlatform(type: string): type is keyof typeof supportedPlatform {
|
||||||
|
return type in supportedPlatform;
|
||||||
|
}
|
||||||
core.setOutput('failed', false) // mark the action not failed by default
|
core.setOutput('failed', false) // mark the action not failed by default
|
||||||
|
|
||||||
core.startGroup(`📘 Reading input values`)
|
core.startGroup(`📘 Reading input values`)
|
||||||
try {
|
try {
|
||||||
// read in path specification, resolve github workspace, and repo path
|
// read in path specification, resolve github workspace, and repo path
|
||||||
|
const platform = core.getInput('platform') || "github"
|
||||||
|
if(!isSupportedPlatform(platform)){
|
||||||
|
core.setFailed(`The ${platform} platform is not supported. `)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const inputPath = core.getInput('path')
|
const inputPath = core.getInput('path')
|
||||||
const repositoryPath = retrieveRepositoryPath(inputPath)
|
const repositoryPath = retrieveRepositoryPath(inputPath)
|
||||||
|
|
||||||
@@ -40,7 +58,7 @@ async function run(): Promise<void> {
|
|||||||
|
|
||||||
// read in repository inputs
|
// read in repository inputs
|
||||||
const baseUrl = core.getInput('baseUrl')
|
const baseUrl = core.getInput('baseUrl')
|
||||||
const token = core.getInput('token')
|
const token = core.getInput('token') || process.env.GITHUB_TOKEN || ""
|
||||||
const owner = core.getInput('owner') || github.context.repo.owner
|
const owner = core.getInput('owner') || github.context.repo.owner
|
||||||
const repo = core.getInput('repo') || github.context.repo.repo
|
const repo = core.getInput('repo') || github.context.repo.repo
|
||||||
// read in from, to tag inputs
|
// read in from, to tag inputs
|
||||||
@@ -59,9 +77,11 @@ async function run(): Promise<void> {
|
|||||||
const exportOnly = core.getInput('exportOnly') === 'true'
|
const exportOnly = core.getInput('exportOnly') === 'true'
|
||||||
const cache = core.getInput('cache')
|
const cache = core.getInput('cache')
|
||||||
|
|
||||||
|
|
||||||
|
const repositoryUtils = new supportedPlatform[platform](token,baseUrl);
|
||||||
const result = await new ReleaseNotesBuilder(
|
const result = await new ReleaseNotesBuilder(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
token,
|
repositoryUtils,
|
||||||
repositoryPath,
|
repositoryPath,
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../pr-collector/src/
|
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
|
||||||
|
import moment from 'moment'
|
||||||
|
import {failOrError} from './utils'
|
||||||
|
import {PullRequestInfo} from './pullRequests'
|
||||||
|
import {Options} from './prCollector'
|
||||||
|
import {BaseRepository} from "../repositories/BaseRepository";
|
||||||
|
|
||||||
|
export interface DiffInfo {
|
||||||
|
changedFiles: number
|
||||||
|
additions: number
|
||||||
|
deletions: number
|
||||||
|
changes: number
|
||||||
|
commits: number
|
||||||
|
commitInfo: CommitInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultDiffInfo: DiffInfo = {
|
||||||
|
changedFiles: 0,
|
||||||
|
additions: 0,
|
||||||
|
deletions: 0,
|
||||||
|
changes: 0,
|
||||||
|
commits: 0,
|
||||||
|
commitInfo: []
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommitInfo {
|
||||||
|
sha: string
|
||||||
|
summary: string
|
||||||
|
message: string
|
||||||
|
author: string
|
||||||
|
authorDate: moment.Moment
|
||||||
|
committer: string
|
||||||
|
commitDate: moment.Moment
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Commits {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
commitsResult.sort((a, b) => {
|
||||||
|
if (a.commitDate.isBefore(b.commitDate)) {
|
||||||
|
return -1
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export function filterCommits(commits: CommitInfo[], excludeMergeBranches: string[]): CommitInfo[] {
|
||||||
|
const filteredCommits = []
|
||||||
|
|
||||||
|
for (const commit of commits) {
|
||||||
|
if (excludeMergeBranches) {
|
||||||
|
let matched = false
|
||||||
|
for (const excludeMergeBranch of excludeMergeBranches) {
|
||||||
|
if (commit.summary.includes(excludeMergeBranch)) {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filteredCommits.push(commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredCommits
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {failOrError} from './utils'
|
|||||||
import {HttpsProxyAgent} from 'https-proxy-agent'
|
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";
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
owner: string // the owner of the repository
|
owner: string // the owner of the repository
|
||||||
@@ -32,7 +33,7 @@ export interface Data {
|
|||||||
export class PullRequestCollector {
|
export class PullRequestCollector {
|
||||||
constructor(
|
constructor(
|
||||||
private baseUrl: string | null,
|
private baseUrl: string | null,
|
||||||
private token: string | null,
|
private repositoryUtils: BaseRepository ,
|
||||||
private repositoryPath: string,
|
private repositoryPath: string,
|
||||||
private owner: string,
|
private owner: string,
|
||||||
private repo: string,
|
private repo: string,
|
||||||
@@ -46,37 +47,15 @@ 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> {
|
||||||
// check proxy setup for GHES environments
|
// 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
|
// ensure proper from <-> to tag range
|
||||||
core.startGroup(`🔖 Resolve tags`)
|
core.startGroup(`🔖 Resolve tags`)
|
||||||
const tagsApi = new Tags(octokit)
|
const tagsApi = new Tags(this.repositoryUtils)
|
||||||
const tagRange = await tagsApi.retrieveRange(
|
const tagRange = await tagsApi.retrieveRange(
|
||||||
this.repositoryPath,
|
this.repositoryPath,
|
||||||
this.owner,
|
this.owner,
|
||||||
@@ -114,7 +93,7 @@ export class PullRequestCollector {
|
|||||||
|
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
return await pullData(octokit, {
|
return await pullData( this.repositoryUtils, {
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
repo: this.repo,
|
repo: this.repo,
|
||||||
fromTag: previousTag,
|
fromTag: previousTag,
|
||||||
@@ -131,14 +110,14 @@ export class PullRequestCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pullData(octokit: Octokit, 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
|
||||||
|
|
||||||
const commitsApi = new Commits(octokit)
|
const commitsApi = new Commits(repositoryUtils)
|
||||||
if (!options.commitMode) {
|
if (!options.commitMode) {
|
||||||
core.startGroup(`🚀 Load pull requests`)
|
core.startGroup(`🚀 Load pull requests`)
|
||||||
const pullRequestsApi = new PullRequests(octokit, commitsApi)
|
const pullRequestsApi = new PullRequests(repositoryUtils, commitsApi)
|
||||||
const [info, prs] = await pullRequestsApi.getMergedPullRequests(options)
|
const [info, prs] = await pullRequestsApi.getMergedPullRequests(options)
|
||||||
mergedPullRequests = prs
|
mergedPullRequests = prs
|
||||||
diffInfo = info
|
diffInfo = info
|
||||||
Executable → Regular
+13
-121
@@ -5,6 +5,7 @@ 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";
|
||||||
|
|
||||||
export interface PullRequestInfo {
|
export interface PullRequestInfo {
|
||||||
number: number
|
number: number
|
||||||
@@ -64,142 +65,33 @@ export const EMPTY_COMMENT_INFO: CommentInfo = {
|
|||||||
state: undefined
|
state: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type PullData = RestEndpointMethodTypes['pulls']['get']['response']['data']
|
export type PullData = RestEndpointMethodTypes['pulls']['get']['response']['data']
|
||||||
|
|
||||||
type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
|
export type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
|
||||||
|
|
||||||
type PullReviewsData = RestEndpointMethodTypes['pulls']['listReviews']['response']['data']
|
export type PullReviewsData = RestEndpointMethodTypes['pulls']['listReviews']['response']['data']
|
||||||
|
|
||||||
export class PullRequests {
|
export class PullRequests {
|
||||||
constructor(
|
constructor(
|
||||||
private octokit: Octokit,
|
private repositoryUtils: BaseRepository,
|
||||||
private commits: Commits
|
private commits: Commits
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getSingle(owner: string, repo: string, prNumber: number): Promise<PullRequestInfo | null> {
|
|
||||||
try {
|
|
||||||
const {data} = await this.octokit.pulls.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: prNumber
|
|
||||||
})
|
|
||||||
|
|
||||||
return mapPullRequest(data)
|
|
||||||
} catch (e: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
|
|
||||||
core.warning(`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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[]> {
|
||||||
const mergedPRs: PullRequestInfo[] = []
|
return sortPrs(await this.repositoryUtils.getForCommitHash(owner, repo, commit_sha, maxPullRequests))
|
||||||
|
|
||||||
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)) {
|
|
||||||
const prs: PullsListData = response.data as PullsListData
|
|
||||||
|
|
||||||
for (const pr of prs) {
|
|
||||||
mergedPRs.push(mapPullRequest(pr, pr.merged_at ? 'merged' : 'open'))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortPrs(mergedPRs)
|
async getBetweenDates(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 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'
|
|
||||||
})
|
|
||||||
|
|
||||||
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(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 (fetchedEnough(prs, fromDate)) {
|
|
||||||
return sortPrs(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 sortPrs(mergedPRs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
async getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
const openPrs: PullRequestInfo[] = []
|
return sortPrs(await this.repositoryUtils.getOpen(owner, repo,maxPullRequests))
|
||||||
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(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 sortPrs(openPrs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
|
async getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
|
||||||
const options = this.octokit.pulls.listReviews.endpoint.merge({
|
await this.repositoryUtils.getReviews(owner, repo, pr)
|
||||||
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(mapComment(comment))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pr.reviews = prReviews
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMergedPullRequests(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
|
async getMergedPullRequests(options: Options): Promise<[DiffInfo, PullRequestInfo[]]> {
|
||||||
@@ -323,7 +215,7 @@ export class PullRequests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchedEnough(pullRequests: PullsListData, fromDate: moment.Moment): boolean {
|
export function fetchedEnough(pullRequests: PullsListData, fromDate: moment.Moment): boolean {
|
||||||
for (let i = 0; i < Math.min(pullRequests.length, 3); i++) {
|
for (let i = 0; i < Math.min(pullRequests.length, 3); i++) {
|
||||||
const firstPR = pullRequests[i]
|
const firstPR = pullRequests[i]
|
||||||
if (!firstPR.merged_at) {
|
if (!firstPR.merged_at) {
|
||||||
@@ -408,7 +300,7 @@ function attachSpeciaLabels(status: 'open' | 'merged', labels: string[]): string
|
|||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapPullRequest = (pr: PullData | Unpacked<PullsListData>, status: 'open' | 'merged' = 'open'): PullRequestInfo => ({
|
export const mapPullRequest = (pr: PullData | Unpacked<PullsListData>, status: 'open' | 'merged' = 'open'): PullRequestInfo => ({
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
title: pr.title,
|
title: pr.title,
|
||||||
htmlURL: pr.html_url,
|
htmlURL: pr.html_url,
|
||||||
@@ -429,7 +321,7 @@ const mapPullRequest = (pr: PullData | Unpacked<PullsListData>, status: 'open' |
|
|||||||
status
|
status
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapComment = (comment: Unpacked<PullReviewsData>): CommentInfo => ({
|
export const mapComment = (comment: Unpacked<PullReviewsData>): CommentInfo => ({
|
||||||
id: comment.id,
|
id: comment.id,
|
||||||
htmlURL: comment.html_url,
|
htmlURL: comment.html_url,
|
||||||
submittedAt: comment.submitted_at ? moment(comment.submitted_at) : undefined,
|
submittedAt: comment.submitted_at ? moment(comment.submitted_at) : undefined,
|
||||||
Executable → Regular
+4
-59
@@ -1,12 +1,12 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as semver from 'semver'
|
import * as semver from 'semver'
|
||||||
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
|
|
||||||
import {SemVer} from 'semver'
|
import {SemVer} from 'semver'
|
||||||
import {RegexTransformer, TagResolver, Transformer} from './types'
|
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";
|
||||||
|
|
||||||
export interface TagResult {
|
export interface TagResult {
|
||||||
from: TagInfo | null
|
from: TagInfo | null
|
||||||
@@ -25,69 +25,14 @@ export interface SortableTagInfo extends TagInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Tags {
|
export class Tags {
|
||||||
constructor(private octokit: Octokit) {}
|
constructor(private repositoryUtils: BaseRepository) {}
|
||||||
|
|
||||||
async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
|
async getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
|
||||||
const tagsInfo: TagInfo[] = []
|
return this.repositoryUtils.getTags(owner,repo,maxTagsToFetch)
|
||||||
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> {
|
async fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> {
|
||||||
const options = this.octokit.repos.getReleaseByTag.endpoint.merge({
|
return this.repositoryUtils.fillTagInformation(repositoryPath,owner,repo,tagInfo)
|
||||||
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) {
|
|
||||||
core.info(`⚠️ No release information found for ${tagInfo.name}, trying to retrieve tag creation time as fallback.`)
|
|
||||||
const gitHelper = await createCommandManager(repositoryPath)
|
|
||||||
const creationTimeString = await gitHelper.tagCreation(tagInfo.name)
|
|
||||||
const creationTime = moment(creationTimeString)
|
|
||||||
if (creationTimeString !== null && creationTime.isValid()) {
|
|
||||||
tagInfo.date = creationTime
|
|
||||||
core.info(
|
|
||||||
`ℹ️ Resolved tag creation time (${creationTimeString}) from '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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findPredecessorTag(
|
async findPredecessorTag(
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
export interface PullConfiguration {
|
||||||
|
max_tags_to_fetch: number
|
||||||
|
max_pull_requests: number
|
||||||
|
max_back_track_time_days: number
|
||||||
|
exclude_merge_branches: string[]
|
||||||
|
sort: Sort | string // "ASC" or "DESC"
|
||||||
|
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 | 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexTransformer {
|
||||||
|
pattern: RegExp | null
|
||||||
|
target: string
|
||||||
|
onProperty?: Property[]
|
||||||
|
method?: 'replace' | 'match'
|
||||||
|
onEmpty?: string
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ 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 * as fs from 'fs'
|
||||||
|
import {BaseRepository} from "./repositories/BaseRepository";
|
||||||
|
|
||||||
export interface ReleaseNotesOptions {
|
export interface ReleaseNotesOptions {
|
||||||
owner: string // the owner of the repository
|
owner: string // the owner of the repository
|
||||||
@@ -32,7 +33,7 @@ export interface Data {
|
|||||||
export class ReleaseNotesBuilder {
|
export class ReleaseNotesBuilder {
|
||||||
constructor(
|
constructor(
|
||||||
private baseUrl: string | null,
|
private baseUrl: string | null,
|
||||||
private token: string | null,
|
private repositoryUtils: BaseRepository,
|
||||||
private repositoryPath: string,
|
private repositoryPath: string,
|
||||||
private owner: string | null,
|
private owner: string | null,
|
||||||
private repo: string | null,
|
private repo: string | null,
|
||||||
@@ -78,7 +79,7 @@ export class ReleaseNotesBuilder {
|
|||||||
|
|
||||||
const prData = await new PullRequestCollector(
|
const prData = await new PullRequestCollector(
|
||||||
this.baseUrl,
|
this.baseUrl,
|
||||||
this.token,
|
this.repositoryUtils,
|
||||||
this.repositoryPath,
|
this.repositoryPath,
|
||||||
this.owner,
|
this.owner,
|
||||||
this.repo,
|
this.repo,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {TagInfo} from "../pr-collector/tags";
|
||||||
|
import {DiffInfo} from "../pr-collector/commits";
|
||||||
|
import {Options} from "../pr-collector/prCollector";
|
||||||
|
import moment from "moment/moment";
|
||||||
|
import {PullRequestInfo} from "../pr-collector/pullRequests";
|
||||||
|
|
||||||
|
export abstract class BaseRepository {
|
||||||
|
proxy?: string;
|
||||||
|
noProxyArray: string[]
|
||||||
|
|
||||||
|
// Define an abstract getter for the default URL
|
||||||
|
abstract get defaultUrl(): string;
|
||||||
|
|
||||||
|
protected constructor(protected token: string, protected url?: string) {
|
||||||
|
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 fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo>
|
||||||
|
|
||||||
|
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 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 getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void>
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {BaseRepository} from "./BaseRepository";
|
||||||
|
import {TagInfo} from "../pr-collector/tags";
|
||||||
|
import {PullRequestInfo} from "../pr-collector/pullRequests";
|
||||||
|
import {DiffInfo} from "../pr-collector/commits";
|
||||||
|
|
||||||
|
export class GiteaRepository extends BaseRepository{
|
||||||
|
get defaultUrl(): string {
|
||||||
|
return "https://gitea.com/api/v1";
|
||||||
|
}
|
||||||
|
constructor(token: string, url?: string) {
|
||||||
|
super(token, url);
|
||||||
|
this.url = url || this.defaultUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
fillTagInformation(repositoryPath: string, owner: string, repo: string, tagInfo: TagInfo): Promise<TagInfo> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBetweenDates(owner: string, repo: string, fromDate: moment.Moment, toDate: moment.Moment, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<DiffInfo> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpen(owner: string, repo: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getReviews(owner: string, repo: string, pr: PullRequestInfo): Promise<void> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTags(owner: string, repo: string, maxTagsToFetch: number): Promise<TagInfo[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import {BaseRepository} from "./BaseRepository";
|
||||||
|
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
||||||
|
import {HttpsProxyAgent} from "https-proxy-agent";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import {TagInfo} from "../pr-collector/tags";
|
||||||
|
import moment from "moment/moment";
|
||||||
|
import {createCommandManager} from "../pr-collector/gitHelper";
|
||||||
|
import {DiffInfo} from "../pr-collector/commits";
|
||||||
|
import {
|
||||||
|
CommentInfo,
|
||||||
|
fetchedEnough,
|
||||||
|
mapComment,
|
||||||
|
mapPullRequest,
|
||||||
|
PullRequestInfo,
|
||||||
|
PullReviewsData, PullsListData
|
||||||
|
} from "../pr-collector/pullRequests";
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Fetch comparisons recursively until we don't find any commits
|
||||||
|
// This is because the GitHub API limits the number of commits returned in a single response.
|
||||||
|
let commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = []
|
||||||
|
let compareHead = head
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const compareResult = await this.octokit.repos.compareCommits({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
base,
|
||||||
|
head: compareHead
|
||||||
|
})
|
||||||
|
if (compareResult.data.total_commits === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getForCommitHash(owner: string, repo: string, commit_sha: string, maxPullRequests: number): Promise<PullRequestInfo[]> {
|
||||||
|
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)) {
|
||||||
|
const prs: PullsListData = response.data as PullsListData
|
||||||
|
|
||||||
|
for (const pr of prs) {
|
||||||
|
mergedPRs.push(mapPullRequest(pr, pr.merged_at ? 'merged' : 'open'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
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(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 (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(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(mapComment(comment))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pr.reviews = prReviews
|
||||||
|
}
|
||||||
|
get defaultUrl(): string {
|
||||||
|
return "https://api.example.com";
|
||||||
|
}
|
||||||
|
private octokit:Octokit
|
||||||
|
constructor(token: string, url?: string) {
|
||||||
|
super(token, url);
|
||||||
|
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) {
|
||||||
|
core.info(`⚠️ No release information found for ${tagInfo.name}, trying to retrieve tag creation time as fallback.`)
|
||||||
|
const gitHelper = await createCommandManager(repositoryPath)
|
||||||
|
const creationTimeString = await gitHelper.tagCreation(tagInfo.name)
|
||||||
|
const creationTime = moment(creationTimeString)
|
||||||
|
if (creationTimeString !== null && creationTime.isValid()) {
|
||||||
|
tagInfo.date = creationTime
|
||||||
|
core.info(
|
||||||
|
`ℹ️ Resolved tag creation time (${creationTimeString}) from '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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+3
-1
@@ -9,5 +9,7 @@
|
|||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
"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` */
|
"lib": [ "ES2021.String" ] /* Enable custom `ES2021.String` extension in typescript for `replaceAll` */
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "**/*.test.ts", "pr-collector"]
|
"exclude": ["node_modules", "**/*.test.ts",
|
||||||
|
"src/pr-collector"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user