- fix a ton of formatting issues automatically

- fix many javascript issues
This commit is contained in:
Mike Penz
2020-10-16 16:29:13 +02:00
parent 8d59911332
commit db70432deb
10 changed files with 636 additions and 557 deletions
+19 -20
View File
@@ -2,11 +2,11 @@ import {wait} from '../src/wait'
import * as process from 'process'
import * as cp from 'child_process'
import * as path from 'path'
import { ReleaseNotes } from '../src/releaseNotes';
import { readConfiguration } from '../src/utils';
import { createCommandManager } from '../src/git-helper';
import * as core from '@actions/core';
import { Tags } from '../src/tags';
import {ReleaseNotes} from '../src/releaseNotes'
import {readConfiguration} from '../src/utils'
import {createCommandManager} from '../src/git-helper'
import * as core from '@actions/core'
import {Tags} from '../src/tags'
// shows how the runner will run a javascript action with env / stdout protocol
/*
@@ -22,22 +22,21 @@ test('test runs', () => {
})
*/
it("Should be true", async () => {
jest.setTimeout(180000);
const configuration = readConfiguration('configuration.json')
const releaseNotes = new ReleaseNotes({
owner: "mikepenz",
repo: "MaterialDrawer",
fromTag: null,
toTag: "v8.1.6",
configuration: configuration
})
it('Should be true', async () => {
jest.setTimeout(180000)
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`
const configuration = readConfiguration('configuration.json')
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'MaterialDrawer',
fromTag: null,
toTag: 'v8.1.6',
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`
<details>
<summary>Uncategorized</summary>
+79 -57
View File
@@ -1,70 +1,92 @@
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
import moment from 'moment';
import * as core from '@actions/core';
import moment from 'moment'
import * as core from '@actions/core'
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
export interface CommitInfo {
sha: string
summary: string
message: string
author: string
date: moment.Moment
prNumber: number | undefined
sha: string
summary: string
message: string
author: string
date: moment.Moment
prNumber: number | undefined
}
export class Commits {
constructor(private octokit: Octokit) { }
constructor(private octokit: Octokit) {}
async getDiff(owner: string, repo: string, base: string, head: string): Promise<CommitInfo[]> {
const commits: CommitInfo[] = await this.getDiffRemote(owner, repo, base, head)
return this.sortCommits(commits)
async getDiff(
owner: string,
repo: string,
base: string,
head: string
): Promise<CommitInfo[]> {
const commits: CommitInfo[] = await this.getDiffRemote(
owner,
repo,
base,
head
)
return this.sortCommits(commits)
}
private async getDiffRemote(
owner: string,
repo: string,
base: string,
head: string
): Promise<CommitInfo[]> {
// 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
while (true) {
const compareResult = await this.octokit.repos.compareCommits({
owner,
repo,
base,
head: compareHead
})
if (compareResult.data.total_commits === 0) {
break
}
commits = compareResult.data.commits.concat(commits)
compareHead = `${commits[0].sha}^`
}
private async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<CommitInfo[]> {
// 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
while (true) {
const compareResult = await this.octokit.repos.compareCommits({ owner, repo, base, head: compareHead })
if (compareResult.data.total_commits === 0) {
break
}
commits = compareResult.data.commits.concat(commits)
compareHead = `${commits[0].sha}^`
}
core.info(
`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`
)
return commits.map(commit => ({
sha: commit.sha,
summary: commit.commit.message.split('\n')[0],
message: commit.commit.message,
date: moment(commit.commit.committer.date),
author: commit.commit.author.name,
prNumber: undefined
}))
}
core.info(`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`)
return commits.map(commit => ({
sha: commit.sha,
summary: commit.commit.message.split("\n")[0],
message: commit.commit.message,
date: moment(commit.commit.committer.date),
author: commit.commit.author.name,
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)
}
private sortCommits(commits: CommitInfo[]): CommitInfo[] {
const commitsResult = []
const shas: { [key: string]: boolean } = {}
commitsResult.sort((a, b) => {
if (a.date.isBefore(b.date)) {
return -1
} else if (b.date.isBefore(a.date)) {
return 1
}
return 0
})
for (const commit of commits) {
if (shas[commit.sha]) {
continue
}
shas[commit.sha] = true
commitsResult.push(commit)
}
commitsResult.sort((a, b) => {
if (a.date.isBefore(b.date)) {
return -1
} else if (b.date.isBefore(a.date)) {
return 1
}
return 0
})
return commitsResult
}
return commitsResult
}
}
+20 -20
View File
@@ -1,27 +1,27 @@
interface Configuration {
sort: string;
template: string;
pr_template: string;
empty_template: string;
categories: Array<Category>;
transformers: Array<Transformer>;
export interface Configuration {
sort: string
template: string
pr_template: string
empty_template: string
categories: Category[]
transformers: Transformer[]
}
interface Category {
title: string;
labels: Array<string>;
export interface Category {
title: string
labels: string[]
}
interface Transformer {
pattern: string;
target: string;
export interface Transformer {
pattern: string
target: string
}
const DefaultConfiguration: Configuration = {
sort: "ASC",
template: "${{CHANGELOG}}",
pr_template: "- ${{TITLE}}\n - PR: #${{NUMBER}}",
empty_template: "- no changes",
categories: [],
transformers: []
export const DefaultConfiguration: Configuration = {
sort: 'ASC',
template: '${{CHANGELOG}}',
pr_template: '- ${{TITLE}}\n - PR: #${{NUMBER}}',
empty_template: '- no changes',
categories: [],
transformers: []
}
+85 -78
View File
@@ -1,108 +1,115 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as io from '@actions/io'
export async function createCommandManager(
workingDirectory: string
workingDirectory: string
): Promise<GitCommandManager> {
return await GitCommandManager.createCommandManager(workingDirectory)
return await GitCommandManager.createCommandManager(workingDirectory)
}
function directoryExistsSync(path: string, required?: boolean): boolean {
if (!path) {
throw new Error("Arg 'path' must not be empty")
}
if (!path) {
throw new Error("Arg 'path' must not be empty")
}
let stats: fs.Stats
try {
stats = fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
)
}
if (stats.isDirectory()) {
return true
} else if (!required) {
let stats: fs.Stats
try {
stats = fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}
throw new Error(`Directory '${path}' does not exist`)
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
)
}
if (stats.isDirectory()) {
return true
} else if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}
class GitCommandManager {
private gitPath = ''
private workingDirectory = ''
private gitPath = ''
private workingDirectory = ''
// Private constructor; use createCommandManager()
private constructor() { }
// Private constructor; use createCommandManager()
private constructor() {}
getWorkingDirectory(): string {
return this.workingDirectory
}
getWorkingDirectory(): string {
return this.workingDirectory
}
async latestTag(): Promise<string> {
const revListOutput = await this.execGit(['rev-list', '--tags', '--skip=0', '--max-count=1'])
const output = await this.execGit(['describe', '--abbrev=0', '--tags', revListOutput.stdout.trim()])
return output.stdout.trim()
}
async latestTag(): Promise<string> {
const revListOutput = await this.execGit([
'rev-list',
'--tags',
'--skip=0',
'--max-count=1'
])
const output = await this.execGit([
'describe',
'--abbrev=0',
'--tags',
revListOutput.stdout.trim()
])
return output.stdout.trim()
}
static async createCommandManager(
workingDirectory: string
): Promise<GitCommandManager> {
const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory)
return result
}
static async createCommandManager(
workingDirectory: string
): Promise<GitCommandManager> {
const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory)
return result
}
private async execGit(
args: string[],
allowAllExitCodes = false,
silent = false
): Promise<GitOutput> {
directoryExistsSync(this.workingDirectory, true)
private async execGit(
args: string[],
allowAllExitCodes = false,
silent = false
): Promise<GitOutput> {
directoryExistsSync(this.workingDirectory, true)
const result = new GitOutput()
const result = new GitOutput()
const stdout: string[] = []
const stdout: string[] = []
const options = {
cwd: this.workingDirectory,
silent,
ignoreReturnCode: allowAllExitCodes,
listeners: {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
const options = {
cwd: this.workingDirectory,
silent,
ignoreReturnCode: allowAllExitCodes,
listeners: {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('')
return result
}
}
private async initializeCommandManager(
workingDirectory: string
): Promise<void> {
this.workingDirectory = workingDirectory
this.gitPath = await io.which('git', true)
}
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('')
return result
}
private async initializeCommandManager(
workingDirectory: string
): Promise<void> {
this.workingDirectory = workingDirectory
this.gitPath = await io.which('git', true)
}
}
class GitOutput {
stdout = ''
exitCode = 0
}
stdout = ''
exitCode = 0
}
+21 -20
View File
@@ -1,11 +1,9 @@
import * as core from '@actions/core'
import { wait } from './wait'
import { readConfiguration } from './utils';
import { ReleaseNotes } from './releaseNotes';
import { createCommandManager } from './git-helper';
import {readConfiguration} from './utils'
import {ReleaseNotes} from './releaseNotes'
import {createCommandManager} from './git-helper'
import * as github from '@actions/github'
import * as path from 'path';
import * as path from 'path'
async function run(): Promise<void> {
try {
@@ -17,10 +15,7 @@ async function run(): Promise<void> {
core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`)
let repositoryPath = core.getInput('path') || '.'
repositoryPath = path.resolve(
githubWorkspacePath,
repositoryPath
)
repositoryPath = path.resolve(githubWorkspacePath, repositoryPath)
core.debug(`repositoryPath = '${repositoryPath}'`)
const configurationFile: string = core.getInput('configuration')
@@ -31,12 +26,12 @@ async function run(): Promise<void> {
core.debug(`configurationPath = '${configurationPath}'`)
const configuration = readConfiguration(configurationPath)
let token = core.getInput('token')
const token = core.getInput('token')
let owner = core.getInput('owner')
let repo = core.getInput('repo')
let fromTag = core.getInput("fromTag")
let toTag = core.getInput("toTag")
const fromTag = core.getInput('fromTag')
let toTag = core.getInput('toTag')
if (!toTag) {
// if not specified try to retrieve tag from git
@@ -48,10 +43,16 @@ async function run(): Promise<void> {
if (!owner || !repo) {
// Qualified repository
const qualifiedRepository = core.getInput('repository') || `${github.context.repo.owner}/${github.context.repo.repo}`
const qualifiedRepository =
core.getInput('repository') ||
`${github.context.repo.owner}/${github.context.repo.repo}`
core.debug(`qualified repository = '${qualifiedRepository}'`)
const splitRepository = qualifiedRepository.split('/')
if (splitRepository.length !== 2 || !splitRepository[0] || !splitRepository[1]) {
if (
splitRepository.length !== 2 ||
!splitRepository[0] ||
!splitRepository[1]
) {
throw new Error(
`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`
)
@@ -76,11 +77,11 @@ async function run(): Promise<void> {
}
const releaseNotes = new ReleaseNotes({
owner: owner,
repo: repo,
fromTag: fromTag,
toTag: toTag,
configuration: configuration
owner,
repo,
fromTag,
toTag,
configuration
})
core.setOutput('changelog', await releaseNotes.pull(token))
+123 -106
View File
@@ -1,126 +1,143 @@
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
import moment from 'moment';
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import moment from 'moment'
import { CommitInfo } from "./commits"
import * as core from '@actions/core';
import {CommitInfo} from './commits'
import * as core from '@actions/core'
export interface PullRequestInfo {
number: number
title: string
htmlURL: string
mergedAt: moment.Moment
author: string
repoName: string
labels: Array<string>
body: string
number: number
title: string
htmlURL: string
mergedAt: moment.Moment
author: string
repoName: string
labels: string[]
body: string
}
export class PullRequests {
constructor(private octokit: Octokit) {}
constructor(private octokit: Octokit) {}
async getSingle(owner: string, repo: string, prNumber: number): Promise<PullRequestInfo | null> {
try {
const pr = await this.octokit.pulls.get({ owner, repo, pull_number: prNumber })
async getSingle(
owner: string,
repo: string,
prNumber: number
): Promise<PullRequestInfo | null> {
try {
const pr = await this.octokit.pulls.get({
owner,
repo,
pull_number: prNumber
})
return {
number: pr.data.number,
title: pr.data.title,
htmlURL: pr.data.html_url,
mergedAt: moment(pr.data.merged_at),
author: pr.data.user.login,
repoName: pr.data.base.repo.full_name,
labels: pr.data.labels.map(function (label) { return label.name }),
body: pr.data.body
}
} catch (e) {
core.warning(`Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`)
return null
}
return {
number: pr.data.number,
title: pr.data.title,
htmlURL: pr.data.html_url,
mergedAt: moment(pr.data.merged_at),
author: pr.data.user.login,
repoName: pr.data.base.repo.full_name,
labels: pr.data.labels.map(function (label) {
return label.name
}),
body: pr.data.body
}
} catch (e) {
core.warning(`Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`)
return null
}
}
async getBetweenDates(
owner: string,
repo: string,
fromDate: moment.Moment,
toDate: moment.Moment
): Promise<PullRequestInfo[]> {
const mergedPRs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({
owner,
repo,
state: "closed",
sort: "updated",
direction: "desc"
})
async getBetweenDates(
owner: string,
repo: string,
fromDate: moment.Moment,
toDate: moment.Moment
): Promise<PullRequestInfo[]> {
const mergedPRs: PullRequestInfo[] = []
const options = this.octokit.pulls.list.endpoint.merge({
owner,
repo,
state: 'closed',
sort: 'updated',
direction: 'desc'
})
for await (const response of this.octokit.paginate.iterator(options)) {
type PullsListData = RestEndpointMethodTypes["pulls"]["list"]["response"]["data"]
const prs: PullsListData = response.data as PullsListData
const firstPR = prs[0]
if(firstPR.merged_at && fromDate.isAfter(moment(firstPR.merged_at))) {
// bail out early to not keep iterating on PRs super old
return sortPullRequests(mergedPRs, true)
}
prs.filter(
pr =>
!!pr.merged_at &&
fromDate.isBefore(moment(pr.merged_at)) &&
toDate.isSameOrAfter(moment(pr.merged_at))
).forEach(pr => {
mergedPRs.push({
number: pr.number,
title: pr.title,
htmlURL: pr.html_url,
mergedAt: moment(pr.merged_at),
author: pr.user.login,
repoName: pr.base.repo.full_name,
labels: pr.labels.map(function (label) { return label.name }),
body: pr.body
})
})
}
for await (const response of this.octokit.paginate.iterator(options)) {
type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
const prs: PullsListData = response.data as PullsListData
const firstPR = prs[0]
if (firstPR.merged_at && fromDate.isAfter(moment(firstPR.merged_at))) {
// bail out early to not keep iterating on PRs super old
return sortPullRequests(mergedPRs, true)
}
prs
.filter(
pr =>
!!pr.merged_at &&
fromDate.isBefore(moment(pr.merged_at)) &&
toDate.isSameOrAfter(moment(pr.merged_at))
)
.forEach(pr => {
mergedPRs.push({
number: pr.number,
title: pr.title,
htmlURL: pr.html_url,
mergedAt: moment(pr.merged_at),
author: pr.user.login,
repoName: pr.base.repo.full_name,
labels: pr.labels.map(function (label) {
return label.name
}),
body: pr.body
})
})
}
filterCommits(commits: CommitInfo[]): CommitInfo[] {
const prRegex = /Merge pull request #(\d+)/
const filteredCommits = []
return sortPullRequests(mergedPRs, true)
}
for (const commit of commits) {
const match = commit.summary.match(prRegex)
if (!match) {
continue
}
commit.prNumber = Number.parseInt(match[1], 10)
filteredCommits.push(commit)
}
filterCommits(commits: CommitInfo[]): CommitInfo[] {
const prRegex = /Merge pull request #(\d+)/
const filteredCommits = []
return filteredCommits
for (const commit of commits) {
const match = commit.summary.match(prRegex)
if (!match) {
continue
}
commit.prNumber = Number.parseInt(match[1], 10)
filteredCommits.push(commit)
}
return filteredCommits
}
}
export function sortPullRequests(pullRequests: PullRequestInfo[], ascending: Boolean): PullRequestInfo[] {
if(ascending) {
pullRequests.sort((a, b) => {
if (a.mergedAt.isBefore(b.mergedAt)) {
return -1
} else if (b.mergedAt.isBefore(a.mergedAt)) {
return 1
}
return 0
})
} else {
pullRequests.sort((b, a) => {
if (a.mergedAt.isBefore(b.mergedAt)) {
return -1
} else if (b.mergedAt.isBefore(a.mergedAt)) {
return 1
}
return 0
})
}
return pullRequests
}
export function sortPullRequests(
pullRequests: PullRequestInfo[],
ascending: Boolean
): PullRequestInfo[] {
if (ascending) {
pullRequests.sort((a, b) => {
if (a.mergedAt.isBefore(b.mergedAt)) {
return -1
} else if (b.mergedAt.isBefore(a.mergedAt)) {
return 1
}
return 0
})
} else {
pullRequests.sort((b, a) => {
if (a.mergedAt.isBefore(b.mergedAt)) {
return -1
} else if (b.mergedAt.isBefore(a.mergedAt)) {
return 1
}
return 0
})
}
return pullRequests
}
+108 -91
View File
@@ -1,107 +1,124 @@
import { Octokit } from "@octokit/rest"
import { Commits } from "./commits"
import { PullRequestInfo, PullRequests } from "./pullRequests"
import { buildChangelog } from './transform';
import * as core from '@actions/core';
import { Tags } from './tags';
import {Octokit} from '@octokit/rest'
import {Commits} from './commits'
import {PullRequestInfo, PullRequests} from './pullRequests'
import {buildChangelog} from './transform'
import * as core from '@actions/core'
import {Tags} from './tags'
import {Configuration, DefaultConfiguration} from './configuration'
export interface ReleaseNotesOptions {
owner: string
repo: string
fromTag: string | null
toTag: string
configuration: Configuration
owner: string
repo: string
fromTag: string | null
toTag: string
configuration: Configuration
}
export class ReleaseNotes {
constructor(private options: ReleaseNotesOptions) {
constructor(private options: ReleaseNotesOptions) {}
async pull(token?: string): Promise<string> {
const octokit = new Octokit({
auth: `token ${token || process.env.GITHUB_TOKEN}`
})
const {owner, repo, fromTag, toTag, configuration} = this.options
if (!fromTag) {
const tagsApi = new Tags(octokit)
const previousTag = await tagsApi.findPredecessorTag(owner, repo, toTag)
if (previousTag == null) {
core.error(`Unable to retrieve previous tag given ${toTag}`)
return configuration.empty_template
? configuration.empty_template
: DefaultConfiguration.empty_template
}
this.options.fromTag = previousTag.name
}
async pull(token?: string): Promise<string> {
const octokit = new Octokit({
auth: `token ${token || process.env.GITHUB_TOKEN}`
})
const mergedPullRequests = await this.getMergedPullRequests(octokit)
const { owner, repo, fromTag, toTag, configuration } = this.options
if(fromTag == null) {
const tagsApi = new Tags(octokit)
const previousTag = await tagsApi.findPredecessorTag(owner, repo, toTag)
if(previousTag == null) {
core.error(`Unable to retrieve previous tag given ${toTag}`)
return configuration.empty_template ? configuration.empty_template : DefaultConfiguration.empty_template
}
this.options.fromTag = previousTag.name
}
const mergedPullRequests = await this.getMergedPullRequests(octokit)
if (mergedPullRequests.length == 0) {
core.warning(`No pull requests found for between ${fromTag}...${toTag}`)
return configuration.empty_template ? configuration.empty_template : DefaultConfiguration.empty_template
}
return buildChangelog(mergedPullRequests, configuration)
if (mergedPullRequests.length === 0) {
core.warning(`No pull requests found for between ${fromTag}...${toTag}`)
return configuration.empty_template
? configuration.empty_template
: DefaultConfiguration.empty_template
}
private async getMergedPullRequests(octokit: Octokit): Promise<PullRequestInfo[]> {
const { owner, repo, fromTag, toTag } = this.options
core.info(`Comparing ${owner}/${repo} ${fromTag}...${toTag}`)
return buildChangelog(mergedPullRequests, configuration)
}
const commitsApi = new Commits(octokit)
const commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
private async getMergedPullRequests(
octokit: Octokit
): Promise<PullRequestInfo[]> {
const {owner, repo, fromTag, toTag} = this.options
core.info(`Comparing ${owner}/${repo} ${fromTag}...${toTag}`)
if (commits.length === 0) {
return []
}
const commitsApi = new Commits(octokit)
const commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
const firstCommit = commits[0]
const lastCommit = commits[commits.length - 1]
const fromDate = firstCommit.date
const toDate = lastCommit.date
core.info(`Fetching PRs between dates ${fromDate.toISOString()} ${toDate.toISOString()} for ${owner}/${repo}`)
const pullRequestsApi = new PullRequests(octokit)
const pullRequests = await pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate)
core.info(`Found ${pullRequests.length} merged PRs for ${owner}/${repo}`)
const prCommits = pullRequestsApi.filterCommits(commits)
const filteredPullRequests = []
const pullRequestsByNumber: { [key: number]: PullRequestInfo } = {}
for (const pr of pullRequests) {
pullRequestsByNumber[pr.number] = pr
}
for (const commit of prCommits) {
if (!commit.prNumber) {
continue
}
const prRef = `${owner}/${repo}#${commit.prNumber}`
if (pullRequestsByNumber[commit.prNumber]) {
filteredPullRequests.push(pullRequestsByNumber[commit.prNumber])
} else if (fromDate.toISOString() === toDate.toISOString()) {
core.info(`${prRef} not in date range, fetching explicitly`)
const pullRequest = await pullRequestsApi.getSingle(owner, repo, commit.prNumber)
if (pullRequest) {
filteredPullRequests.push(pullRequest)
} else {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`)
}
} else {
core.info(`${prRef} not in date range, likely a merge commit from a fork-to-fork PR`)
}
}
return filteredPullRequests
if (commits.length === 0) {
return []
}
const firstCommit = commits[0]
const lastCommit = commits[commits.length - 1]
const fromDate = firstCommit.date
const toDate = lastCommit.date
core.info(
`Fetching PRs between dates ${fromDate.toISOString()} ${toDate.toISOString()} for ${owner}/${repo}`
)
const pullRequestsApi = new PullRequests(octokit)
const pullRequests = await pullRequestsApi.getBetweenDates(
owner,
repo,
fromDate,
toDate
)
core.info(`Found ${pullRequests.length} merged PRs for ${owner}/${repo}`)
const prCommits = pullRequestsApi.filterCommits(commits)
const filteredPullRequests = []
const pullRequestsByNumber: {[key: number]: PullRequestInfo} = {}
for (const pr of pullRequests) {
pullRequestsByNumber[pr.number] = pr
}
for (const commit of prCommits) {
if (!commit.prNumber) {
continue
}
const prRef = `${owner}/${repo}#${commit.prNumber}`
if (pullRequestsByNumber[commit.prNumber]) {
filteredPullRequests.push(pullRequestsByNumber[commit.prNumber])
} else if (fromDate.toISOString() === toDate.toISOString()) {
core.info(`${prRef} not in date range, fetching explicitly`)
const pullRequest = await pullRequestsApi.getSingle(
owner,
repo,
commit.prNumber
)
if (pullRequest) {
filteredPullRequests.push(pullRequest)
} else {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`)
}
} else {
core.info(
`${prRef} not in date range, likely a merge commit from a fork-to-fork PR`
)
}
}
return filteredPullRequests
}
}
+71 -68
View File
@@ -1,83 +1,86 @@
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
import * as core from '@actions/core';
import { PullRequestInfo } from './pullRequests';
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest'
import * as core from '@actions/core'
export interface TagInfo {
name: string,
commit: string
name: string
commit: string
}
export class Tags {
constructor(private octokit: Octokit) { }
constructor(private octokit: Octokit) {}
async getTags(owner: string, repo: string): Promise<TagInfo[]> {
const tagsInfo: TagInfo[] = []
const options = this.octokit.repos.listTags.endpoint.merge({
owner,
repo,
direction: "desc",
per_page: 100
async getTags(owner: string, repo: string): Promise<TagInfo[]> {
const tagsInfo: TagInfo[] = []
const options = this.octokit.repos.listTags.endpoint.merge({
owner,
repo,
direction: 'desc',
per_page: 100
})
const max = 200
for await (const response of this.octokit.paginate.iterator(options)) {
type TagsListData = RestEndpointMethodTypes['repos']['listTags']['response']['data']
const tags: TagsListData = response.data as TagsListData
tags.forEach(tag => {
tagsInfo.push({
name: tag.name,
commit: tag.commit.sha
})
})
const max: number = 200
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 performance only fetch newest 200 tags!!
if (tagsInfo.length >= max) {
break
}
}
tags.forEach(tag => {
tagsInfo.push({
name: tag.name,
commit: tag.commit.sha
})
})
core.info(
`Found ${tagsInfo.length} (fetching max: ${max}) tags from the GitHub API for ${owner}/${repo}`
)
return tagsInfo
}
// for performance only fetch newest 200 tags!!
if (tagsInfo.length >= max) {
break
}
async findPredecessorTag(
owner: string,
repo: string,
tag: string
): Promise<TagInfo | null> {
const tags = this.sortTags(await this.getTags(owner, repo))
const length = tags.length
for (let i = 0; i < length; i++) {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
return tags[i + 1]
}
}
// not found, throw exception?
return tags[0]
}
private sortTags(commits: TagInfo[]): TagInfo[] {
commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-')
const partsB = b.name.replace(/^v/, '').split('-')
const versionCompare = partsA[0].localeCompare(partsB[0])
if (versionCompare !== 0) {
return versionCompare
} else {
if (partsA.length === 1) {
return 0
} else if (partsB.length === 1) {
return 1
} else {
return partsA[1].localeCompare(partsB[1])
}
core.info(`Found ${tagsInfo.length} (fetching max: ${max}) tags from the GitHub API for ${owner}/${repo}`)
return tagsInfo
}
async findPredecessorTag(owner: string, repo: string, tag: string): Promise<TagInfo | null> {
const tags = this.sortTags(await this.getTags(owner, repo))
var length = tags.length;
for (var i = 0; i < length; i++) {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
return tags[i + 1]
}
}
// not found, throw exception?
return tags[0]
}
private sortTags(commits: TagInfo[]): TagInfo[] {
commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-')
const partsB = b.name.replace(/^v/, '').split('-')
const versionCompare = partsA[0].localeCompare(partsB[0])
if(versionCompare != 0) {
return versionCompare
} else {
if(partsA.length == 1) {
return 0
} else if(partsB.length == 1) {
return 1
} else {
return partsA[1].localeCompare(partsB[1])
}
}
})
return commits
}
}
})
return commits
}
}
/*
2020.3.2 ( should resolve 2020.3.1 )
@@ -93,4 +96,4 @@ export class Tags {
2020.3.1-a01
2020.3.0
*/
*/
+103 -91
View File
@@ -1,114 +1,126 @@
import { PullRequestInfo, sortPullRequests } from './pullRequests';
import * as core from '@actions/core';
import {PullRequestInfo, sortPullRequests} from './pullRequests'
import * as core from '@actions/core'
import {Category, Configuration, Transformer} from './configuration'
export function buildChangelog(prs: PullRequestInfo[], config: Configuration): string {
// sort to target order
prs = sortPullRequests(prs, config.sort.toUpperCase() === "ASC")
export function buildChangelog(
prs: PullRequestInfo[],
config: Configuration
): string {
// sort to target order
prs = sortPullRequests(prs, config.sort.toUpperCase() === 'ASC')
const validatedTransformers = validateTransfomers(config.transformers)
let transformedMap = new Map<PullRequestInfo, string>();
// convert PRs to their text representation
prs.forEach(pr => {
transformedMap.set(pr, transform(fillTemplate(pr, config.pr_template), validatedTransformers))
const validatedTransformers = validateTransfomers(config.transformers)
const transformedMap = new Map<PullRequestInfo, string>()
// convert PRs to their text representation
prs.forEach(pr => {
transformedMap.set(
pr,
transform(fillTemplate(pr, config.pr_template), validatedTransformers)
)
})
// bring PRs into the order of categories
const categorized = new Map<Category, string[]>()
config.categories.forEach(category => {
categorized.set(category, [])
})
const uncategorized: string[] = []
// bring elements in order
transformedMap.forEach((body, pr) => {
let matched = false
categorized.forEach((pullRequests, category) => {
if (haveCommonElements(category.labels, pr.labels)) {
pullRequests.push(body)
matched = true
}
})
// bring PRs into the order of categories
let categorized = new Map<Category, string[]>();
config.categories.forEach(category => {
categorized.set(category, [])
})
let uncategorized: Array<string> = [];
if (!matched) {
uncategorized.push(body)
}
})
// bring elements in order
transformedMap.forEach((body, pr) => {
let matched = false
// construct final changelog
let changelog = ''
categorized.forEach((pullRequests, category) => {
if (pullRequests.length > 0) {
changelog = `${changelog + category.title}\n\n`
categorized.forEach((prs, category) => {
if (findCommonElements3(category.labels, pr.labels)) {
prs.push(body)
matched = true
}
})
pullRequests.forEach(pr => {
changelog = `${changelog + pr}\n`
})
if (!matched) {
uncategorized.push(body)
}
})
// add space between
changelog = `${changelog}\n`
}
})
// construct final changelog
let changelog = ""
categorized.forEach((prs, category) => {
if (prs.length > 0) {
changelog = changelog + category.title + "\n\n"
let changelogUncategorized = ''
uncategorized.forEach(pr => {
changelogUncategorized = `${changelogUncategorized + pr}\n`
})
prs.forEach(pr => {
changelog = changelog + pr + "\n"
})
// add space between
changelog = changelog + "\n"
}
})
let changelogUncategorized = ""
uncategorized.forEach(pr => {
changelogUncategorized = changelogUncategorized + pr + "\n"
})
// fill template
let transformedChangelog = config.template
transformedChangelog = transformedChangelog.replace("${{CHANGELOG}}", changelog)
transformedChangelog = transformedChangelog.replace("${{UNCATEGORIZED}}", changelogUncategorized)
return transformedChangelog;
// fill template
let transformedChangelog = config.template
transformedChangelog = transformedChangelog.replace(
'${{CHANGELOG}}',
changelog
)
transformedChangelog = transformedChangelog.replace(
'${{UNCATEGORIZED}}',
changelogUncategorized
)
return transformedChangelog
}
function findCommonElements3(arr1: string[], arr2: string[]) {
return arr1.some(item => arr2.includes(item))
function haveCommonElements(arr1: string[], arr2: string[]): Boolean {
return arr1.some(item => arr2.includes(item))
}
function fillTemplate(pr: PullRequestInfo, template: string): string {
let transformed = template
transformed = transformed.replace("${{NUMBER}}", pr.number.toString())
transformed = transformed.replace("${{TITLE}}", pr.title)
transformed = transformed.replace("${{URL}}", pr.htmlURL)
transformed = transformed.replace("${{MERGED_AT}}", pr.mergedAt.toString)
transformed = transformed.replace("${{AUTHOR}}", pr.author)
transformed = transformed.replace("${{BODY}}", pr.body)
return transformed
let transformed = template
transformed = transformed.replace('${{NUMBER}}', pr.number.toString())
transformed = transformed.replace('${{TITLE}}', pr.title)
transformed = transformed.replace('${{URL}}', pr.htmlURL)
transformed = transformed.replace('${{MERGED_AT}}', pr.mergedAt.toString())
transformed = transformed.replace('${{AUTHOR}}', pr.author)
transformed = transformed.replace('${{BODY}}', pr.body)
return transformed
}
function transform(filled: string, transformers: RegexTransformer[]): string {
if (transformers.length == 0) {
return filled
}
let transformed = filled
transformers.forEach(({ pattern, target }) => {
transformed = transformed.replace(pattern!!, target)
})
return transformed
if (transformers.length === 0) {
return filled
}
let transformed = filled
transformers.forEach(({pattern, target}) => {
transformed = transformed.replace(pattern!!, target)
})
return transformed
}
function validateTransfomers(transformers: Transformer[]): RegexTransformer[] {
return transformers
.map((transformer) => {
try {
return {
pattern: new RegExp(transformer.pattern.replace("\\\\", '\\'), "g"),
target: transformer.target
}
} catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`)
return {
pattern: null,
target: ""
}
}
})
.filter(transformer => transformer.pattern != null)
return transformers
.map(transformer => {
try {
return {
pattern: new RegExp(transformer.pattern.replace('\\\\', '\\'), 'g'),
target: transformer.target
}
} catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`)
return {
pattern: null,
target: ''
}
}
})
.filter(transformer => transformer.pattern != null)
}
interface RegexTransformer {
pattern: RegExp | null;
target: string;
}
pattern: RegExp | null
target: string
}
+7 -6
View File
@@ -1,7 +1,8 @@
const fs = require("fs");
import * as fs from 'fs'
import {Configuration} from './configuration'
export function readConfiguration(filename: string) {
const rawdata = fs.readFileSync(filename);
const configurationJSON: Configuration = JSON.parse(rawdata);
return configurationJSON;
}
export function readConfiguration(filename: string): Configuration {
const rawdata = fs.readFileSync(filename, 'utf8')
const configurationJSON: Configuration = JSON.parse(rawdata)
return configurationJSON
}