Merge pull request #11 from mikepenz/feature/specify_action
[CI] Update github action specification
This commit is contained in:
@@ -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
@@ -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>`)
|
`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+186
-95
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+80
-57
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user