diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index ebf454c..2ba3ae5 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -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(`
Uncategorized diff --git a/src/commits.ts b/src/commits.ts index 6751c42..97113af 100755 --- a/src/commits.ts +++ b/src/commits.ts @@ -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 { - 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 { + 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 { + // 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 { - // 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 + } } diff --git a/src/configuration.ts b/src/configuration.ts index fb0201d..ead0b38 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,27 +1,27 @@ -interface Configuration { - sort: string; - template: string; - pr_template: string; - empty_template: string; - categories: Array; - transformers: Array; +export interface Configuration { + sort: string + template: string + pr_template: string + empty_template: string + categories: Category[] + transformers: Transformer[] } -interface Category { - title: string; - labels: Array; +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: [] } diff --git a/src/git-helper.ts b/src/git-helper.ts index 5e805a9..25b5c94 100644 --- a/src/git-helper.ts +++ b/src/git-helper.ts @@ -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 { - 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 { - 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 { + 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 { - const result = new GitCommandManager() - await result.initializeCommandManager(workingDirectory) - return result - } + static async createCommandManager( + workingDirectory: string + ): Promise { + const result = new GitCommandManager() + await result.initializeCommandManager(workingDirectory) + return result + } - private async execGit( - args: string[], - allowAllExitCodes = false, - silent = false - ): Promise { - directoryExistsSync(this.workingDirectory, true) + private async execGit( + args: string[], + allowAllExitCodes = false, + silent = false + ): Promise { + 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 { - 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 { + this.workingDirectory = workingDirectory + this.gitPath = await io.which('git', true) + } } class GitOutput { - stdout = '' - exitCode = 0 -} \ No newline at end of file + stdout = '' + exitCode = 0 +} diff --git a/src/main.ts b/src/main.ts index 7b70037..3d3404c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { try { @@ -17,10 +15,7 @@ async function run(): Promise { 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 { 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 { 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 { } 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)) diff --git a/src/pullRequests.ts b/src/pullRequests.ts index 606404d..4ce4a88 100755 --- a/src/pullRequests.ts +++ b/src/pullRequests.ts @@ -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 - 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 { - try { - const pr = await this.octokit.pulls.get({ owner, repo, pull_number: prNumber }) + async getSingle( + owner: string, + repo: string, + prNumber: number + ): Promise { + 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 { - 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 { + 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 -} \ No newline at end of file +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 +} diff --git a/src/releaseNotes.ts b/src/releaseNotes.ts index ba248ea..8de1267 100755 --- a/src/releaseNotes.ts +++ b/src/releaseNotes.ts @@ -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 { + 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 { - 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 { - 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 { + 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 + } } diff --git a/src/tags.ts b/src/tags.ts index 289311b..a47162a 100755 --- a/src/tags.ts +++ b/src/tags.ts @@ -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 { - 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 { + 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 { + 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 { - 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 -*/ \ No newline at end of file +*/ diff --git a/src/transform.ts b/src/transform.ts index c3bc2e8..db029b9 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -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(); - // 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() + // 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() + 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(); - config.categories.forEach(category => { - categorized.set(category, []) - }) - let uncategorized: Array = []; + 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; -} \ No newline at end of file + pattern: RegExp | null + target: string +} diff --git a/src/utils.ts b/src/utils.ts index 71c9c67..d461d05 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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; -} \ No newline at end of file +export function readConfiguration(filename: string): Configuration { + const rawdata = fs.readFileSync(filename, 'utf8') + const configurationJSON: Configuration = JSON.parse(rawdata) + return configurationJSON +}