Merge pull request #11 from mikepenz/feature/specify_action

[CI] Update github action specification
This commit is contained in:
Mike Penz
2020-10-16 17:43:40 +02:00
committed by GitHub
16 changed files with 917 additions and 672 deletions
+28 -2
View File
@@ -15,10 +15,36 @@ jobs:
npm install npm install
- run: | - run: |
npm run all npm run all
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
test: # make sure the action works on a clean machine without building test: # make sure the action works on a clean machine without building
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: ./
with: with:
milliseconds: 1000 fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified
- name: "Minimal Configuration"
id: minimal_release
uses: ./
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Minimal
run: echo "${{steps.minimal_release.outputs.changelog}}"
- name: "Complex Configuration"
id: complex_release
uses: ./
with:
configuration: "configuration_complex.json"
owner: "mikepenz"
repo: "release-changelog-builder-action"
fromTag: "0.0.2"
toTag: "0.0.3"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Complex
run: echo "${{steps.complex_release.outputs.changelog}}"
+21 -29
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,32 +22,24 @@ 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: 'release-changelog-builder-action',
fromTag: null,
toTag: '0.0.3',
configuration: configuration
})
<details> const changeLog = await releaseNotes.pull()
<summary>Uncategorized</summary> console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
- Improve GitHub Actions - [CI] Specify Test Case
- PR: #2655 - PR: #10
- Update dependencies
- PR: #2656
- [RELEASE] v8.1.6
- PR: #2657
</details>`) `)
}) })
+1
View File
@@ -5,6 +5,7 @@ inputs:
configuration: configuration:
required: true required: true
description: 'path to the configuration file' description: 'path to the configuration file'
default: "configuration.json"
path: path:
description: 'the path to runt his action in' description: 'the path to runt his action in'
owner: owner:
+2 -8
View File
@@ -18,13 +18,7 @@
} }
], ],
"sort": "ASC", "sort": "ASC",
"template": "${{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n${{UNCATEGORIZED}}\n</details>", "template": "${{CHANGELOG}}",
"pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}", "pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}",
"empty_template": "- no changes", "empty_template": "- no changes"
"transformers": [
{
"pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)",
"target": "- $4\n - $6"
}
]
} }
+30
View File
@@ -0,0 +1,30 @@
{
"categories": [
{
"title": "## 🚀 Features",
"labels": ["feature"]
},
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix"]
},
{
"title": "## 🧪 Tests",
"labels": ["test"]
}
],
"sort": "ASC",
"template": "${{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n${{UNCATEGORIZED}}\n</details>",
"pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}",
"empty_template": "- no changes",
"transformers": [
{
"pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)",
"target": "- $4\n - $6"
}
]
}
Generated Vendored
+186 -95
View File
@@ -58,8 +58,14 @@ class Commits {
// This is because the GitHub API limits the number of commits returned in a single response. // This is because the GitHub API limits the number of commits returned in a single response.
let commits = []; let commits = [];
let compareHead = head; let compareHead = head;
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const compareResult = yield this.octokit.repos.compareCommits({ owner, repo, base, head: compareHead }); const compareResult = yield this.octokit.repos.compareCommits({
owner,
repo,
base,
head: compareHead
});
if (compareResult.data.total_commits === 0) { if (compareResult.data.total_commits === 0) {
break; break;
} }
@@ -69,7 +75,7 @@ class Commits {
core.info(`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`); core.info(`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`);
return commits.map(commit => ({ return commits.map(commit => ({
sha: commit.sha, sha: commit.sha,
summary: commit.commit.message.split("\n")[0], summary: commit.commit.message.split('\n')[0],
message: commit.commit.message, message: commit.commit.message,
date: moment_1.default(commit.commit.committer.date), date: moment_1.default(commit.commit.committer.date),
author: commit.commit.author.name, author: commit.commit.author.name,
@@ -102,6 +108,25 @@ class Commits {
exports.Commits = Commits; exports.Commits = Commits;
/***/ }),
/***/ 5527:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DefaultConfiguration = void 0;
exports.DefaultConfiguration = {
sort: 'ASC',
template: '${{CHANGELOG}}',
pr_template: '- ${{TITLE}}\n - PR: #${{NUMBER}}',
empty_template: '- no changes',
categories: [],
transformers: []
};
/***/ }), /***/ }),
/***/ 9621: /***/ 9621:
@@ -184,8 +209,18 @@ class GitCommandManager {
} }
latestTag() { latestTag() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const revListOutput = yield this.execGit(['rev-list', '--tags', '--skip=0', '--max-count=1']); const revListOutput = yield this.execGit([
const output = yield this.execGit(['describe', '--abbrev=0', '--tags', revListOutput.stdout.trim()]); 'rev-list',
'--tags',
'--skip=0',
'--max-count=1'
]);
const output = yield this.execGit([
'describe',
'--abbrev=0',
'--tags',
revListOutput.stdout.trim()
]);
return output.stdout.trim(); return output.stdout.trim();
}); });
} }
@@ -289,11 +324,11 @@ function run() {
const configurationPath = path.resolve(githubWorkspacePath, configurationFile); const configurationPath = path.resolve(githubWorkspacePath, configurationFile);
core.debug(`configurationPath = '${configurationPath}'`); core.debug(`configurationPath = '${configurationPath}'`);
const configuration = utils_1.readConfiguration(configurationPath); const configuration = utils_1.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
const gitHelper = yield git_helper_1.createCommandManager(repositoryPath); const gitHelper = yield git_helper_1.createCommandManager(repositoryPath);
@@ -303,10 +338,13 @@ function run() {
} }
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(`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`); throw new Error(`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`);
} }
owner = splitRepository[0]; owner = splitRepository[0];
@@ -316,20 +354,29 @@ function run() {
core.error(`Missing or couldn't resolve 'owner'`); core.error(`Missing or couldn't resolve 'owner'`);
return; return;
} }
else {
core.debug(`Resolved 'owner' as ${owner}`);
}
if (!repo) { if (!repo) {
core.error(`Missing or couldn't resolve 'owner'`); core.error(`Missing or couldn't resolve 'owner'`);
return; return;
} }
else {
core.debug(`Resolved 'repo' as ${repo}`);
}
if (!toTag) { if (!toTag) {
core.error(`Missing or couldn't resolve 'toTag'`); core.error(`Missing or couldn't resolve 'toTag'`);
return; return;
} }
else {
core.debug(`Resolved 'toTag' as ${toTag}`);
}
const releaseNotes = new releaseNotes_1.ReleaseNotes({ const releaseNotes = new releaseNotes_1.ReleaseNotes({
owner: owner, owner,
repo: repo, repo,
fromTag: fromTag, fromTag,
toTag: toTag, toTag,
configuration: configuration configuration
}); });
core.setOutput('changelog', yield releaseNotes.pull(token)); core.setOutput('changelog', yield releaseNotes.pull(token));
} }
@@ -397,7 +444,11 @@ class PullRequests {
getSingle(owner, repo, prNumber) { getSingle(owner, repo, prNumber) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
const pr = yield this.octokit.pulls.get({ owner, repo, pull_number: prNumber }); const pr = yield 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,
@@ -405,7 +456,9 @@ class PullRequests {
mergedAt: moment_1.default(pr.data.merged_at), mergedAt: moment_1.default(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) {
return label.name;
}),
body: pr.data.body body: pr.data.body
}; };
} }
@@ -415,29 +468,23 @@ class PullRequests {
} }
}); });
} }
getBetweenDates(owner, repo, fromDate, toDate) { getBetweenDates(owner, repo, fromDate, toDate // eslint-disable-line @typescript-eslint/no-unused-vars
) {
var e_1, _a; var e_1, _a;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const mergedPRs = []; const mergedPRs = [];
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'
}); });
try { try {
for (var _b = __asyncValues(this.octokit.paginate.iterator(options)), _c; _c = yield _b.next(), !_c.done;) { for (var _b = __asyncValues(this.octokit.paginate.iterator(options)), _c; _c = yield _b.next(), !_c.done;) {
const response = _c.value; const response = _c.value;
const prs = response.data; const prs = response.data;
const firstPR = prs[0]; for (const pr of prs.filter(p => !!p.merged_at)) {
if (firstPR.merged_at && fromDate.isAfter(moment_1.default(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_1.default(pr.merged_at)) &&
toDate.isSameOrAfter(moment_1.default(pr.merged_at))).forEach(pr => {
mergedPRs.push({ mergedPRs.push({
number: pr.number, number: pr.number,
title: pr.title, title: pr.title,
@@ -445,10 +492,17 @@ class PullRequests {
mergedAt: moment_1.default(pr.merged_at), mergedAt: moment_1.default(pr.merged_at),
author: pr.user.login, author: pr.user.login,
repoName: pr.base.repo.full_name, repoName: pr.base.repo.full_name,
labels: pr.labels.map(function (label) { return label.name; }), labels: pr.labels.map(function (label) {
return label.name;
}),
body: pr.body body: pr.body
}); });
}); }
const firstPR = prs[0];
if (firstPR.merged_at && fromDate.isAfter(moment_1.default(firstPR.merged_at))) {
// bail out early to not keep iterating on PRs super old
return sortPullRequests(mergedPRs, true);
}
} }
} }
catch (e_1_1) { e_1 = { error: e_1_1 }; } catch (e_1_1) { e_1 = { error: e_1_1 }; }
@@ -547,6 +601,7 @@ const pullRequests_1 = __webpack_require__(4217);
const transform_1 = __webpack_require__(1644); const transform_1 = __webpack_require__(1644);
const core = __importStar(__webpack_require__(2186)); const core = __importStar(__webpack_require__(2186));
const tags_1 = __webpack_require__(7532); const tags_1 = __webpack_require__(7532);
const configuration_1 = __webpack_require__(5527);
class ReleaseNotes { class ReleaseNotes {
constructor(options) { constructor(options) {
this.options = options; this.options = options;
@@ -556,20 +611,26 @@ class ReleaseNotes {
const octokit = new rest_1.Octokit({ const octokit = new rest_1.Octokit({
auth: `token ${token || process.env.GITHUB_TOKEN}` auth: `token ${token || process.env.GITHUB_TOKEN}`
}); });
const { owner, repo, fromTag, toTag, configuration } = this.options; const { owner, repo, toTag, configuration } = this.options;
if (fromTag == null) { if (!this.options.fromTag) {
core.debug(`fromTag undefined, trying to resolve via API`);
const tagsApi = new tags_1.Tags(octokit); const tagsApi = new tags_1.Tags(octokit);
const previousTag = yield tagsApi.findPredecessorTag(owner, repo, toTag); const previousTag = yield tagsApi.findPredecessorTag(owner, repo, toTag);
if (previousTag == null) { if (previousTag == null) {
core.error(`Unable to retrieve previous tag given ${toTag}`); core.error(`Unable to retrieve previous tag given ${toTag}`);
return configuration.empty_template ? configuration.empty_template : DefaultConfiguration.empty_template; return configuration.empty_template
? configuration.empty_template
: configuration_1.DefaultConfiguration.empty_template;
} }
this.options.fromTag = previousTag.name; this.options.fromTag = previousTag.name;
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`);
} }
const mergedPullRequests = yield this.getMergedPullRequests(octokit); const mergedPullRequests = yield this.getMergedPullRequests(octokit);
if (mergedPullRequests.length == 0) { if (mergedPullRequests.length === 0) {
core.warning(`No pull requests found for between ${fromTag}...${toTag}`); core.warning(`No pull requests found for between ${this.options.fromTag}...${toTag}`);
return configuration.empty_template ? configuration.empty_template : DefaultConfiguration.empty_template; return configuration.empty_template
? configuration.empty_template
: configuration_1.DefaultConfiguration.empty_template;
} }
return transform_1.buildChangelog(mergedPullRequests, configuration); return transform_1.buildChangelog(mergedPullRequests, configuration);
}); });
@@ -577,7 +638,7 @@ class ReleaseNotes {
getMergedPullRequests(octokit) { getMergedPullRequests(octokit) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const { owner, repo, fromTag, toTag } = this.options; const { owner, repo, fromTag, toTag } = this.options;
core.info(`Comparing ${owner}/${repo} ${fromTag}...${toTag}`); core.info(`Comparing ${owner}/${repo} - ${fromTag}...${toTag}`);
const commitsApi = new commits_1.Commits(octokit); const commitsApi = new commits_1.Commits(octokit);
const commits = yield commitsApi.getDiff(owner, repo, fromTag, toTag); const commits = yield commitsApi.getDiff(owner, repo, fromTag, toTag);
if (commits.length === 0) { if (commits.length === 0) {
@@ -682,7 +743,7 @@ class Tags {
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; const max = 200;
@@ -690,12 +751,12 @@ class Tags {
for (var _b = __asyncValues(this.octokit.paginate.iterator(options)), _c; _c = yield _b.next(), !_c.done;) { for (var _b = __asyncValues(this.octokit.paginate.iterator(options)), _c; _c = yield _b.next(), !_c.done;) {
const response = _c.value; const response = _c.value;
const tags = response.data; const tags = response.data;
tags.forEach(tag => { for (const tag of tags) {
tagsInfo.push({ tagsInfo.push({
name: tag.name, name: tag.name,
commit: tag.commit.sha commit: tag.commit.sha
}); });
}); }
// for performance only fetch newest 200 tags!! // for performance only fetch newest 200 tags!!
if (tagsInfo.length >= max) { if (tagsInfo.length >= max) {
break; break;
@@ -716,8 +777,8 @@ class Tags {
findPredecessorTag(owner, repo, tag) { findPredecessorTag(owner, repo, tag) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const tags = this.sortTags(yield this.getTags(owner, repo)); const tags = this.sortTags(yield this.getTags(owner, repo));
var length = tags.length; const length = tags.length;
for (var i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) { if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
return tags[i + 1]; return tags[i + 1];
} }
@@ -731,14 +792,14 @@ class Tags {
const partsA = a.name.replace(/^v/, '').split('-'); const partsA = a.name.replace(/^v/, '').split('-');
const partsB = b.name.replace(/^v/, '').split('-'); const partsB = b.name.replace(/^v/, '').split('-');
const versionCompare = partsA[0].localeCompare(partsB[0]); const versionCompare = partsA[0].localeCompare(partsB[0]);
if (versionCompare != 0) { if (versionCompare !== 0) {
return versionCompare; return versionCompare;
} }
else { else {
if (partsA.length == 1) { if (partsA.length === 1) {
return 0; return 0;
} }
else if (partsB.length == 1) { else if (partsB.length === 1) {
return 1; return 1;
} }
else { else {
@@ -765,7 +826,7 @@ exports.Tags = Tags;
2020.3.1-a01 2020.3.1-a01
2020.3.0 2020.3.0
*/ */
/***/ }), /***/ }),
@@ -798,86 +859,97 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.buildChangelog = void 0; exports.buildChangelog = void 0;
const pullRequests_1 = __webpack_require__(4217); const pullRequests_1 = __webpack_require__(4217);
const core = __importStar(__webpack_require__(2186)); const core = __importStar(__webpack_require__(2186));
const configuration_1 = __webpack_require__(5527);
function buildChangelog(prs, config) { function buildChangelog(prs, config) {
// sort to target order // sort to target order
prs = pullRequests_1.sortPullRequests(prs, config.sort.toUpperCase() === "ASC"); prs = pullRequests_1.sortPullRequests(prs, (config.sort ? config.sort : configuration_1.DefaultConfiguration.sort).toUpperCase() ===
'ASC');
const validatedTransformers = validateTransfomers(config.transformers); const validatedTransformers = validateTransfomers(config.transformers);
let transformedMap = new Map(); const transformedMap = new Map();
// convert PRs to their text representation // convert PRs to their text representation
prs.forEach(pr => { for (const pr of prs) {
transformedMap.set(pr, transform(fillTemplate(pr, config.pr_template), validatedTransformers)); transformedMap.set(pr, transform(fillTemplate(pr, config.pr_template
}); ? config.pr_template
: configuration_1.DefaultConfiguration.pr_template), validatedTransformers));
}
// bring PRs into the order of categories // bring PRs into the order of categories
let categorized = new Map(); const categorized = new Map();
config.categories.forEach(category => { if (config.categories) {
categorized.set(category, []); for (const category of config.categories) {
}); categorized.set(category, []);
let uncategorized = []; }
}
const uncategorized = [];
// bring elements in order // bring elements in order
transformedMap.forEach((body, pr) => { for (const [pr, body] of transformedMap) {
let matched = false; let matched = false;
categorized.forEach((prs, category) => { for (const [category, pullRequests] of categorized) {
if (findCommonElements3(category.labels, pr.labels)) { if (haveCommonElements(category.labels, pr.labels)) {
prs.push(body); pullRequests.push(body);
matched = true; matched = true;
} }
}); }
if (!matched) { if (!matched) {
uncategorized.push(body); uncategorized.push(body);
} }
}); }
// construct final changelog // construct final changelog
let changelog = ""; let changelog = '';
categorized.forEach((prs, category) => { for (const [category, pullRequests] of categorized) {
if (prs.length > 0) { if (pullRequests.length > 0) {
changelog = changelog + category.title + "\n\n"; changelog = `${changelog + category.title}\n\n`;
prs.forEach(pr => { for (const pr of pullRequests) {
changelog = changelog + pr + "\n"; changelog = `${changelog + pr}\n`;
}); }
// add space between // add space between
changelog = changelog + "\n"; changelog = `${changelog}\n`;
} }
}); }
let changelogUncategorized = ""; let changelogUncategorized = '';
uncategorized.forEach(pr => { for (const pr of uncategorized) {
changelogUncategorized = changelogUncategorized + pr + "\n"; changelogUncategorized = `${changelogUncategorized + pr}\n`;
}); }
// fill template // fill template
let transformedChangelog = config.template; let transformedChangelog = config.template
transformedChangelog = transformedChangelog.replace("${{CHANGELOG}}", changelog); ? config.template
transformedChangelog = transformedChangelog.replace("${{UNCATEGORIZED}}", changelogUncategorized); : configuration_1.DefaultConfiguration.template;
transformedChangelog = transformedChangelog.replace('${{CHANGELOG}}', changelog);
transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED}}', changelogUncategorized);
return transformedChangelog; return transformedChangelog;
} }
exports.buildChangelog = buildChangelog; exports.buildChangelog = buildChangelog;
function findCommonElements3(arr1, arr2) { function haveCommonElements(arr1, arr2) {
return arr1.some(item => arr2.includes(item)); return arr1.some(item => arr2.includes(item));
} }
function fillTemplate(pr, template) { function fillTemplate(pr, template) {
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, transformers) { function transform(filled, transformers) {
if (transformers.length == 0) { if (transformers.length === 0) {
return filled; return filled;
} }
let transformed = filled; let transformed = filled;
transformers.forEach(({ pattern, target }) => { for (const { target, pattern } of transformers) {
transformed = transformed.replace(pattern, target); transformed = transformed.replace(pattern, target);
}); }
return transformed; return transformed;
} }
function validateTransfomers(transformers) { function validateTransfomers(specifiedTransformers) {
const transformers = specifiedTransformers
? specifiedTransformers
: configuration_1.DefaultConfiguration.transformers;
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
}; };
} }
@@ -885,7 +957,7 @@ function validateTransfomers(transformers) {
core.warning(`Bad replacer regex: ${transformer.pattern}`); core.warning(`Bad replacer regex: ${transformer.pattern}`);
return { return {
pattern: null, pattern: null,
target: "" target: ''
}; };
} }
}) })
@@ -896,15 +968,34 @@ function validateTransfomers(transformers) {
/***/ }), /***/ }),
/***/ 918: /***/ 918:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => { /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.readConfiguration = void 0; exports.readConfiguration = void 0;
const fs = __webpack_require__(5747); const fs = __importStar(__webpack_require__(5747));
function readConfiguration(filename) { function readConfiguration(filename) {
const rawdata = fs.readFileSync(filename); const rawdata = fs.readFileSync(filename, 'utf8');
const configurationJSON = JSON.parse(rawdata); const configurationJSON = JSON.parse(rawdata);
return configurationJSON; return configurationJSON;
} }
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+80 -57
View File
@@ -1,70 +1,93 @@
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
// eslint-disable-next-line no-constant-condition
while (true) {
const compareResult = await this.octokit.repos.compareCommits({
owner,
repo,
base,
head: compareHead
})
if (compareResult.data.total_commits === 0) {
break
}
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
} }
+27 -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}.`
) )
@@ -63,24 +64,30 @@ async function run(): Promise<void> {
if (!owner) { if (!owner) {
core.error(`Missing or couldn't resolve 'owner'`) core.error(`Missing or couldn't resolve 'owner'`)
return return
} else {
core.debug(`Resolved 'owner' as ${owner}`)
} }
if (!repo) { if (!repo) {
core.error(`Missing or couldn't resolve 'owner'`) core.error(`Missing or couldn't resolve 'owner'`)
return return
} else {
core.debug(`Resolved 'repo' as ${repo}`)
} }
if (!toTag) { if (!toTag) {
core.error(`Missing or couldn't resolve 'toTag'`) core.error(`Missing or couldn't resolve 'toTag'`)
return return
} else {
core.debug(`Resolved 'toTag' as ${toTag}`)
} }
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))
+116 -106
View File
@@ -1,126 +1,136 @@
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 // eslint-disable-line @typescript-eslint/no-unused-vars
): 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)) {
type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
const prs: PullsListData = response.data as PullsListData
for (const pr of prs.filter(p => !!p.merged_at)) {
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)) { const firstPR = prs[0]
type PullsListData = RestEndpointMethodTypes["pulls"]["list"]["response"]["data"] if (firstPR.merged_at && fromDate.isAfter(moment(firstPR.merged_at))) {
const prs: PullsListData = response.data as PullsListData // bail out early to not keep iterating on PRs super old
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
})
})
}
return sortPullRequests(mergedPRs, true) return sortPullRequests(mergedPRs, true)
}
} }
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
}
+112 -91
View File
@@ -1,107 +1,128 @@
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, toTag, configuration} = this.options
if (!this.options.fromTag) {
core.debug(`fromTag undefined, trying to resolve via API`)
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
core.debug(`fromTag resolved via previousTag as: ${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(
if(fromTag == null) { `No pull requests found for between ${this.options.fromTag}...${toTag}`
const tagsApi = new Tags(octokit) )
return configuration.empty_template
const previousTag = await tagsApi.findPredecessorTag(owner, repo, toTag) ? configuration.empty_template
if(previousTag == null) { : DefaultConfiguration.empty_template
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
for (const tag of tags) {
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
*/ */
+130 -91
View File
@@ -1,114 +1,153 @@
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,
DefaultConfiguration
} 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 ? config.sort : DefaultConfiguration.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 => { for (const pr of prs) {
transformedMap.set(pr, transform(fillTemplate(pr, config.pr_template), validatedTransformers)) transformedMap.set(
}) pr,
transform(
fillTemplate(
pr,
config.pr_template
? config.pr_template
: DefaultConfiguration.pr_template
),
validatedTransformers
)
)
}
// bring PRs into the order of categories // bring PRs into the order of categories
let categorized = new Map<Category, string[]>(); const categorized = new Map<Category, string[]>()
config.categories.forEach(category => { if (config.categories) {
categorized.set(category, []) for (const category of config.categories) {
}) categorized.set(category, [])
let uncategorized: Array<string> = []; }
}
const uncategorized: string[] = []
// bring elements in order // bring elements in order
transformedMap.forEach((body, pr) => { for (const [pr, body] of transformedMap) {
let matched = false let matched = false
categorized.forEach((prs, category) => { for (const [category, pullRequests] of categorized) {
if (findCommonElements3(category.labels, pr.labels)) { if (haveCommonElements(category.labels, pr.labels)) {
prs.push(body) pullRequests.push(body)
matched = true matched = true
} }
}) }
if (!matched) { if (!matched) {
uncategorized.push(body) uncategorized.push(body)
} }
}) }
// construct final changelog // construct final changelog
let changelog = "" let changelog = ''
categorized.forEach((prs, category) => { for (const [category, pullRequests] of categorized) {
if (prs.length > 0) { if (pullRequests.length > 0) {
changelog = changelog + category.title + "\n\n" changelog = `${changelog + category.title}\n\n`
prs.forEach(pr => { for (const pr of pullRequests) {
changelog = changelog + pr + "\n" changelog = `${changelog + pr}\n`
}) }
// add space between // add space between
changelog = changelog + "\n" changelog = `${changelog}\n`
} }
}) }
let changelogUncategorized = "" let changelogUncategorized = ''
uncategorized.forEach(pr => { for (const pr of uncategorized) {
changelogUncategorized = changelogUncategorized + pr + "\n" changelogUncategorized = `${changelogUncategorized + pr}\n`
}) }
// fill template // fill template
let transformedChangelog = config.template let transformedChangelog = config.template
transformedChangelog = transformedChangelog.replace("${{CHANGELOG}}", changelog) ? config.template
transformedChangelog = transformedChangelog.replace("${{UNCATEGORIZED}}", changelogUncategorized) : DefaultConfiguration.template
return transformedChangelog; 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 }) => { for (const {target, pattern} of transformers) {
transformed = transformed.replace(pattern!!, target) transformed = transformed.replace(pattern!!, target)
}
return transformed
}
function validateTransfomers(
specifiedTransformers: Transformer[]
): RegexTransformer[] {
const transformers = specifiedTransformers
? specifiedTransformers
: DefaultConfiguration.transformers
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: ''
}
}
}) })
return transformed .filter(transformer => transformer.pattern != null)
} }
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)
}
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
} }