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' if: github.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - uses: actions/checkout@v2
with: with:
fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified
@@ -32,31 +44,27 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Minimal - name: Echo Minimal Configuration Changelog
run: echo "${{steps.minimal_release.outputs.changelog}}" run: echo "${{steps.minimal_release.outputs.changelog}}"
- name: "Complex Configuration" - name: "Complex Configuration"
id: complex_release id: complex_release
uses: ./ uses: ./
with: with:
configuration: "configuration_complex.json" configuration: "configs/configuration_complex.json"
owner: "mikepenz" owner: "mikepenz"
repo: "release-changelog-builder-action" repo: "release-changelog-builder-action"
fromTag: "v0.0.1" fromTag: "v0.0.1"
toTag: "v0.0.3" toTag: "v0.0.3"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo Complex - name: Echo Complex Configuration Changelog
run: echo "${{steps.complex_release.outputs.changelog}}" run: echo "${{steps.complex_release.outputs.changelog}}"
release: release:
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified
- name: Prepare Tag - name: Prepare Tag
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
id: tag_version id: tag_version
@@ -66,6 +74,7 @@ jobs:
id: github_release id: github_release
uses: mikepenz/release-changelog-builder-action@main uses: mikepenz/release-changelog-builder-action@main
with: with:
configuration: "configs/configuration_repo.json"
toTag: ${{ steps.tag_version.outputs.VERSION }} toTag: ${{ steps.tag_version.outputs.VERSION }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 ```yml
- name: "Build Changelog" - 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 }} 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 ```yml
# ${{steps.{CHANGELOG_STEP_ID}.outputs.changelog}}
${{steps.build_changelog.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": [ "categories": [
{ {
"title": "## 🚀 Features", "title": "## 🚀 Features",
"labels": ["feature"] "labels": ["feature"]
}, },
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{ {
"title": "## 🐛 Fixes", "title": "## 🐛 Fixes",
"labels": ["fix"] "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) 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 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" configuration: "configuration_complex.json"
owner: "mikepenz" owner: "mikepenz"
repo: "release-changelog-builder-action" 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" fromTag: "0.0.2"
toTag: "0.0.3" 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 | ### PR Template placeholders
| --------- | -------------------------- |
| `${{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 |
## Template placeholdrs Table of supported placeholders allowed to be used in the `template` configuration.
| Variable | Description | | **Placeholder** | **Description** |
| --------- | -------------------------- | |------------------|-------------------------------------------------------------|
| `${{CHANGELOG}}` | The contents of the main changelog, matching the labels as specified in the categories configuration | | `${{NUMBER}}` | The number referencing this pull request. E.g. 13 |
| `${{UNCATEGORIZED}}` | All pull requests not matching a label | | `${{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 ```bash
# Install the dependencies # Install the dependencies
@@ -127,29 +229,28 @@ $ npm test
$ npm run lint -- --fix $ 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 ```bash
export GITHUB_TOKEN=your_personal_github_pat export GITHUB_TOKEN=your_personal_github_pat
``` ```
## Developed By
# Developed By
* Mike Penz * Mike Penz
* [mikepenz.com](http://mikepenz.com) - <mikepenz@gmail.com> * [mikepenz.com](http://mikepenz.com) - <mikepenz@gmail.com>
* [paypal.me/mikepenz](http://paypal.me/mikepenz) * [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/) - 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. 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. 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 {ReleaseNotes} from '../src/releaseNotes'
import {readConfiguration} from '../src/utils' 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 // 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()) 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 () => { it('Should match generated changelog (tags)', async () => {
jest.setTimeout(180000) jest.setTimeout(180000)
const configuration = readConfiguration('configuration.json') const configuration = readConfiguration('configs/configuration.json')!!
const releaseNotes = new ReleaseNotes({ const releaseNotes = new ReleaseNotes({
owner: 'mikepenz', owner: 'mikepenz',
repo: 'release-changelog-builder-action', repo: 'release-changelog-builder-action',
@@ -48,7 +58,7 @@ it('Should match generated changelog (tags)', async () => {
it('Should match generated changelog (unspecified fromTag)', async () => { it('Should match generated changelog (unspecified fromTag)', async () => {
jest.setTimeout(180000) jest.setTimeout(180000)
const configuration = readConfiguration('configuration.json') const configuration = readConfiguration('configs/configuration.json')!!
const releaseNotes = new ReleaseNotes({ const releaseNotes = new ReleaseNotes({
owner: 'mikepenz', owner: 'mikepenz',
repo: 'release-changelog-builder-action', repo: 'release-changelog-builder-action',
@@ -71,7 +81,7 @@ it('Should match generated changelog (unspecified fromTag)', async () => {
it('Should match generated changelog (refs)', async () => { it('Should match generated changelog (refs)', async () => {
jest.setTimeout(180000) jest.setTimeout(180000)
const configuration = readConfiguration('configuration.json') const configuration = readConfiguration('configs/configuration_all_placeholders.json')!!
const releaseNotes = new ReleaseNotes({ const releaseNotes = new ReleaseNotes({
owner: 'mikepenz', owner: 'mikepenz',
repo: 'release-changelog-builder-action', repo: 'release-changelog-builder-action',
@@ -85,8 +95,16 @@ it('Should match generated changelog (refs)', async () => {
console.log(changeLog) console.log(changeLog)
expect(changeLog).toStrictEqual(`## 🧪 Tests expect(changeLog).toStrictEqual(`## 🧪 Tests
- [CI] Specify Test Case [CI] Specify Test Case
- PR: #10 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' 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' author: 'Mike Penz'
branding: branding:
icon: 'award' icon: 'award'
color: 'green' color: 'green'
inputs: inputs:
configuration: configuration:
required: true description: 'Defines the relative 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: 'Defines the directory the repo is located in (checkout directory)'
owner: 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: repo:
description: 'the repository to create the changelog for' description: 'Defines the repository to create the changelog for'
fromTag: fromTag:
description: 'the previous tag to compare against' description: 'Defines the previous tag to compare against'
toTag: toTag:
description: 'the new tag created' description: 'Defines the newly tag created'
ignorePreReleases: 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" default: "false"
token: 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: outputs:
changelog: changelog:
description: The generated changelog as markdown. description: Returns the generated changelog as markdown.
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'
@@ -4,10 +4,6 @@
"title": "## 🚀 Features", "title": "## 🚀 Features",
"labels": ["feature"] "labels": ["feature"]
}, },
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{ {
"title": "## 🐛 Fixes", "title": "## 🐛 Fixes",
"labels": ["fix"] "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", "title": "## 🚀 Features",
"labels": ["feature"] "labels": ["feature"]
}, },
{
"title": "## 🦄 Internal Features",
"labels": ["internal"]
},
{ {
"title": "## 🐛 Fixes", "title": "## 🐛 Fixes",
"labels": ["fix"] "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
+179 -106
View File
@@ -72,7 +72,7 @@ class Commits {
commits = compareResult.data.commits.concat(commits); commits = compareResult.data.commits.concat(commits);
compareHead = `${commits[0].sha}^`; compareHead = `${commits[0].sha}^`;
} }
core.info(`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`); core.info(`Found ${commits.length} commits from the GitHub API for ${owner}/${repo}`);
return commits.map(commit => ({ return commits.map(commit => ({
sha: commit.sha, sha: commit.sha,
summary: commit.commit.message.split('\n')[0], summary: commit.commit.message.split('\n')[0],
@@ -126,14 +126,27 @@ exports.DefaultConfiguration = {
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: [
{
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` transformers: [] // transformers to apply on the PR description according to the `pr_template`
}; };
/***/ }), /***/ }),
/***/ 9621: /***/ 353:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) { /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -169,39 +182,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createCommandManager = void 0; exports.createCommandManager = void 0;
const exec = __importStar(__webpack_require__(1514)); const exec = __importStar(__webpack_require__(1514));
const fs = __importStar(__webpack_require__(5747));
const io = __importStar(__webpack_require__(7436)); const io = __importStar(__webpack_require__(7436));
const utils_1 = __webpack_require__(918);
function createCommandManager(workingDirectory) { function createCommandManager(workingDirectory) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
return yield GitCommandManager.createCommandManager(workingDirectory); return yield GitCommandManager.createCommandManager(workingDirectory);
}); });
} }
exports.createCommandManager = createCommandManager; 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 { class GitCommandManager {
// Private constructor; use createCommandManager() // Private constructor; use createCommandManager()
constructor() { constructor() {
@@ -237,7 +225,7 @@ class GitCommandManager {
} }
execGit(args, allowAllExitCodes = false, silent = false) { execGit(args, allowAllExitCodes = false, silent = false) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
directoryExistsSync(this.workingDirectory, true); utils_1.directoryExistsSync(this.workingDirectory, true);
const result = new GitOutput(); const result = new GitOutput();
const stdout = []; const stdout = [];
const options = { const options = {
@@ -309,11 +297,13 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__webpack_require__(2186)); const core = __importStar(__webpack_require__(2186));
const utils_1 = __webpack_require__(918); const utils_1 = __webpack_require__(918);
const releaseNotes_1 = __webpack_require__(5882); 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 github = __importStar(__webpack_require__(5438));
const path = __importStar(__webpack_require__(5622)); const path = __importStar(__webpack_require__(5622));
const configuration_1 = __webpack_require__(5527);
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
core.startGroup(`📘 Reading input values`);
try { try {
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']; let githubWorkspacePath = process.env['GITHUB_WORKSPACE'];
if (!githubWorkspacePath) { if (!githubWorkspacePath) {
@@ -325,17 +315,27 @@ function run() {
repositoryPath = path.resolve(githubWorkspacePath, repositoryPath); repositoryPath = path.resolve(githubWorkspacePath, repositoryPath);
core.debug(`repositoryPath = '${repositoryPath}'`); core.debug(`repositoryPath = '${repositoryPath}'`);
const configurationFile = core.getInput('configuration'); const configurationFile = core.getInput('configuration');
let configuration = configuration_1.DefaultConfiguration;
if (configurationFile) {
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 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'); 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');
const fromTag = core.getInput('fromTag'); const fromTag = core.getInput('fromTag');
let toTag = core.getInput('toTag'); let toTag = core.getInput('toTag');
const ignorePreReleases = core.getInput('ignorePreReleases');
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 gitHelper_1.createCommandManager(repositoryPath);
const latestTag = yield gitHelper.latestTag(); const latestTag = yield gitHelper.latestTag();
toTag = latestTag; toTag = latestTag;
core.debug(`toTag = '${latestTag}'`); core.debug(`toTag = '${latestTag}'`);
@@ -355,31 +355,33 @@ function run() {
repo = splitRepository[1]; repo = splitRepository[1];
} }
if (!owner) { if (!owner) {
core.error(`Missing or couldn't resolve 'owner'`); core.error(`💥 Missing or couldn't resolve 'owner'`);
return; return;
} }
else { else {
core.debug(`Resolved 'owner' as ${owner}`); 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 { else {
core.debug(`Resolved 'repo' as ${repo}`); 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 { else {
core.debug(`Resolved 'toTag' as ${toTag}`); core.debug(`Resolved 'toTag' as ${toTag}`);
} }
core.endGroup();
const releaseNotes = new releaseNotes_1.ReleaseNotes({ const releaseNotes = new releaseNotes_1.ReleaseNotes({
owner, owner,
repo, repo,
fromTag, fromTag,
toTag, toTag,
ignorePreReleases: ignorePreReleases === 'true',
configuration configuration
}); });
core.setOutput('changelog', yield releaseNotes.pull(token)); core.setOutput('changelog', yield releaseNotes.pull(token));
@@ -446,6 +448,7 @@ class PullRequests {
this.octokit = octokit; this.octokit = octokit;
} }
getSingle(owner, repo, prNumber) { getSingle(owner, repo, prNumber) {
var _a, _b, _c;
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({ const pr = yield this.octokit.pulls.get({
@@ -463,17 +466,25 @@ class PullRequests {
labels: pr.data.labels.map(function (label) { labels: pr.data.labels.map(function (label) {
return label.name; 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) { catch (e) {
core.warning(`Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`); core.warning(`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`);
return null; return null;
} }
}); });
} }
getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests) { getBetweenDates(owner, repo, fromDate, toDate, maxPullRequests) {
var e_1, _a; var e_1, _a;
var _b, _c, _d, _e;
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({
@@ -484,8 +495,8 @@ class PullRequests {
direction: 'desc' direction: 'desc'
}); });
try { try {
for (var _b = __asyncValues(this.octokit.paginate.iterator(options)), _c; _c = yield _b.next(), !_c.done;) { for (var _f = __asyncValues(this.octokit.paginate.iterator(options)), _g; _g = yield _f.next(), !_g.done;) {
const response = _c.value; const response = _g.value;
const prs = response.data; const prs = response.data;
for (const pr of prs.filter(p => !!p.merged_at)) { for (const pr of prs.filter(p => !!p.merged_at)) {
mergedPRs.push({ mergedPRs.push({
@@ -495,17 +506,24 @@ 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) { labels: (_b = pr.labels) === null || _b === void 0 ? void 0 : _b.map(function (label) {
return label.name; 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]; const firstPR = prs[0];
if ((firstPR.merged_at && fromDate.isAfter(moment_1.default(firstPR.merged_at))) || if ((firstPR.merged_at && fromDate.isAfter(moment_1.default(firstPR.merged_at))) ||
mergedPRs.length >= maxPullRequests) { mergedPRs.length >= maxPullRequests) {
if (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 // bail out early to not keep iterating on PRs super old
return sortPullRequests(mergedPRs, true); return sortPullRequests(mergedPRs, true);
@@ -515,7 +533,7 @@ class PullRequests {
catch (e_1_1) { e_1 = { error: e_1_1 }; } catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally { finally {
try { 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; } finally { if (e_1) throw e_1.error; }
} }
@@ -626,67 +644,72 @@ class ReleaseNotes {
this.options = options; this.options = options;
} }
pull(token) { pull(token) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
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, toTag, configuration } = this.options; const { owner, repo, toTag, ignorePreReleases, configuration } = this.options;
if (!this.options.fromTag) { if (!this.options.fromTag) {
core.startGroup(`🔖 Resolve previous tag`);
core.debug(`fromTag undefined, trying to resolve via API`); 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, configuration.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);
? configuration.max_tags_to_fetch
: configuration_1.DefaultConfiguration.max_tags_to_fetch);
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 return ((_b = configuration.empty_template) !== null && _b !== void 0 ? _b : configuration_1.DefaultConfiguration.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}`); core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`);
core.endGroup();
} }
core.startGroup(`🚀 Load pull requests`);
const mergedPullRequests = yield this.getMergedPullRequests(octokit); const mergedPullRequests = yield this.getMergedPullRequests(octokit);
core.endGroup();
if (mergedPullRequests.length === 0) { if (mergedPullRequests.length === 0) {
core.warning(`No pull requests found for between ${this.options.fromTag}...${toTag}`); core.warning(`⚠️ No pull requests found`);
return configuration.empty_template return (_c = configuration.empty_template) !== null && _c !== void 0 ? _c : configuration_1.DefaultConfiguration.empty_template;
? configuration.empty_template
: 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) { getMergedPullRequests(octokit) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const { owner, repo, fromTag, toTag, configuration } = this.options; 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 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) { if (commits.length === 0) {
core.warning(`💥 No commits found between - ${fromTag}...${toTag}`);
return []; return [];
} }
const firstCommit = commits[0]; const firstCommit = commits[0];
const lastCommit = commits[commits.length - 1]; const lastCommit = commits[commits.length - 1];
let fromDate = firstCommit.date; let fromDate = firstCommit.date;
const toDate = lastCommit.date; const toDate = lastCommit.date;
const maxDays = configuration.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;
? configuration.max_back_track_time_days
: configuration_1.DefaultConfiguration.max_back_track_time_days;
const maxFromDate = toDate.clone().subtract(maxDays, 'days'); const maxFromDate = toDate.clone().subtract(maxDays, 'days');
if (maxFromDate.isAfter(fromDate)) { if (maxFromDate.isAfter(fromDate)) {
core.info(`Adjusted 'fromDate' to go max ${maxDays} back`); core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`);
fromDate = maxFromDate; 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 pullRequestsApi = new pullRequests_1.PullRequests(octokit);
const pullRequests = yield pullRequestsApi.getBetweenDates(owner, repo, fromDate, toDate, configuration.max_pull_requests 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);
? configuration.max_pull_requests core.info(`️ Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`);
: configuration_1.DefaultConfiguration.max_pull_requests); const prCommits = pullRequestsApi.filterCommits(commits, (_c = configuration.exclude_merge_branches) !== null && _c !== void 0 ? _c : configuration_1.DefaultConfiguration.exclude_merge_branches);
core.info(`Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`); core.info(`Retrieved ${prCommits.length} PR merge commits 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 filteredPullRequests = []; const filteredPullRequests = [];
const pullRequestsByNumber = {}; const pullRequestsByNumber = {};
for (const pr of pullRequests) { for (const pr of pullRequests) {
@@ -706,11 +729,11 @@ class ReleaseNotes {
filteredPullRequests.push(pullRequest); filteredPullRequests.push(pullRequest);
} }
else { else {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`); core.warning(`⚠️ ${prRef} not found! Commit text: ${commit.summary}`);
} }
} }
else { else {
core.info(`${prRef} not in date range, excluding from changelog`); core.info(`${prRef} not in date range, excluding from changelog`);
} }
} }
return filteredPullRequests; return filteredPullRequests;
@@ -802,23 +825,49 @@ class Tags {
} }
finally { if (e_1) throw e_1.error; } 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; return tagsInfo;
}); });
} }
findPredecessorTag(owner, repo, tag, maxTagsToFetch) { findPredecessorTag(owner, repo, tag, ignorePreReleases, maxTagsToFetch) {
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, maxTagsToFetch)); const tags = this.sortTags(yield this.getTags(owner, repo, maxTagsToFetch));
try {
const length = tags.length; const length = tags.length;
for (let 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()) {
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[i + 1];
} }
} }
// not found, throw exception?
return tags[0]; return tags[0];
}
catch (error) {
return null;
}
}); });
} }
/*
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) { sortTags(commits) {
commits.sort((b, a) => { commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-'); const partsA = a.name.replace(/^v/, '').split('-');
@@ -843,22 +892,6 @@ class Tags {
} }
} }
exports.Tags = 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 core = __importStar(__webpack_require__(2186));
const configuration_1 = __webpack_require__(5527); const configuration_1 = __webpack_require__(5527);
function buildChangelog(prs, config) { function buildChangelog(prs, config) {
var _a, _b, _c;
// sort to target order // sort to target order
prs = pullRequests_1.sortPullRequests(prs, (config.sort ? config.sort : configuration_1.DefaultConfiguration.sort).toUpperCase() === const sort = (_a = config.sort) !== null && _a !== void 0 ? _a : configuration_1.DefaultConfiguration.sort;
'ASC'); 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 validatedTransformers = validateTransfomers(config.transformers);
const transformedMap = new Map(); const transformedMap = new Map();
// convert PRs to their text representation // convert PRs to their text representation
@@ -904,13 +940,14 @@ function buildChangelog(prs, config) {
? config.pr_template ? config.pr_template
: configuration_1.DefaultConfiguration.pr_template), validatedTransformers)); : 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 // bring PRs into the order of categories
const categorized = new Map(); const categorized = new Map();
if (config.categories) { const categories = (_b = config.categories) !== null && _b !== void 0 ? _b : configuration_1.DefaultConfiguration.categories;
for (const category of config.categories) { for (const category of categories) {
categorized.set(category, []); categorized.set(category, []);
} }
}
const uncategorized = []; const uncategorized = [];
// bring elements in order // bring elements in order
for (const [pr, body] of transformedMap) { for (const [pr, body] of transformedMap) {
@@ -925,6 +962,7 @@ function buildChangelog(prs, config) {
uncategorized.push(body); uncategorized.push(body);
} }
} }
core.info(`️ Ordered all pull requests into ${categories.length} categories`);
// construct final changelog // construct final changelog
let changelog = ''; let changelog = '';
for (const [category, pullRequests] of categorized) { for (const [category, pullRequests] of categorized) {
@@ -937,16 +975,17 @@ function buildChangelog(prs, config) {
changelog = `${changelog}\n`; changelog = `${changelog}\n`;
} }
} }
core.info(`✒️ Wrote ${categorized.size} categorized pull requests down`);
let changelogUncategorized = ''; let changelogUncategorized = '';
for (const pr of uncategorized) { for (const pr of uncategorized) {
changelogUncategorized = `${changelogUncategorized + pr}\n`; changelogUncategorized = `${changelogUncategorized + pr}\n`;
} }
core.info(`✒️ Wrote ${changelogUncategorized.length} non categorized pull requests down`);
// fill template // fill template
let transformedChangelog = config.template let transformedChangelog = (_c = config.template) !== null && _c !== void 0 ? _c : configuration_1.DefaultConfiguration.template;
? config.template
: configuration_1.DefaultConfiguration.template;
transformedChangelog = transformedChangelog.replace('${{CHANGELOG}}', changelog); transformedChangelog = transformedChangelog.replace('${{CHANGELOG}}', changelog);
transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED}}', changelogUncategorized); transformedChangelog = transformedChangelog.replace('${{UNCATEGORIZED}}', changelogUncategorized);
core.info(`️ Filled template`);
return transformedChangelog; return transformedChangelog;
} }
exports.buildChangelog = buildChangelog; exports.buildChangelog = buildChangelog;
@@ -954,13 +993,18 @@ 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) {
var _a, _b, _c, _d, _e, _f, _g;
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.toISOString()); transformed = transformed.replace('${{MERGED_AT}}', pr.mergedAt.toISOString());
transformed = transformed.replace('${{AUTHOR}}', pr.author); 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('${{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; return transformed;
} }
function transform(filled, transformers) { function transform(filled, transformers) {
@@ -974,9 +1018,7 @@ function transform(filled, transformers) {
return transformed; return transformed;
} }
function validateTransfomers(specifiedTransformers) { function validateTransfomers(specifiedTransformers) {
const transformers = specifiedTransformers const transformers = specifiedTransformers !== null && specifiedTransformers !== void 0 ? specifiedTransformers : configuration_1.DefaultConfiguration.transformers;
? specifiedTransformers
: configuration_1.DefaultConfiguration.transformers;
return transformers return transformers
.map(transformer => { .map(transformer => {
try { try {
@@ -986,7 +1028,7 @@ function validateTransfomers(specifiedTransformers) {
}; };
} }
catch (e) { catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`); core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`);
return { return {
pattern: null, pattern: null,
target: '' target: ''
@@ -1024,14 +1066,45 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.readConfiguration = void 0; exports.directoryExistsSync = exports.readConfiguration = void 0;
const fs = __importStar(__webpack_require__(5747)); const fs = __importStar(__webpack_require__(5747));
function readConfiguration(filename) { function readConfiguration(filename) {
try {
const rawdata = fs.readFileSync(filename, 'utf8'); const rawdata = fs.readFileSync(filename, 'utf8');
const configurationJSON = JSON.parse(rawdata); const configurationJSON = JSON.parse(rawdata);
return configurationJSON; return configurationJSON;
}
catch (error) {
return null;
}
} }
exports.readConfiguration = readConfiguration; 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( 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 => ({ return commits.map(commit => ({
sha: commit.sha, sha: commit.sha,
+14 -1
View File
@@ -30,6 +30,19 @@ export const DefaultConfiguration: Configuration = {
template: '${{CHANGELOG}}', // the global template to host the changelog template: '${{CHANGELOG}}', // the global template to host the changelog
pr_template: '- ${{TITLE}}\n - PR: #${{NUMBER}}', // the per PR template to pick 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 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` 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 exec from '@actions/exec'
import * as fs from 'fs'
import * as io from '@actions/io' import * as io from '@actions/io'
import {directoryExistsSync} from './utils'
export async function createCommandManager( export async function createCommandManager(
workingDirectory: string workingDirectory: string
@@ -8,37 +8,6 @@ export async function createCommandManager(
return await GitCommandManager.createCommandManager(workingDirectory) return await GitCommandManager.createCommandManager(workingDirectory)
} }
function directoryExistsSync(path: string, required?: boolean): boolean {
if (!path) {
throw new Error("Arg 'path' must not be empty")
}
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 { class GitCommandManager {
private gitPath = '' private gitPath = ''
private workingDirectory = '' private workingDirectory = ''
+18 -5
View File
@@ -1,11 +1,13 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {readConfiguration} from './utils' import {readConfiguration} from './utils'
import {ReleaseNotes} from './releaseNotes' import {ReleaseNotes} from './releaseNotes'
import {createCommandManager} from './git-helper' import {createCommandManager} from './gitHelper'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as path from 'path' import * as path from 'path'
import {DefaultConfiguration} from './configuration'
async function run(): Promise<void> { async function run(): Promise<void> {
core.startGroup(`📘 Reading input values`)
try { try {
let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
if (!githubWorkspacePath) { if (!githubWorkspacePath) {
@@ -19,12 +21,22 @@ async function run(): Promise<void> {
core.debug(`repositoryPath = '${repositoryPath}'`) core.debug(`repositoryPath = '${repositoryPath}'`)
const configurationFile: string = core.getInput('configuration') const configurationFile: string = core.getInput('configuration')
let configuration = DefaultConfiguration
if (configurationFile) {
const configurationPath = path.resolve( const configurationPath = path.resolve(
githubWorkspacePath, githubWorkspacePath,
configurationFile configurationFile
) )
core.debug(`configurationPath = '${configurationPath}'`) core.debug(`configurationPath = '${configurationPath}'`)
const configuration = readConfiguration(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') const token = core.getInput('token')
let owner = core.getInput('owner') let owner = core.getInput('owner')
@@ -64,25 +76,26 @@ 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 { } else {
core.debug(`Resolved 'owner' as ${owner}`) 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 { } else {
core.debug(`Resolved 'repo' as ${repo}`) 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 { } else {
core.debug(`Resolved 'toTag' as ${toTag}`) core.debug(`Resolved 'toTag' as ${toTag}`)
} }
core.endGroup()
const releaseNotes = new ReleaseNotes({ const releaseNotes = new ReleaseNotes({
owner, owner,
+26 -5
View File
@@ -12,7 +12,10 @@ export interface PullRequestInfo {
author: string author: string
repoName: string repoName: string
labels: string[] labels: string[]
milestone: string
body: string body: string
assignees: string[]
requestedReviewers: string[]
} }
export class PullRequests { export class PullRequests {
@@ -40,10 +43,21 @@ export class PullRequests {
labels: pr.data.labels.map(function (label) { labels: pr.data.labels.map(function (label) {
return label.name 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) { } catch (e) {
core.warning(`Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`) core.warning(
`⚠️ Cannot find PR ${owner}/${repo}#${prNumber} - ${e.message}`
)
return null return null
} }
} }
@@ -76,10 +90,17 @@ export class PullRequests {
mergedAt: moment(pr.merged_at), mergedAt: moment(pr.merged_at),
author: pr.user.login, author: pr.user.login,
repoName: pr.base.repo.full_name, repoName: pr.base.repo.full_name,
labels: pr.labels.map(function (label) { labels: pr.labels?.map(function (label) {
return label.name 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 mergedPRs.length >= maxPullRequests
) { ) {
if (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 // 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 {Octokit} from '@octokit/rest'
import {Commits} from './commits' import {Commits, CommitInfo} 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'
@@ -26,6 +26,7 @@ export class ReleaseNotes {
const {owner, repo, toTag, ignorePreReleases, configuration} = this.options const {owner, repo, toTag, ignorePreReleases, configuration} = this.options
if (!this.options.fromTag) { if (!this.options.fromTag) {
core.startGroup(`🔖 Resolve previous tag`)
core.debug(`fromTag undefined, trying to resolve via API`) core.debug(`fromTag undefined, trying to resolve via API`)
const tagsApi = new Tags(octokit) const tagsApi = new Tags(octokit)
@@ -34,45 +35,51 @@ export class ReleaseNotes {
repo, repo,
toTag, toTag,
ignorePreReleases, ignorePreReleases,
configuration.max_tags_to_fetch configuration.max_tags_to_fetch ??
? configuration.max_tags_to_fetch DefaultConfiguration.max_tags_to_fetch
: DefaultConfiguration.max_tags_to_fetch
) )
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 return (
? configuration.empty_template configuration.empty_template ?? DefaultConfiguration.empty_template
: DefaultConfiguration.empty_template )
} }
this.options.fromTag = previousTag.name this.options.fromTag = previousTag.name
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`) core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
core.endGroup()
} }
core.startGroup(`🚀 Load pull requests`)
const mergedPullRequests = await this.getMergedPullRequests(octokit) const mergedPullRequests = await this.getMergedPullRequests(octokit)
core.endGroup()
if (mergedPullRequests.length === 0) { if (mergedPullRequests.length === 0) {
core.warning( core.warning(`⚠️ No pull requests found`)
`No pull requests found for between ${this.options.fromTag}...${toTag}` return configuration.empty_template ?? DefaultConfiguration.empty_template
)
return configuration.empty_template
? 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( private async getMergedPullRequests(
octokit: Octokit octokit: Octokit
): Promise<PullRequestInfo[]> { ): Promise<PullRequestInfo[]> {
const {owner, repo, fromTag, toTag, configuration} = this.options 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 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) { if (commits.length === 0) {
core.warning(`💥 No commits found between - ${fromTag}...${toTag}`)
return [] return []
} }
@@ -81,17 +88,17 @@ export class ReleaseNotes {
let fromDate = firstCommit.date let fromDate = firstCommit.date
const toDate = lastCommit.date const toDate = lastCommit.date
const maxDays = configuration.max_back_track_time_days const maxDays =
? configuration.max_back_track_time_days configuration.max_back_track_time_days ??
: DefaultConfiguration.max_back_track_time_days DefaultConfiguration.max_back_track_time_days
const maxFromDate = toDate.clone().subtract(maxDays, 'days') const maxFromDate = toDate.clone().subtract(maxDays, 'days')
if (maxFromDate.isAfter(fromDate)) { if (maxFromDate.isAfter(fromDate)) {
core.info(`Adjusted 'fromDate' to go max ${maxDays} back`) core.info(`⚠️ Adjusted 'fromDate' to go max ${maxDays} back`)
fromDate = maxFromDate fromDate = maxFromDate
} }
core.info( 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) const pullRequestsApi = new PullRequests(octokit)
@@ -100,24 +107,21 @@ export class ReleaseNotes {
repo, repo,
fromDate, fromDate,
toDate, toDate,
configuration.max_pull_requests configuration.max_pull_requests ?? DefaultConfiguration.max_pull_requests
? configuration.max_pull_requests
: DefaultConfiguration.max_pull_requests
) )
core.info( core.info(
`Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}` `Retrieved ${pullRequests.length} merged PRs for ${owner}/${repo}`
) )
const prCommits = pullRequestsApi.filterCommits( const prCommits = pullRequestsApi.filterCommits(
commits, commits,
configuration.exclude_merge_branches configuration.exclude_merge_branches ??
? configuration.exclude_merge_branches DefaultConfiguration.exclude_merge_branches
: DefaultConfiguration.exclude_merge_branches
) )
core.info( core.info(
`Retrieved ${prCommits.length} PR merge commits for ${owner}/${repo}` `Retrieved ${prCommits.length} PR merge commits for ${owner}/${repo}`
) )
const filteredPullRequests = [] const filteredPullRequests = []
@@ -146,10 +150,10 @@ export class ReleaseNotes {
if (pullRequest) { if (pullRequest) {
filteredPullRequests.push(pullRequest) filteredPullRequests.push(pullRequest)
} else { } else {
core.warning(`${prRef} not found! Commit text: ${commit.summary}`) core.warning(`⚠️ ${prRef} not found! Commit text: ${commit.summary}`)
} }
} else { } else {
core.info(`${prRef} not in date range, 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( 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 return tagsInfo
} }
@@ -60,7 +60,7 @@ export class Tags {
if (tags[i].name.toLowerCase() === tag.toLowerCase()) { if (tags[i].name.toLowerCase() === tag.toLowerCase()) {
if (ignorePreReleases) { if (ignorePreReleases) {
core.info( core.info(
`Enabled 'ignorePreReleases', searching for the closest release` `Enabled 'ignorePreReleases', searching for the closest release`
) )
for (let ii = i + 1; ii < length; ii++) { for (let ii = i + 1; ii < length; ii++) {
if (!tags[ii].name.includes('-')) { 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[] { private sortTags(commits: TagInfo[]): TagInfo[] {
commits.sort((b, a) => { commits.sort((b, a) => {
const partsA = a.name.replace(/^v/, '').split('-') const partsA = a.name.replace(/^v/, '').split('-')
@@ -97,20 +111,3 @@ export class Tags {
return commits 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
*/
+30 -16
View File
@@ -12,11 +12,10 @@ export function buildChangelog(
config: Configuration config: Configuration
): string { ): string {
// sort to target order // sort to target order
prs = sortPullRequests( const sort = config.sort ?? DefaultConfiguration.sort
prs, const sortAsc = sort.toUpperCase() === 'ASC'
(config.sort ? config.sort : DefaultConfiguration.sort).toUpperCase() === prs = sortPullRequests(prs, sortAsc)
'ASC' core.info(`️ Sorted all pull requests ascending: ${sort}`)
)
const validatedTransformers = validateTransfomers(config.transformers) const validatedTransformers = validateTransfomers(config.transformers)
const transformedMap = new Map<PullRequestInfo, string>() const transformedMap = new Map<PullRequestInfo, string>()
@@ -35,14 +34,17 @@ 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 // bring PRs into the order of categories
const categorized = new Map<Category, string[]>() const categorized = new Map<Category, string[]>()
if (config.categories) { const categories = config.categories ?? DefaultConfiguration.categories
for (const category of config.categories) { for (const category of categories) {
categorized.set(category, []) categorized.set(category, [])
} }
}
const uncategorized: string[] = [] const uncategorized: string[] = []
// bring elements in order // bring elements in order
@@ -60,6 +62,7 @@ export function buildChangelog(
uncategorized.push(body) uncategorized.push(body)
} }
} }
core.info(`️ Ordered all pull requests into ${categories.length} categories`)
// construct final changelog // construct final changelog
let changelog = '' let changelog = ''
@@ -75,16 +78,18 @@ export function buildChangelog(
changelog = `${changelog}\n` changelog = `${changelog}\n`
} }
} }
core.info(`✒️ Wrote ${categorized.size} categorized pull requests down`)
let changelogUncategorized = '' let changelogUncategorized = ''
for (const pr of uncategorized) { for (const pr of uncategorized) {
changelogUncategorized = `${changelogUncategorized + pr}\n` changelogUncategorized = `${changelogUncategorized + pr}\n`
} }
core.info(
`✒️ Wrote ${changelogUncategorized.length} non categorized pull requests down`
)
// fill template // fill template
let transformedChangelog = config.template let transformedChangelog = config.template ?? DefaultConfiguration.template
? config.template
: DefaultConfiguration.template
transformedChangelog = transformedChangelog.replace( transformedChangelog = transformedChangelog.replace(
'${{CHANGELOG}}', '${{CHANGELOG}}',
changelog changelog
@@ -93,6 +98,7 @@ export function buildChangelog(
'${{UNCATEGORIZED}}', '${{UNCATEGORIZED}}',
changelogUncategorized changelogUncategorized
) )
core.info(`️ Filled template`)
return transformedChangelog return transformedChangelog
} }
@@ -107,7 +113,17 @@ function fillTemplate(pr: PullRequestInfo, template: string): string {
transformed = transformed.replace('${{URL}}', pr.htmlURL) transformed = transformed.replace('${{URL}}', pr.htmlURL)
transformed = transformed.replace('${{MERGED_AT}}', pr.mergedAt.toISOString()) transformed = transformed.replace('${{MERGED_AT}}', pr.mergedAt.toISOString())
transformed = transformed.replace('${{AUTHOR}}', pr.author) 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('${{BODY}}', pr.body)
transformed = transformed.replace(
'${{ASSIGNEES}}',
pr.assignees?.join(', ') ?? ''
)
transformed = transformed.replace(
'${{REVIEWERS}}',
pr.requestedReviewers?.join(', ') ?? ''
)
return transformed return transformed
} }
@@ -125,10 +141,8 @@ function transform(filled: string, transformers: RegexTransformer[]): string {
function validateTransfomers( function validateTransfomers(
specifiedTransformers: Transformer[] specifiedTransformers: Transformer[]
): RegexTransformer[] { ): RegexTransformer[] {
const transformers = specifiedTransformers const transformers =
? specifiedTransformers specifiedTransformers ?? DefaultConfiguration.transformers
: DefaultConfiguration.transformers
return transformers return transformers
.map(transformer => { .map(transformer => {
try { try {
@@ -137,7 +151,7 @@ function validateTransfomers(
target: transformer.target target: transformer.target
} }
} catch (e) { } catch (e) {
core.warning(`Bad replacer regex: ${transformer.pattern}`) core.warning(`⚠️ Bad replacer regex: ${transformer.pattern}`)
return { return {
pattern: null, pattern: null,
target: '' target: ''
+36 -1
View File
@@ -1,8 +1,43 @@
import * as fs from 'fs' import * as fs from 'fs'
import {Configuration} from './configuration' import {Configuration} from './configuration'
export function readConfiguration(filename: string): Configuration { export function readConfiguration(filename: string): Configuration | null {
try {
const rawdata = fs.readFileSync(filename, 'utf8') const rawdata = fs.readFileSync(filename, 'utf8')
const configurationJSON: Configuration = JSON.parse(rawdata) const configurationJSON: Configuration = JSON.parse(rawdata)
return configurationJSON 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`)
} }