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}}"
+14 -22
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 configuration = readConfiguration('configuration.json')
const releaseNotes = new ReleaseNotes({ const releaseNotes = new ReleaseNotes({
owner: "mikepenz", owner: 'mikepenz',
repo: "MaterialDrawer", repo: 'release-changelog-builder-action',
fromTag: null, fromTag: null,
toTag: "v8.1.6", toTag: '0.0.3',
configuration: configuration configuration: configuration
}) })
const changeLog = await releaseNotes.pull() const changeLog = await releaseNotes.pull()
console.log(changeLog) console.log(changeLog)
expect(changeLog).toStrictEqual(` expect(changeLog).toStrictEqual(`## 🧪 Tests
<details> - [CI] Specify Test Case
<summary>Uncategorized</summary> - PR: #10
- Improve GitHub Actions `)
- PR: #2655
- 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 {
@@ -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) {
for (const category of config.categories) {
categorized.set(category, []); 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
let changelog = "";
categorized.forEach((prs, category) => {
if (prs.length > 0) {
changelog = changelog + category.title + "\n\n";
prs.forEach(pr => {
changelog = changelog + pr + "\n";
});
// add space between
changelog = changelog + "\n";
} }
}); // construct final changelog
let changelogUncategorized = ""; let changelog = '';
uncategorized.forEach(pr => { for (const [category, pullRequests] of categorized) {
changelogUncategorized = changelogUncategorized + pr + "\n"; if (pullRequests.length > 0) {
}); changelog = `${changelog + category.title}\n\n`;
for (const pr of pullRequests) {
changelog = `${changelog + pr}\n`;
}
// add space between
changelog = `${changelog}\n`;
}
}
let changelogUncategorized = '';
for (const pr of uncategorized) {
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
+33 -10
View File
@@ -1,6 +1,6 @@
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
@@ -14,18 +14,39 @@ export interface CommitInfo {
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,
repo: string,
base: string,
head: string
): Promise<CommitInfo[]> {
const commits: CommitInfo[] = await this.getDiffRemote(
owner,
repo,
base,
head
)
return this.sortCommits(commits) return this.sortCommits(commits)
} }
private async getDiffRemote(owner: string, repo: string, base: string, head: string): Promise<CommitInfo[]> { private async getDiffRemote(
owner: string,
repo: string,
base: string,
head: string
): Promise<CommitInfo[]> {
// Fetch comparisons recursively until we don't find any commits // 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. // 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 commits: RestEndpointMethodTypes['repos']['compareCommits']['response']['data']['commits'] = []
let compareHead = head let compareHead = head
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const compareResult = await this.octokit.repos.compareCommits({ owner, repo, base, head: compareHead }) const compareResult = await this.octokit.repos.compareCommits({
owner,
repo,
base,
head: compareHead
})
if (compareResult.data.total_commits === 0) { if (compareResult.data.total_commits === 0) {
break break
} }
@@ -33,10 +54,12 @@ export class Commits {
compareHead = `${commits[0].sha}^` compareHead = `${commits[0].sha}^`
} }
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(commit.commit.committer.date), date: moment(commit.commit.committer.date),
author: commit.commit.author.name, author: commit.commit.author.name,
+18 -18
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: []
} }
+12 -5
View File
@@ -1,4 +1,3 @@
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'
@@ -9,7 +8,6 @@ export async function createCommandManager(
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")
@@ -41,7 +39,6 @@ function directoryExistsSync(path: string, required?: boolean): boolean {
throw new Error(`Directory '${path}' does not exist`) throw new Error(`Directory '${path}' does not exist`)
} }
class GitCommandManager { class GitCommandManager {
private gitPath = '' private gitPath = ''
private workingDirectory = '' private workingDirectory = ''
@@ -54,8 +51,18 @@ class GitCommandManager {
} }
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',
'--tags',
'--skip=0',
'--max-count=1'
])
const output = await this.execGit([
'describe',
'--abbrev=0',
'--tags',
revListOutput.stdout.trim()
])
return output.stdout.trim() return output.stdout.trim()
} }
+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))
+38 -28
View File
@@ -1,8 +1,8 @@
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
@@ -11,16 +11,24 @@ export interface PullRequestInfo {
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(
owner: string,
repo: string,
prNumber: number
): Promise<PullRequestInfo | null> {
try { try {
const pr = await this.octokit.pulls.get({ owner, repo, pull_number: prNumber }) const pr = await this.octokit.pulls.get({
owner,
repo,
pull_number: prNumber
})
return { return {
number: pr.data.number, number: pr.data.number,
@@ -29,7 +37,9 @@ export class PullRequests {
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) {
return label.name
}),
body: pr.data.body body: pr.data.body
} }
} catch (e) { } catch (e) {
@@ -42,33 +52,22 @@ export class PullRequests {
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)) { for await (const response of this.octokit.paginate.iterator(options)) {
type PullsListData = RestEndpointMethodTypes["pulls"]["list"]["response"]["data"] type PullsListData = RestEndpointMethodTypes['pulls']['list']['response']['data']
const prs: PullsListData = response.data as PullsListData const prs: PullsListData = response.data as PullsListData
const firstPR = prs[0] for (const pr of prs.filter(p => !!p.merged_at)) {
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({ mergedPRs.push({
number: pr.number, number: pr.number,
title: pr.title, title: pr.title,
@@ -76,10 +75,18 @@ export class PullRequests {
mergedAt: moment(pr.merged_at), mergedAt: moment(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(firstPR.merged_at))) {
// bail out early to not keep iterating on PRs super old
return sortPullRequests(mergedPRs, true)
}
} }
return sortPullRequests(mergedPRs, true) return sortPullRequests(mergedPRs, true)
@@ -102,7 +109,10 @@ export class PullRequests {
} }
} }
export function sortPullRequests(pullRequests: PullRequestInfo[], ascending: Boolean): PullRequestInfo[] { export function sortPullRequests(
pullRequests: PullRequestInfo[],
ascending: Boolean
): PullRequestInfo[] {
if (ascending) { if (ascending) {
pullRequests.sort((a, b) => { pullRequests.sort((a, b) => {
if (a.mergedAt.isBefore(b.mergedAt)) { if (a.mergedAt.isBefore(b.mergedAt)) {
+43 -22
View File
@@ -1,10 +1,10 @@
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
@@ -15,42 +15,50 @@ export interface ReleaseNotesOptions {
} }
export class ReleaseNotes { export class ReleaseNotes {
constructor(private options: ReleaseNotesOptions) { constructor(private options: ReleaseNotesOptions) {}
}
async pull(token?: string): Promise<string> { async pull(token?: string): Promise<string> {
const octokit = new Octokit({ const octokit = new 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(octokit) const tagsApi = new Tags(octokit)
const previousTag = await tagsApi.findPredecessorTag(owner, repo, toTag) const previousTag = await 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
: DefaultConfiguration.empty_template
} }
this.options.fromTag = previousTag.name this.options.fromTag = previousTag.name
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
} }
const mergedPullRequests = await this.getMergedPullRequests(octokit) const mergedPullRequests = await this.getMergedPullRequests(octokit)
if (mergedPullRequests.length == 0) { if (mergedPullRequests.length === 0) {
core.warning(`No pull requests found for between ${fromTag}...${toTag}`) core.warning(
return configuration.empty_template ? configuration.empty_template : DefaultConfiguration.empty_template `No pull requests found for between ${this.options.fromTag}...${toTag}`
)
return configuration.empty_template
? configuration.empty_template
: DefaultConfiguration.empty_template
} }
return buildChangelog(mergedPullRequests, configuration) return buildChangelog(mergedPullRequests, configuration)
} }
private async getMergedPullRequests(octokit: Octokit): Promise<PullRequestInfo[]> { private async getMergedPullRequests(
octokit: Octokit
): Promise<PullRequestInfo[]> {
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(octokit) const commitsApi = new Commits(octokit)
const commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag) const commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
@@ -64,10 +72,17 @@ export class ReleaseNotes {
const fromDate = firstCommit.date const fromDate = firstCommit.date
const toDate = lastCommit.date const toDate = lastCommit.date
core.info(`Fetching PRs between dates ${fromDate.toISOString()} ${toDate.toISOString()} for ${owner}/${repo}`) core.info(
`Fetching PRs between dates ${fromDate.toISOString()} ${toDate.toISOString()} for ${owner}/${repo}`
)
const pullRequestsApi = new PullRequests(octokit) const pullRequestsApi = new PullRequests(octokit)
const pullRequests = await pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate) const pullRequests = await pullRequestsApi.getBetweenDates(
owner,
repo,
fromDate,
toDate
)
core.info(`Found ${pullRequests.length} merged PRs for ${owner}/${repo}`) core.info(`Found ${pullRequests.length} merged PRs for ${owner}/${repo}`)
@@ -90,7 +105,11 @@ export class ReleaseNotes {
filteredPullRequests.push(pullRequestsByNumber[commit.prNumber]) filteredPullRequests.push(pullRequestsByNumber[commit.prNumber])
} else if (fromDate.toISOString() === toDate.toISOString()) { } else if (fromDate.toISOString() === toDate.toISOString()) {
core.info(`${prRef} not in date range, fetching explicitly`) core.info(`${prRef} not in date range, fetching explicitly`)
const pullRequest = await pullRequestsApi.getSingle(owner, repo, commit.prNumber) const pullRequest = await pullRequestsApi.getSingle(
owner,
repo,
commit.prNumber
)
if (pullRequest) { if (pullRequest) {
filteredPullRequests.push(pullRequest) filteredPullRequests.push(pullRequest)
@@ -98,7 +117,9 @@ export class ReleaseNotes {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`) core.warning(`${prRef} not found! Commit text: ${commit.summary}`)
} }
} else { } else {
core.info(`${prRef} not in date range, likely a merge commit from a fork-to-fork PR`) core.info(
`${prRef} not in date range, likely a merge commit from a fork-to-fork PR`
)
} }
} }
+21 -18
View File
@@ -1,9 +1,8 @@
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
} }
@@ -15,21 +14,21 @@ export 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: number = 200 const max = 200
for await (const response of this.octokit.paginate.iterator(options)) { for await (const response of this.octokit.paginate.iterator(options)) {
type TagsListData = RestEndpointMethodTypes["repos"]["listTags"]["response"]["data"] type TagsListData = RestEndpointMethodTypes['repos']['listTags']['response']['data']
const tags: TagsListData = response.data as TagsListData const tags: TagsListData = response.data as TagsListData
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) {
@@ -37,16 +36,21 @@ export class Tags {
} }
} }
core.info(`Found ${tagsInfo.length} (fetching max: ${max}) tags from the GitHub API for ${owner}/${repo}`) core.info(
`Found ${tagsInfo.length} (fetching max: ${max}) tags from the GitHub API for ${owner}/${repo}`
)
return tagsInfo return tagsInfo
} }
async findPredecessorTag(
async findPredecessorTag(owner: string, repo: string, tag: string): Promise<TagInfo | null> { owner: string,
repo: string,
tag: string
): Promise<TagInfo | null> {
const tags = this.sortTags(await this.getTags(owner, repo)) const tags = this.sortTags(await 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]
} }
@@ -61,12 +65,12 @@ export 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 {
return partsA[1].localeCompare(partsB[1]) return partsA[1].localeCompare(partsB[1])
@@ -77,7 +81,6 @@ export class Tags {
} }
} }
/* /*
2020.3.2 ( should resolve 2020.3.1 ) 2020.3.2 ( should resolve 2020.3.1 )
+90 -51
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(
prs: PullRequestInfo[],
config: Configuration
): string {
// sort to target order // sort to target order
prs = sortPullRequests(prs, config.sort.toUpperCase() === "ASC") 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) {
for (const category of config.categories) {
categorized.set(category, []) 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 return transformed
} }
function validateTransfomers(transformers: Transformer[]): RegexTransformer[] { function validateTransfomers(
specifiedTransformers: Transformer[]
): RegexTransformer[] {
const transformers = specifiedTransformers
? specifiedTransformers
: 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
} }
} catch (e) { } catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`) core.warning(`Bad replacer regex: ${transformer.pattern}`)
return { return {
pattern: null, pattern: null,
target: "" target: ''
} }
} }
}) })
.filter(transformer => transformer.pattern != null) .filter(transformer => transformer.pattern != null)
} }
interface RegexTransformer { interface RegexTransformer {
pattern: RegExp | null; pattern: RegExp | null
target: string; target: string
} }
+6 -5
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
} }