Merge pull request #30 from mikepenz/develop

develop -> main
This commit is contained in:
Mike Penz
2020-10-17 20:18:23 +02:00
committed by GitHub
19 changed files with 599 additions and 319 deletions
+16 -7
View File
@@ -22,6 +22,18 @@ jobs:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
# Won't support providing a configuration file, as no checkout was done
- name: "Configuration without Checkout"
id: without_checkout
uses: mikepenz/release-changelog-builder-action@develop
with:
toTag: "v0.0.3"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Configuration without Checkout Changelog
run: echo "${{steps.without_checkout.outputs.changelog}}"
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified
@@ -32,31 +44,27 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Minimal
- name: Echo Minimal Configuration Changelog
run: echo "${{steps.minimal_release.outputs.changelog}}"
- name: "Complex Configuration"
id: complex_release
uses: ./
with:
configuration: "configuration_complex.json"
configuration: "configs/configuration_complex.json"
owner: "mikepenz"
repo: "release-changelog-builder-action"
fromTag: "v0.0.1"
toTag: "v0.0.3"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Complex
- name: Echo Complex Configuration Changelog
run: echo "${{steps.complex_release.outputs.changelog}}"
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified
- name: Prepare Tag
if: startsWith(github.ref, 'refs/tags/')
id: tag_version
@@ -66,6 +74,7 @@ jobs:
id: github_release
uses: mikepenz/release-changelog-builder-action@main
with:
configuration: "configs/configuration_repo.json"
toTag: ${{ steps.tag_version.outputs.VERSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+140 -39
View File
@@ -1,11 +1,52 @@
<div align="center">
:octocat:📄🔖📦
</div>
<h1 align="center">
release-changelog-builder-action
</h1>
# release-changelog-builder-action
<p align="center">
... a github action that builds your release notes fast, easy and exactly the way you want.
</p>
Builds the release notes between two tags (or refs) from pull requests merged.
<div align="center">
<a href="https://github.com/mikepenz/release-changelog-builder-action/actions">
<img src="https://github.com/mikepenz/release-changelog-builder-action/workflows/CI/badge.svg"/>
</a>
</div>
<br />
## Action usage
-------
Include this action in your build by defining the action in your workflow:
<p align="center">
<a href="#whats-included-">What's included 🚀</a> &bull;
<a href="#setup">Setup 🛠️</a> &bull;
<a href="#customization-%EF%B8%8F">Customization 🖍️</a> &bull;
<a href="#contribute-">Contribute 🧬</a> &bull;
<a href="#complete-sample-%EF%B8%8F">Complete Sample 🖥️</a> &bull;
<a href="#license">License 📓</a>
</p>
-------
### What's included 🚀
- Super simple integration
- even on huge repositories with hundreds of tags
- Parallel releases support
- Blazingly fast execution
- Supports any git project
- Highly flexible configuration
- Lightweight
- Supports any branch
-------
## Setup
### Configure the workflow
Specify the action as part of your GitHub actions workflow:
```yml
- name: "Build Changelog"
@@ -16,31 +57,41 @@ Include this action in your build by defining the action in your workflow:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
This will automatically pull the tag from the current commit (the latest tag), and try to resolve the tag before this.
By default the action will try to automatically retrieve the `tag` from the current commit and automtacally resolve the `tag` before. Read more about this here.
## Action outputs
### Action outputs
The result of this action is returned via the outputs, and can be retrieved via the `changelog` value in the step afterwards. See the `test.yml` for a sample.
The action will succeed and return the `changelog` as a step output. Use it in any follow up step by referencing via its id. For example `build_changelog`.
```yml
# ${{steps.{CHANGELOG_STEP_ID}.outputs.changelog}}
${{steps.build_changelog.outputs.changelog}}
```
## Configuration
## Customization 🖍️
By default the action will look for a file called `configuration.json` within the root of the repository to load the config from. If this file does not exist, defaults are used.
### Changelog Configuration
The action supports flexible configuration options to modify vast areas of its behavior. To do so, provide the configuration file to the workflow using the `configuration` setting.
```yml
- name: "Build Changelog"
uses: mikepenz/release-changelog-builder-action@{latest-release}
with:
configuration: "configuration.json"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
This configuration is a `.json` file in the following format.
```json
{
"categories": [
{
"title": "## 🚀 Features",
"labels": ["feature"]
},
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix"]
@@ -69,11 +120,11 @@ By default the action will look for a file called `configuration.json` within th
}
```
Any section of the configruation can be ommited, to have defaults apply
Any section of the configruation can be ommited to have defaults apply.
Defaults for the configuraiton can be found in the [configuration.ts](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/src/configuration.ts)
## Advanced workflow specification
### Advanced workflow specification
For advanced usecases additional settings can be provided to the action
@@ -86,32 +137,83 @@ For advanced usecases additional settings can be provided to the action
configuration: "configuration_complex.json"
owner: "mikepenz"
repo: "release-changelog-builder-action"
ignorePreReleases: "false" # allows to skip any pre releases, if `fromTag` needs to be automatically resolved (ignores 0.0.2-rc02 for example) - only relevant if `fromTag` is not provided
ignorePreReleases: "false"
fromTag: "0.0.2"
toTag: "0.0.3"
token: ${{ secrets.GITHUB_TOKEN }} # the token to use, for a different repository a PAT is required (Personal access token)
token: ${{ secrets.PAT }}
```
## PR Template placeholders
💡 `ignorePreReleases` will be ignored, if `fromTag` is specified. `${{ secrets.GITHUB_TOKEN }}` only grants rights to the current repository, for other repos please use a PAT (Personal Access Token).
| Variable | Description |
| --------- | -------------------------- |
| `${{NUMBER}}` | Pull request number |
| `${{TITLE}}` | The title of the pull request |
| `${{URL}}` | The URL linking to the pull request |
| `${{MERGED_AT}}` | The time this PR was merged |
| `${{AUTHOR}}` | The author of the pull request |
| `${{BODY}}` | The body / description of the pull request |
### PR Template placeholders
## Template placeholdrs
Table of supported placeholders allowed to be used in the `template` configuration.
| Variable | Description |
| --------- | -------------------------- |
| `${{CHANGELOG}}` | The contents of the main changelog, matching the labels as specified in the categories configuration |
| `${{UNCATEGORIZED}}` | All pull requests not matching a label |
| **Placeholder** | **Description** |
|------------------|-------------------------------------------------------------|
| `${{NUMBER}}` | The number referencing this pull request. E.g. 13 |
| `${{TITLE}}` | Specified title of the merged pull request |
| `${{URL}}` | Url linking to the pull request on GitHub |
| `${{MERGED_AT}}` | The ISO time, the pull request was merged at |
| `${{AUTHOR}}` | Author creating and opening the pull request |
| `${{LABELS}}` | The labels associated with this pull request, joined by `,` |
| `${{MILESTONE}}` | Milestone this PR was part of, as assigned on GitHub |
| `${{BODY}}` | Description/Body of the pull request as specified on GitHub |
| `${{ASSIGNEES}}` | Login names of assigned GitHub users, joined by `,` |
| `${{REVIEWERS}}` | GitHub Login names of specified reviewers, joined by `,` |
### Template placeholders
Table of supported placeholders allowed to be used in the `pr_template` configuration.
| **Placeholder** | **Description** |
|----------------------|-------------------------------------------------------------------------------------------------|
| `${{CHANGELOG}}` | The contents of the changelog, matching the labels as specified in the categories configuration |
| `${{UNCATEGORIZED}}` | All pull requests not matching a specified label in categories |
# Contribute
## Complete Sample 🖥️
Below is a complete example showcasing how to define a build, which is executed when tagging the project. It consists of:
- Prepare tag, via the GITHUB_REF environment variable
- Build changelog, given the tag
- Create release on GitHub - specifying body with constructed changelog
```yml
name: 'CI'
on:
push:
tags:
- '*'
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Retrieve tag
if: startsWith(github.ref, 'refs/tags/')
id: tag_version
run: echo ::set-output name=VERSION::$(echo ${GITHUB_REF:10})
- name: Build Changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@main
with:
toTag: ${{ steps.tag_version.outputs.VERSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: ${{steps.github_release.outputs.changelog}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
## Contribute 🧬
```bash
# Install the dependencies
@@ -127,29 +229,28 @@ $ npm test
$ npm run lint -- --fix
```
It's suggested to export the token to your path, before running the tests, so API calls can be done to github.
It's suggested to export the token to your path before running the tests, so that API calls can be done to github.
```bash
export GITHUB_TOKEN=your_personal_github_pat
```
# Developed By
## Developed By
* Mike Penz
* [mikepenz.com](http://mikepenz.com) - <mikepenz@gmail.com>
* [paypal.me/mikepenz](http://paypal.me/mikepenz)
# Credits
## Credits
Core parts of the PR fetching logic, are based on [pull-release-notes](https://github.com/nblagoev/pull-release-notes)
Core parts of the PR fetching logic are based on [pull-release-notes](https://github.com/nblagoev/pull-release-notes)
- Nikolay Blagoev - [GitHub](https://github.com/nblagoev/)
# License
## License
Copyright for portions of pr-release-notes are held by Nikolay Blagoev, 2019-2020 as part of project pull-release-notes. All other copyright for project pr-release-notes are held by Mike Penz, 2020.
# Fork License
## Fork License
All patches and changes applied to the original source are licensed under the Apache 2.0 license.
+30 -12
View File
@@ -1,12 +1,5 @@
import {wait} from '../src/wait'
import * as process from 'process'
import * as cp from 'child_process'
import * as path from 'path'
import {ReleaseNotes} from '../src/releaseNotes'
import {readConfiguration} from '../src/utils'
import {createCommandManager} from '../src/git-helper'
import * as core from '@actions/core'
import {Tags} from '../src/tags'
// shows how the runner will run a javascript action with env / stdout protocol
/*
@@ -21,11 +14,28 @@ test('test runs', () => {
console.log(cp.execSync(`node ${ip}`, options).toString())
})
*/
it('Should have empty changelog (tags)', async () => {
jest.setTimeout(180000)
const configuration = readConfiguration('configs/configuration.json')!!
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
fromTag: 'v0.0.1',
toTag: 'v0.0.2',
ignorePreReleases: false,
configuration: configuration
})
const changeLog = await releaseNotes.pull()
console.log(changeLog)
expect(changeLog).toStrictEqual(`- no changes`)
})
it('Should match generated changelog (tags)', async () => {
jest.setTimeout(180000)
const configuration = readConfiguration('configuration.json')
const configuration = readConfiguration('configs/configuration.json')!!
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
@@ -48,7 +58,7 @@ it('Should match generated changelog (tags)', async () => {
it('Should match generated changelog (unspecified fromTag)', async () => {
jest.setTimeout(180000)
const configuration = readConfiguration('configuration.json')
const configuration = readConfiguration('configs/configuration.json')!!
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
@@ -71,7 +81,7 @@ it('Should match generated changelog (unspecified fromTag)', async () => {
it('Should match generated changelog (refs)', async () => {
jest.setTimeout(180000)
const configuration = readConfiguration('configuration.json')
const configuration = readConfiguration('configs/configuration_all_placeholders.json')!!
const releaseNotes = new ReleaseNotes({
owner: 'mikepenz',
repo: 'release-changelog-builder-action',
@@ -85,8 +95,16 @@ it('Should match generated changelog (refs)', async () => {
console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests
- [CI] Specify Test Case
- PR: #10
[CI] Specify Test Case
10
https://github.com/mikepenz/release-changelog-builder-action/pull/10
2020-10-16T13:59:36.000Z
mikepenz
test
1.0.0
- specify test case
mikepenz, nhoelzl
nhoelzl
`)
})
+10 -12
View File
@@ -1,32 +1,30 @@
name: 'Release Changelog Builder'
description: 'Pulls all merged pull requests, and constructs the changelog for a release'
description: 'A GitHub action that builds your release notes fast, easy and exactly the way you want.'
author: 'Mike Penz'
branding:
icon: 'award'
color: 'green'
inputs:
configuration:
required: true
description: 'path to the configuration file'
default: "configuration.json"
description: 'Defines the relative path to the configuration file.'
path:
description: 'the path to runt his action in'
description: 'Defines the directory the repo is located in (checkout directory)'
owner:
description: 'the owner of the repository to create the changelog for'
description: 'Defines the owner of the repository to create the changelog for'
repo:
description: 'the repository to create the changelog for'
description: 'Defines the repository to create the changelog for'
fromTag:
description: 'the previous tag to compare against'
description: 'Defines the previous tag to compare against'
toTag:
description: 'the new tag created'
description: 'Defines the newly tag created'
ignorePreReleases:
description: 'defines if only full releases should be considered to compare against (Only used if fromTag is not defined). E.g. for 1.0.1... 1.0.0-rc02 <- ignore, 1.0.0 <- pick'
description: 'Defines if the action will only use full releases to compare against (Only used if fromTag is not defined). E.g. for 1.0.1... 1.0.0-rc02 <- ignore, 1.0.0 <- pick'
default: "false"
token:
description: 'the token to use to execute the git API requests'
description: 'Defines the token to use to execute the git API requests with, uses `env.GITHUB_TOKEN` by default'
outputs:
changelog:
description: The generated changelog as markdown.
description: Returns the generated changelog as markdown.
runs:
using: 'node12'
main: 'dist/index.js'
@@ -4,10 +4,6 @@
"title": "## 🚀 Features",
"labels": ["feature"]
},
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix"]
@@ -0,0 +1,3 @@
{
"pr_template": "${{TITLE}}\n${{NUMBER}}\n${{URL}}\n${{MERGED_AT}}\n${{AUTHOR}}\n${{LABELS}}\n${{MILESTONE}}\n${{BODY}}\n${{ASSIGNEES}}\n${{REVIEWERS}}"
}
@@ -4,10 +4,6 @@
"title": "## 🚀 Features",
"labels": ["feature"]
},
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix"]
+20
View File
@@ -0,0 +1,20 @@
{
"categories": [
{
"title": "## 🚀 Features",
"labels": ["feature"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix"]
},
{
"title": "## 🧪 Tests",
"labels": ["test"]
},
{
"title": "## 💬 Other",
"labels": ["other"]
}
]
}
Generated Vendored
+190 -117
View File
@@ -72,7 +72,7 @@ class Commits {
commits = compareResult.data.commits.concat(commits);
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 => ({
sha: commit.sha,
summary: commit.commit.message.split('\n')[0],
@@ -126,14 +126,27 @@ exports.DefaultConfiguration = {
template: '${{CHANGELOG}}',
pr_template: '- ${{TITLE}}\n - PR: #${{NUMBER}}',
empty_template: '- no changes',
categories: [],
categories: [
{
title: '## 🚀 Features',
labels: ['feature']
},
{
title: '## 🐛 Fixes',
labels: ['fix']
},
{
title: '## 🧪 Tests',
labels: ['test']
}
],
transformers: [] // transformers to apply on the PR description according to the `pr_template`
};
/***/ }),
/***/ 9621:
/***/ 353:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
@@ -169,39 +182,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createCommandManager = void 0;
const exec = __importStar(__webpack_require__(1514));
const fs = __importStar(__webpack_require__(5747));
const io = __importStar(__webpack_require__(7436));
const utils_1 = __webpack_require__(918);
function createCommandManager(workingDirectory) {
return __awaiter(this, void 0, void 0, function* () {
return yield GitCommandManager.createCommandManager(workingDirectory);
});
}
exports.createCommandManager = createCommandManager;
function directoryExistsSync(path, required) {
if (!path) {
throw new Error("Arg 'path' must not be empty");
}
let stats;
try {
stats = fs.statSync(path);
}
catch (error) {
if (error.code === 'ENOENT') {
if (!required) {
return false;
}
throw new Error(`Directory '${path}' does not exist`);
}
throw new Error(`Encountered an error when checking whether path '${path}' exists: ${error.message}`);
}
if (stats.isDirectory()) {
return true;
}
else if (!required) {
return false;
}
throw new Error(`Directory '${path}' does not exist`);
}
class GitCommandManager {
// Private constructor; use createCommandManager()
constructor() {
@@ -237,7 +225,7 @@ class GitCommandManager {
}
execGit(args, allowAllExitCodes = false, silent = false) {
return __awaiter(this, void 0, void 0, function* () {
directoryExistsSync(this.workingDirectory, true);
utils_1.directoryExistsSync(this.workingDirectory, true);
const result = new GitOutput();
const stdout = [];
const options = {
@@ -309,11 +297,13 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__webpack_require__(2186));
const utils_1 = __webpack_require__(918);
const releaseNotes_1 = __webpack_require__(5882);
const git_helper_1 = __webpack_require__(9621);
const gitHelper_1 = __webpack_require__(353);
const github = __importStar(__webpack_require__(5438));
const path = __importStar(__webpack_require__(5622));
const configuration_1 = __webpack_require__(5527);
function run() {
return __awaiter(this, void 0, void 0, function* () {
core.startGroup(`📘 Reading input values`);
try {
let githubWorkspacePath = process.env['GITHUB_WORKSPACE'];
if (!githubWorkspacePath) {
@@ -325,17 +315,27 @@ function run() {
repositoryPath = path.resolve(githubWorkspacePath, repositoryPath);
core.debug(`repositoryPath = '${repositoryPath}'`);
const configurationFile = core.getInput('configuration');
const configurationPath = path.resolve(githubWorkspacePath, configurationFile);
core.debug(`configurationPath = '${configurationPath}'`);
const configuration = utils_1.readConfiguration(configurationPath);
let configuration = configuration_1.DefaultConfiguration;
if (configurationFile) {
const configurationPath = path.resolve(githubWorkspacePath, configurationFile);
core.debug(`configurationPath = '${configurationPath}'`);
const providedConfiguration = utils_1.readConfiguration(configurationPath);
if (!providedConfiguration) {
core.info(`⚠️ Configuration provided, but it couldn't be found, or failed to parse. Fallback to Defaults`);
}
else {
configuration = providedConfiguration;
}
}
const token = core.getInput('token');
let owner = core.getInput('owner');
let repo = core.getInput('repo');
const fromTag = core.getInput('fromTag');
let toTag = core.getInput('toTag');
const ignorePreReleases = core.getInput('ignorePreReleases');
if (!toTag) {
// if not specified try to retrieve tag from git
const gitHelper = yield git_helper_1.createCommandManager(repositoryPath);
const gitHelper = yield gitHelper_1.createCommandManager(repositoryPath);
const latestTag = yield gitHelper.latestTag();
toTag = latestTag;
core.debug(`toTag = '${latestTag}'`);
@@ -355,31 +355,33 @@ function run() {
repo = splitRepository[1];
}
if (!owner) {
core.error(`Missing or couldn't resolve 'owner'`);
core.error(`💥 Missing or couldn't resolve 'owner'`);
return;
}
else {
core.debug(`Resolved 'owner' as ${owner}`);
}
if (!repo) {
core.error(`Missing or couldn't resolve 'owner'`);
core.error(`💥 Missing or couldn't resolve 'owner'`);
return;
}
else {
core.debug(`Resolved 'repo' as ${repo}`);
}
if (!toTag) {
core.error(`Missing or couldn't resolve 'toTag'`);
core.error(`💥 Missing or couldn't resolve 'toTag'`);
return;
}
else {
core.debug(`Resolved 'toTag' as ${toTag}`);
}
core.endGroup();
const releaseNotes = new releaseNotes_1.ReleaseNotes({
owner,
repo,
fromTag,
toTag,
ignorePreReleases: ignorePreReleases === 'true',
configuration
});
core.setOutput('changelog', yield releaseNotes.pull(token));
@@ -446,6 +448,7 @@ class PullRequests {
this.octokit = octokit;
}
getSingle(owner, repo, prNumber) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
try {
const pr = yield this.octokit.pulls.get({
@@ -463,17 +466,25 @@ class PullRequests {
labels: pr.data.labels.map(function (label) {
return label.name;
}),
body: pr.data.body
milestone: (_a = pr.data.milestone) === null || _a === void 0 ? void 0 : _a.title,
body: pr.data.body,
assignees: (_b = pr.data.assignees) === null || _b === void 0 ? void 0 : _b.map(function (asignee) {
return asignee.login;
}),
requestedReviewers: (_c = pr.data.requested_reviewers) === null || _c === void 0 ? void 0 : _c.map(function (reviewer) {
return reviewer.login;
})
};
}
catch (e) {
core.warning(`Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`);
core.warning(`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`);
return null;
}
});
}
getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests) {
var e_1, _a;
var _b, _c, _d, _e;
return __awaiter(this, void 0, void 0, function* () {
const mergedPRs = [];
const options = this.octokit.pulls.list.endpoint.merge({
@@ -484,8 +495,8 @@ class PullRequests {
direction: 'desc'
});
try {
for (var _b = __asyncValues(this.octokit.paginate.iterator(options)), _c; _c = yield _b.next(), !_c.done;) {
const response = _c.value;
for (var _f = __asyncValues(this.octokit.paginate.iterator(options)), _g; _g = yield _f.next(), !_g.done;) {
const response = _g.value;
const prs = response.data;
for (const pr of prs.filter(p => !!p.merged_at)) {
mergedPRs.push({
@@ -495,17 +506,24 @@ class PullRequests {
mergedAt: moment_1.default(pr.merged_at),
author: pr.user.login,
repoName: pr.base.repo.full_name,
labels: pr.labels.map(function (label) {
labels: (_b = pr.labels) === null || _b === void 0 ? void 0 : _b.map(function (label) {
return label.name;
}),
body: pr.body
milestone: (_c = pr.milestone) === null || _c === void 0 ? void 0 : _c.title,
body: pr.body,
assignees: (_d = pr.assignees) === null || _d === void 0 ? void 0 : _d.map(function (asignee) {
return asignee.login;
}),
requestedReviewers: (_e = pr.requested_reviewers) === null || _e === void 0 ? void 0 : _e.map(function (reviewer) {
return reviewer.login;
})
});
}
const firstPR = prs[0];
if ((firstPR.merged_at && fromDate.isAfter(moment_1.default(firstPR.merged_at))) ||
mergedPRs.length >= maxPullRequests) {
if (mergedPRs.length >= maxPullRequests) {
core.info(`Reached 'maxPullRequests' count ${maxPullRequests}`);
core.info(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests}`);
}
// bail out early to not keep iterating on PRs super old
return sortPullRequests(mergedPRs, true);
@@ -515,7 +533,7 @@ class PullRequests {
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b);
if (_g && !_g.done && (_a = _f.return)) yield _a.call(_f);
}
finally { if (e_1) throw e_1.error; }
}
@@ -626,67 +644,72 @@ class ReleaseNotes {
this.options = options;
}
pull(token) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const octokit = new rest_1.Octokit({
auth: `token ${token || process.env.GITHUB_TOKEN}`
});
const { owner, repo, toTag, configuration } = this.options;
const { owner, repo, toTag, ignorePreReleases, configuration } = this.options;
if (!this.options.fromTag) {
core.startGroup(`🔖 Resolve previous tag`);
core.debug(`fromTag undefined, trying to resolve via API`);
const tagsApi = new tags_1.Tags(octokit);
const previousTag = yield tagsApi.findPredecessorTag(owner, repo, toTag, configuration.max_tags_to_fetch
? configuration.max_tags_to_fetch
: configuration_1.DefaultConfiguration.max_tags_to_fetch);
const previousTag = yield tagsApi.findPredecessorTag(owner, repo, toTag, ignorePreReleases, (_a = configuration.max_tags_to_fetch) !== null && _a !== void 0 ? _a : configuration_1.DefaultConfiguration.max_tags_to_fetch);
if (previousTag == null) {
core.error(`Unable to retrieve previous tag given ${toTag}`);
return configuration.empty_template
? configuration.empty_template
: configuration_1.DefaultConfiguration.empty_template;
core.error(`💥 Unable to retrieve previous tag given ${toTag}`);
return ((_b = configuration.empty_template) !== null && _b !== void 0 ? _b : configuration_1.DefaultConfiguration.empty_template);
}
this.options.fromTag = previousTag.name;
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`);
core.endGroup();
}
core.startGroup(`🚀 Load pull requests`);
const mergedPullRequests = yield this.getMergedPullRequests(octokit);
core.endGroup();
if (mergedPullRequests.length === 0) {
core.warning(`No pull requests found for between ${this.options.fromTag}...${toTag}`);
return configuration.empty_template
? configuration.empty_template
: configuration_1.DefaultConfiguration.empty_template;
core.warning(`⚠️ No pull requests found`);
return (_c = configuration.empty_template) !== null && _c !== void 0 ? _c : configuration_1.DefaultConfiguration.empty_template;
}
return transform_1.buildChangelog(mergedPullRequests, configuration);
core.startGroup('📦 Build changelog');
const resultChangelog = transform_1.buildChangelog(mergedPullRequests, configuration);
core.endGroup();
return resultChangelog;
});
}
getMergedPullRequests(octokit) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const { owner, repo, fromTag, toTag, configuration } = this.options;
core.info(`Comparing ${owner}/${repo} - ${fromTag}...${toTag}`);
core.info(`Comparing ${owner}/${repo} - '${fromTag}...${toTag}'`);
const commitsApi = new commits_1.Commits(octokit);
const commits = yield commitsApi.getDiff(owner, repo, fromTag, toTag);
let commits;
try {
commits = yield commitsApi.getDiff(owner, repo, fromTag, toTag);
}
catch (error) {
core.error(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`);
return [];
}
if (commits.length === 0) {
core.warning(`💥 No commits found between - ${fromTag}...${toTag}`);
return [];
}
const firstCommit = commits[0];
const lastCommit = commits[commits.length - 1];
let fromDate = firstCommit.date;
const toDate = lastCommit.date;
const maxDays = configuration.max_back_track_time_days
? configuration.max_back_track_time_days
: configuration_1.DefaultConfiguration.max_back_track_time_days;
const maxDays = (_a = configuration.max_back_track_time_days) !== null && _a !== void 0 ? _a : configuration_1.DefaultConfiguration.max_back_track_time_days;
const maxFromDate = toDate.clone().subtract(maxDays, 'days');
if (maxFromDate.isAfter(fromDate)) {
core.info(`Adjusted 'fromDate' to go max ${maxDays} back`);
core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`);
fromDate = maxFromDate;
}
core.info(`Fetching PRs between dates ${fromDate.toISOString()} to ${toDate.toISOString()} for ${owner}/${repo}`);
core.info(`Fetching PRs between dates ${fromDate.toISOString()} to ${toDate.toISOString()} for ${owner}/${repo}`);
const pullRequestsApi = new pullRequests_1.PullRequests(octokit);
const pullRequests = yield pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate, configuration.max_pull_requests
? configuration.max_pull_requests
: configuration_1.DefaultConfiguration.max_pull_requests);
core.info(`Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`);
const prCommits = pullRequestsApi.filterCommits(commits, configuration.exclude_merge_branches
? configuration.exclude_merge_branches
: configuration_1.DefaultConfiguration.exclude_merge_branches);
core.info(`Retrieved ${prCommits.length} PR merge commits for ${owner}/${repo}`);
const pullRequests = yield pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate, (_b = configuration.max_pull_requests) !== null && _b !== void 0 ? _b : configuration_1.DefaultConfiguration.max_pull_requests);
core.info(`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`);
const prCommits = pullRequestsApi.filterCommits(commits, (_c = configuration.exclude_merge_branches) !== null && _c !== void 0 ? _c : configuration_1.DefaultConfiguration.exclude_merge_branches);
core.info(`Retrieved ${prCommits.length} PR merge commits for ${owner}/${repo}`);
const filteredPullRequests = [];
const pullRequestsByNumber = {};
for (const pr of pullRequests) {
@@ -706,11 +729,11 @@ class ReleaseNotes {
filteredPullRequests.push(pullRequest);
}
else {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`);
core.warning(`⚠️ ${prRef} not found! Commit text: ${commit.summary}`);
}
}
else {
core.info(`${prRef} not in date range, excluding from changelog`);
core.info(`${prRef} not in date range, excluding from changelog`);
}
}
return filteredPullRequests;
@@ -802,23 +825,49 @@ class Tags {
}
finally { if (e_1) throw e_1.error; }
}
core.info(`Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`);
core.info(`Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`);
return tagsInfo;
});
}
findPredecessorTag(owner, repo, tag, maxTagsToFetch) {
findPredecessorTag(owner, repo, tag, ignorePreReleases, maxTagsToFetch) {
return __awaiter(this, void 0, void 0, function* () {
const tags = this.sortTags(yield this.getTags(owner, repo, maxTagsToFetch));
const length = tags.length;
for (let i = 0; i < length; i++) {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
return tags[i + 1];
try {
const length = tags.length;
for (let i = 0; i < length; i++) {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
if (ignorePreReleases) {
core.info(`️ Enabled 'ignorePreReleases', searching for the closest release`);
for (let ii = i + 1; ii < length; ii++) {
if (!tags[ii].name.includes('-')) {
return tags[ii];
}
}
}
return tags[i + 1];
}
}
return tags[0];
}
catch (error) {
return null;
}
// not found, throw exception?
return tags[0];
});
}
/*
Sorts an array of tags as shown below:
2020.4.0
2020.4.0-rc02
2020.3.2
2020.3.1
2020.3.1-rc03
2020.3.1-rc02
2020.3.1-rc01
2020.3.1-b01
2020.3.1-a01
2020.3.0
*/
sortTags(commits) {
commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-');
@@ -843,22 +892,6 @@ class Tags {
}
}
exports.Tags = Tags;
/*
2020.3.2 ( should resolve 2020.3.1 )
2020.4.0
2020.4.0-rc02
2020.3.1
2020.3.1-rc03
2020.3.1-rc02
2020.3.1-rc01
2020.3.1-b01
2020.3.1-a01
2020.3.0
*/
/***/ }),
@@ -893,9 +926,12 @@ const pullRequests_1 = __webpack_require__(4217);
const core = __importStar(__webpack_require__(2186));
const configuration_1 = __webpack_require__(5527);
function buildChangelog(prs, config) {
var _a, _b, _c;
// sort to target order
prs = pullRequests_1.sortPullRequests(prs, (config.sort ? config.sort : configuration_1.DefaultConfiguration.sort).toUpperCase() ===
'ASC');
const sort = (_a = config.sort) !== null && _a !== void 0 ? _a : configuration_1.DefaultConfiguration.sort;
const sortAsc = sort.toUpperCase() === 'ASC';
prs = pullRequests_1.sortPullRequests(prs, sortAsc);
core.info(`️ Sorted all pull requests ascending: ${sort}`);
const validatedTransformers = validateTransfomers(config.transformers);
const transformedMap = new Map();
// convert PRs to their text representation
@@ -904,12 +940,13 @@ function buildChangelog(prs, config) {
? config.pr_template
: configuration_1.DefaultConfiguration.pr_template), validatedTransformers));
}
core.info(`️ Used ${validateTransfomers.length} transformers to adjust message`);
core.info(`✒️ Wrote messages for ${prs.length} pull requests`);
// bring PRs into the order of categories
const categorized = new Map();
if (config.categories) {
for (const category of config.categories) {
categorized.set(category, []);
}
const categories = (_b = config.categories) !== null && _b !== void 0 ? _b : configuration_1.DefaultConfiguration.categories;
for (const category of categories) {
categorized.set(category, []);
}
const uncategorized = [];
// bring elements in order
@@ -925,6 +962,7 @@ function buildChangelog(prs, config) {
uncategorized.push(body);
}
}
core.info(`️ Ordered all pull requests into ${categories.length} categories`);
// construct final changelog
let changelog = '';
for (const [category, pullRequests] of categorized) {
@@ -937,16 +975,17 @@ function buildChangelog(prs, config) {
changelog = `${changelog}\n`;
}
}
core.info(`✒️ Wrote ${categorized.size} categorized pull requests down`);
let changelogUncategorized = '';
for (const pr of uncategorized) {
changelogUncategorized = `${changelogUncategorized + pr}\n`;
}
core.info(`✒️ Wrote ${changelogUncategorized.length} non categorized pull requests down`);
// fill template
let transformedChangelog = config.template
? config.template
: configuration_1.DefaultConfiguration.template;
let transformedChangelog = (_c = config.template) !== null && _c !== void 0 ? _c : configuration_1.DefaultConfiguration.template;
transformedChangelog = transformedChangelog.replace('${{CHANGELOG}}', changelog);
transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED}}', changelogUncategorized);
core.info(`️ Filled template`);
return transformedChangelog;
}
exports.buildChangelog = buildChangelog;
@@ -954,13 +993,18 @@ function haveCommonElements(arr1, arr2) {
return arr1.some(item => arr2.includes(item));
}
function fillTemplate(pr, template) {
var _a, _b, _c, _d, _e, _f, _g;
let transformed = template;
transformed = transformed.replace('${{NUMBER}}', pr.number.toString());
transformed = transformed.replace('${{TITLE}}', pr.title);
transformed = transformed.replace('${{URL}}', pr.htmlURL);
transformed = transformed.replace('${{MERGED_AT}}', pr.mergedAt.toISOString());
transformed = transformed.replace('${{AUTHOR}}', pr.author);
transformed = transformed.replace('${{LABELS}}', (_b = (_a = pr.labels) === null || _a === void 0 ? void 0 : _a.join(', ')) !== null && _b !== void 0 ? _b : '');
transformed = transformed.replace('${{MILESTONE}}', (_c = pr.milestone) !== null && _c !== void 0 ? _c : '');
transformed = transformed.replace('${{BODY}}', pr.body);
transformed = transformed.replace('${{ASSIGNEES}}', (_e = (_d = pr.assignees) === null || _d === void 0 ? void 0 : _d.join(', ')) !== null && _e !== void 0 ? _e : '');
transformed = transformed.replace('${{REVIEWERS}}', (_g = (_f = pr.requestedReviewers) === null || _f === void 0 ? void 0 : _f.join(', ')) !== null && _g !== void 0 ? _g : '');
return transformed;
}
function transform(filled, transformers) {
@@ -974,9 +1018,7 @@ function transform(filled, transformers) {
return transformed;
}
function validateTransfomers(specifiedTransformers) {
const transformers = specifiedTransformers
? specifiedTransformers
: configuration_1.DefaultConfiguration.transformers;
const transformers = specifiedTransformers !== null && specifiedTransformers !== void 0 ? specifiedTransformers : configuration_1.DefaultConfiguration.transformers;
return transformers
.map(transformer => {
try {
@@ -986,7 +1028,7 @@ function validateTransfomers(specifiedTransformers) {
};
}
catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`);
core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`);
return {
pattern: null,
target: ''
@@ -1024,14 +1066,45 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.readConfiguration = void 0;
exports.directoryExistsSync = exports.readConfiguration = void 0;
const fs = __importStar(__webpack_require__(5747));
function readConfiguration(filename) {
const rawdata = fs.readFileSync(filename, 'utf8');
const configurationJSON = JSON.parse(rawdata);
return configurationJSON;
try {
const rawdata = fs.readFileSync(filename, 'utf8');
const configurationJSON = JSON.parse(rawdata);
return configurationJSON;
}
catch (error) {
return null;
}
}
exports.readConfiguration = readConfiguration;
function directoryExistsSync(path, required) {
if (!path) {
throw new Error("Arg 'path' must not be empty");
}
let stats;
try {
stats = fs.statSync(path);
}
catch (error) {
if (error.code === 'ENOENT') {
if (!required) {
return false;
}
throw new Error(`Directory '${path}' does not exist`);
}
throw new Error(`Encountered an error when checking whether path '${path}' exists: ${error.message}`);
}
if (stats.isDirectory()) {
return true;
}
else if (!required) {
return false;
}
throw new Error(`Directory '${path}' does not exist`);
}
exports.directoryExistsSync = directoryExistsSync;
/***/ }),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -55,7 +55,7 @@ export class Commits {
}
core.info(
`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`
`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`
)
return commits.map(commit => ({
sha: commit.sha,
+14 -1
View File
@@ -30,6 +30,19 @@ export const DefaultConfiguration: Configuration = {
template: '${{CHANGELOG}}', // the global template to host the changelog
pr_template: '- ${{TITLE}}\n - PR: #${{NUMBER}}', // the per PR template to pick
empty_template: '- no changes', // the template to use if no pull requests are found
categories: [], // the categories to support for the ordering
categories: [
{
title: '## 🚀 Features',
labels: ['feature']
},
{
title: '## 🐛 Fixes',
labels: ['fix']
},
{
title: '## 🧪 Tests',
labels: ['test']
}
], // the categories to support for the ordering
transformers: [] // transformers to apply on the PR description according to the `pr_template`
}
+1 -32
View File
@@ -1,6 +1,6 @@
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as io from '@actions/io'
import {directoryExistsSync} from './utils'
export async function createCommandManager(
workingDirectory: string
@@ -8,37 +8,6 @@ export async function createCommandManager(
return await GitCommandManager.createCommandManager(workingDirectory)
}
function directoryExistsSync(path: string, required?: boolean): boolean {
if (!path) {
throw new Error("Arg 'path' must not be empty")
}
let stats: fs.Stats
try {
stats = fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
)
}
if (stats.isDirectory()) {
return true
} else if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}
class GitCommandManager {
private gitPath = ''
private workingDirectory = ''
+23 -10
View File
@@ -1,11 +1,13 @@
import * as core from '@actions/core'
import {readConfiguration} from './utils'
import {ReleaseNotes} from './releaseNotes'
import {createCommandManager} from './git-helper'
import {createCommandManager} from './gitHelper'
import * as github from '@actions/github'
import * as path from 'path'
import {DefaultConfiguration} from './configuration'
async function run(): Promise<void> {
core.startGroup(`📘 Reading input values`)
try {
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
if (!githubWorkspacePath) {
@@ -19,12 +21,22 @@ async function run(): Promise<void> {
core.debug(`repositoryPath = '${repositoryPath}'`)
const configurationFile: string = core.getInput('configuration')
const configurationPath = path.resolve(
githubWorkspacePath,
configurationFile
)
core.debug(`configurationPath = '${configurationPath}'`)
const configuration = readConfiguration(configurationPath)
let configuration = DefaultConfiguration
if (configurationFile) {
const configurationPath = path.resolve(
githubWorkspacePath,
configurationFile
)
core.debug(`configurationPath = '${configurationPath}'`)
const providedConfiguration = readConfiguration(configurationPath)
if (!providedConfiguration) {
core.info(
`⚠️ Configuration provided, but it couldn't be found, or failed to parse. Fallback to Defaults`
)
} else {
configuration = providedConfiguration
}
}
const token = core.getInput('token')
let owner = core.getInput('owner')
@@ -64,25 +76,26 @@ async function run(): Promise<void> {
}
if (!owner) {
core.error(`Missing or couldn't resolve 'owner'`)
core.error(`💥 Missing or couldn't resolve 'owner'`)
return
} else {
core.debug(`Resolved 'owner' as ${owner}`)
}
if (!repo) {
core.error(`Missing or couldn't resolve 'owner'`)
core.error(`💥 Missing or couldn't resolve 'owner'`)
return
} else {
core.debug(`Resolved 'repo' as ${repo}`)
}
if (!toTag) {
core.error(`Missing or couldn't resolve 'toTag'`)
core.error(`💥 Missing or couldn't resolve 'toTag'`)
return
} else {
core.debug(`Resolved 'toTag' as ${toTag}`)
}
core.endGroup()
const releaseNotes = new ReleaseNotes({
owner,
+26 -5
View File
@@ -12,7 +12,10 @@ export interface PullRequestInfo {
author: string
repoName: string
labels: string[]
milestone: string
body: string
assignees: string[]
requestedReviewers: string[]
}
export class PullRequests {
@@ -40,10 +43,21 @@ export class PullRequests {
labels: pr.data.labels.map(function (label) {
return label.name
}),
body: pr.data.body
milestone: pr.data.milestone?.title,
body: pr.data.body,
assignees: pr.data.assignees?.map(function (asignee) {
return asignee.login
}),
requestedReviewers: pr.data.requested_reviewers?.map(function (
reviewer
) {
return reviewer.login
})
}
} catch (e) {
core.warning(`Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`)
core.warning(
`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`
)
return null
}
}
@@ -76,10 +90,17 @@ export class PullRequests {
mergedAt: moment(pr.merged_at),
author: pr.user.login,
repoName: pr.base.repo.full_name,
labels: pr.labels.map(function (label) {
labels: pr.labels?.map(function (label) {
return label.name
}),
body: pr.body
milestone: pr.milestone?.title,
body: pr.body,
assignees: pr.assignees?.map(function (asignee) {
return asignee.login
}),
requestedReviewers: pr.requested_reviewers?.map(function (reviewer) {
return reviewer.login
})
})
}
@@ -89,7 +110,7 @@ export class PullRequests {
mergedPRs.length >= maxPullRequests
) {
if (mergedPRs.length >= maxPullRequests) {
core.info(`Reached 'maxPullRequests' count ${maxPullRequests}`)
core.info(`⚠️ Reached 'maxPullRequests' count ${maxPullRequests}`)
}
// bail out early to not keep iterating on PRs super old
+38 -34
View File
@@ -1,5 +1,5 @@
import {Octokit} from '@octokit/rest'
import {Commits} from './commits'
import {Commits, CommitInfo} from './commits'
import {PullRequestInfo, PullRequests} from './pullRequests'
import {buildChangelog} from './transform'
import * as core from '@actions/core'
@@ -26,6 +26,7 @@ export class ReleaseNotes {
const {owner, repo, toTag, ignorePreReleases, configuration} = this.options
if (!this.options.fromTag) {
core.startGroup(`🔖 Resolve previous tag`)
core.debug(`fromTag undefined, trying to resolve via API`)
const tagsApi = new Tags(octokit)
@@ -34,45 +35,51 @@ export class ReleaseNotes {
repo,
toTag,
ignorePreReleases,
configuration.max_tags_to_fetch
? configuration.max_tags_to_fetch
: DefaultConfiguration.max_tags_to_fetch
configuration.max_tags_to_fetch ??
DefaultConfiguration.max_tags_to_fetch
)
if (previousTag == null) {
core.error(`Unable to retrieve previous tag given ${toTag}`)
return configuration.empty_template
? configuration.empty_template
: DefaultConfiguration.empty_template
core.error(`💥 Unable to retrieve previous tag given ${toTag}`)
return (
configuration.empty_template ?? DefaultConfiguration.empty_template
)
}
this.options.fromTag = previousTag.name
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
core.endGroup()
}
core.startGroup(`🚀 Load pull requests`)
const mergedPullRequests = await this.getMergedPullRequests(octokit)
core.endGroup()
if (mergedPullRequests.length === 0) {
core.warning(
`No pull requests found for between ${this.options.fromTag}...${toTag}`
)
return configuration.empty_template
? configuration.empty_template
: DefaultConfiguration.empty_template
core.warning(`⚠️ No pull requests found`)
return configuration.empty_template ?? DefaultConfiguration.empty_template
}
return buildChangelog(mergedPullRequests, configuration)
core.startGroup('📦 Build changelog')
const resultChangelog = buildChangelog(mergedPullRequests, configuration)
core.endGroup()
return resultChangelog
}
private async getMergedPullRequests(
octokit: Octokit
): Promise<PullRequestInfo[]> {
const {owner, repo, fromTag, toTag, configuration} = this.options
core.info(`Comparing ${owner}/${repo} - ${fromTag}...${toTag}`)
core.info(`Comparing ${owner}/${repo} - '${fromTag}...${toTag}'`)
const commitsApi = new Commits(octokit)
const commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
let commits: CommitInfo[]
try {
commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
} catch (error) {
core.error(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`)
return []
}
if (commits.length === 0) {
core.warning(`💥 No commits found between - ${fromTag}...${toTag}`)
return []
}
@@ -81,17 +88,17 @@ export class ReleaseNotes {
let fromDate = firstCommit.date
const toDate = lastCommit.date
const maxDays = configuration.max_back_track_time_days
? configuration.max_back_track_time_days
: DefaultConfiguration.max_back_track_time_days
const maxDays =
configuration.max_back_track_time_days ??
DefaultConfiguration.max_back_track_time_days
const maxFromDate = toDate.clone().subtract(maxDays, 'days')
if (maxFromDate.isAfter(fromDate)) {
core.info(`Adjusted 'fromDate' to go max ${maxDays} back`)
core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`)
fromDate = maxFromDate
}
core.info(
`Fetching PRs between dates ${fromDate.toISOString()} to ${toDate.toISOString()} for ${owner}/${repo}`
`Fetching PRs between dates ${fromDate.toISOString()} to ${toDate.toISOString()} for ${owner}/${repo}`
)
const pullRequestsApi = new PullRequests(octokit)
@@ -100,24 +107,21 @@ export class ReleaseNotes {
repo,
fromDate,
toDate,
configuration.max_pull_requests
? configuration.max_pull_requests
: DefaultConfiguration.max_pull_requests
configuration.max_pull_requests ?? DefaultConfiguration.max_pull_requests
)
core.info(
`Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`
`Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`
)
const prCommits = pullRequestsApi.filterCommits(
commits,
configuration.exclude_merge_branches
? configuration.exclude_merge_branches
: DefaultConfiguration.exclude_merge_branches
configuration.exclude_merge_branches ??
DefaultConfiguration.exclude_merge_branches
)
core.info(
`Retrieved ${prCommits.length} PR merge commits for ${owner}/${repo}`
`Retrieved ${prCommits.length} PR merge commits for ${owner}/${repo}`
)
const filteredPullRequests = []
@@ -146,10 +150,10 @@ export class ReleaseNotes {
if (pullRequest) {
filteredPullRequests.push(pullRequest)
} else {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`)
core.warning(`⚠️ ${prRef} not found! Commit text: ${commit.summary}`)
}
} else {
core.info(`${prRef} not in date range, excluding from changelog`)
core.info(`${prRef} not in date range, excluding from changelog`)
}
}
+16 -19
View File
@@ -40,7 +40,7 @@ export class Tags {
}
core.info(
`Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`
`Found ${tagsInfo.length} (fetching max: ${maxTagsToFetch}) tags from the GitHub API for ${owner}/${repo}`
)
return tagsInfo
}
@@ -60,7 +60,7 @@ export class Tags {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
if (ignorePreReleases) {
core.info(
`Enabled 'ignorePreReleases', searching for the closest release`
`Enabled 'ignorePreReleases', searching for the closest release`
)
for (let ii = i + 1; ii < length; ii++) {
if (!tags[ii].name.includes('-')) {
@@ -77,6 +77,20 @@ export class Tags {
}
}
/*
Sorts an array of tags as shown below:
2020.4.0
2020.4.0-rc02
2020.3.2
2020.3.1
2020.3.1-rc03
2020.3.1-rc02
2020.3.1-rc01
2020.3.1-b01
2020.3.1-a01
2020.3.0
*/
private sortTags(commits: TagInfo[]): TagInfo[] {
commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-')
@@ -97,20 +111,3 @@ export class Tags {
return commits
}
}
/*
2020.3.2 ( should resolve 2020.3.1 )
2020.4.0
2020.4.0-rc02
2020.3.1
2020.3.1-rc03
2020.3.1-rc02
2020.3.1-rc01
2020.3.1-b01
2020.3.1-a01
2020.3.0
*/
+31 -17
View File
@@ -12,11 +12,10 @@ export function buildChangelog(
config: Configuration
): string {
// sort to target order
prs = sortPullRequests(
prs,
(config.sort ? config.sort : DefaultConfiguration.sort).toUpperCase() ===
'ASC'
)
const sort = config.sort ?? DefaultConfiguration.sort
const sortAsc = sort.toUpperCase() === 'ASC'
prs = sortPullRequests(prs, sortAsc)
core.info(`️ Sorted all pull requests ascending: ${sort}`)
const validatedTransformers = validateTransfomers(config.transformers)
const transformedMap = new Map<PullRequestInfo, string>()
@@ -35,13 +34,16 @@ export function buildChangelog(
)
)
}
core.info(
`️ Used ${validateTransfomers.length} transformers to adjust message`
)
core.info(`✒️ Wrote messages for ${prs.length} pull requests`)
// bring PRs into the order of categories
const categorized = new Map<Category, string[]>()
if (config.categories) {
for (const category of config.categories) {
categorized.set(category, [])
}
const categories = config.categories ?? DefaultConfiguration.categories
for (const category of categories) {
categorized.set(category, [])
}
const uncategorized: string[] = []
@@ -60,6 +62,7 @@ export function buildChangelog(
uncategorized.push(body)
}
}
core.info(`️ Ordered all pull requests into ${categories.length} categories`)
// construct final changelog
let changelog = ''
@@ -75,16 +78,18 @@ export function buildChangelog(
changelog = `${changelog}\n`
}
}
core.info(`✒️ Wrote ${categorized.size} categorized pull requests down`)
let changelogUncategorized = ''
for (const pr of uncategorized) {
changelogUncategorized = `${changelogUncategorized + pr}\n`
}
core.info(
`✒️ Wrote ${changelogUncategorized.length} non categorized pull requests down`
)
// fill template
let transformedChangelog = config.template
? config.template
: DefaultConfiguration.template
let transformedChangelog = config.template ?? DefaultConfiguration.template
transformedChangelog = transformedChangelog.replace(
'${{CHANGELOG}}',
changelog
@@ -93,6 +98,7 @@ export function buildChangelog(
'${{UNCATEGORIZED}}',
changelogUncategorized
)
core.info(`️ Filled template`)
return transformedChangelog
}
@@ -107,7 +113,17 @@ function fillTemplate(pr: PullRequestInfo, template: string): string {
transformed = transformed.replace('${{URL}}', pr.htmlURL)
transformed = transformed.replace('${{MERGED_AT}}', pr.mergedAt.toISOString())
transformed = transformed.replace('${{AUTHOR}}', pr.author)
transformed = transformed.replace('${{LABELS}}', pr.labels?.join(', ') ?? '')
transformed = transformed.replace('${{MILESTONE}}', pr.milestone ?? '')
transformed = transformed.replace('${{BODY}}', pr.body)
transformed = transformed.replace(
'${{ASSIGNEES}}',
pr.assignees?.join(', ') ?? ''
)
transformed = transformed.replace(
'${{REVIEWERS}}',
pr.requestedReviewers?.join(', ') ?? ''
)
return transformed
}
@@ -125,10 +141,8 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
function validateTransfomers(
specifiedTransformers: Transformer[]
): RegexTransformer[] {
const transformers = specifiedTransformers
? specifiedTransformers
: DefaultConfiguration.transformers
const transformers =
specifiedTransformers ?? DefaultConfiguration.transformers
return transformers
.map(transformer => {
try {
@@ -137,7 +151,7 @@ function validateTransfomers(
target: transformer.target
}
} catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`)
core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`)
return {
pattern: null,
target: ''
+39 -4
View File
@@ -1,8 +1,43 @@
import * as fs from 'fs'
import {Configuration} from './configuration'
export function readConfiguration(filename: string): Configuration {
const rawdata = fs.readFileSync(filename, 'utf8')
const configurationJSON: Configuration = JSON.parse(rawdata)
return configurationJSON
export function readConfiguration(filename: string): Configuration | null {
try {
const rawdata = fs.readFileSync(filename, 'utf8')
const configurationJSON: Configuration = JSON.parse(rawdata)
return configurationJSON
} catch (error) {
return null
}
}
export function directoryExistsSync(path: string, required?: boolean): boolean {
if (!path) {
throw new Error("Arg 'path' must not be empty")
}
let stats: fs.Stats
try {
stats = fs.statSync(path)
} catch (error) {
if (error.code === 'ENOENT') {
if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}
throw new Error(
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
)
}
if (stats.isDirectory()) {
return true
} else if (!required) {
return false
}
throw new Error(`Directory '${path}' does not exist`)
}