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