+26
-10
@@ -10,10 +10,13 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install NPM
|
||||
run: |
|
||||
npm install
|
||||
- run: |
|
||||
- name: Run NPM
|
||||
run: |
|
||||
npm run all
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -22,6 +25,8 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# the below actions use the local state of the action. please replace `./` with `mikepenz/release-changelog-builder-action@{latest-release}`
|
||||
# Showcases how to use the action without a prior checkout
|
||||
# Won't support providing a configuration file, as no checkout was done
|
||||
- name: "Configuration without Checkout"
|
||||
id: without_checkout
|
||||
@@ -37,8 +42,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Checkout full depth so tags can be discovered automatically if not specified
|
||||
fetch-depth: 0
|
||||
|
||||
# Showcase the most minimal configuration possible
|
||||
- name: "Minimal Configuration"
|
||||
id: minimal_release
|
||||
uses: ./
|
||||
@@ -48,6 +54,7 @@ jobs:
|
||||
- name: Echo Minimal Configuration Changelog
|
||||
run: echo "${{steps.minimal_release.outputs.changelog}}"
|
||||
|
||||
# Showcases a more complex configuration, providing a configuration, and specifically referencing owner, repo, from and to tag
|
||||
- name: "Complex Configuration"
|
||||
id: complex_release
|
||||
uses: ./
|
||||
@@ -62,6 +69,21 @@ jobs:
|
||||
- name: Echo Complex Configuration Changelog
|
||||
run: echo "${{steps.complex_release.outputs.changelog}}"
|
||||
|
||||
# Showcases the capability to generate the changelog for an external repository provided
|
||||
- name: "External Repo Configuration"
|
||||
id: external_changelog
|
||||
uses: ./
|
||||
with:
|
||||
configuration: "configs/configuration_complex.json"
|
||||
owner: "mikepenz"
|
||||
repo: "MaterialDrawer"
|
||||
fromTag: "v8.1.0"
|
||||
toTag: "v8.1.6"
|
||||
token: ${{ secrets.PERSONAL_TOKEN }}
|
||||
|
||||
- name: Echo External Repo Configuration Changelog
|
||||
run: echo "${{steps.external_changelog.outputs.changelog}}"
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
@@ -69,17 +91,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare 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:
|
||||
configuration: "configs/configuration_repo.json"
|
||||
toTag: ${{ steps.tag_version.outputs.VERSION }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
... a github action that builds your release notes fast, easy and exactly the way you want.
|
||||
... a GitHub action that builds your release notes fast, easy and exactly the way you want.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -21,9 +21,9 @@
|
||||
<p align="center">
|
||||
<a href="#whats-included-">What's included 🚀</a> •
|
||||
<a href="#setup">Setup 🛠️</a> •
|
||||
<a href="#full-sample-%EF%B8%8F">Full Sample 🖥️</a> •
|
||||
<a href="#customization-%EF%B8%8F">Customization 🖍️</a> •
|
||||
<a href="#contribute-">Contribute 🧬</a> •
|
||||
<a href="#complete-sample-%EF%B8%8F">Complete Sample 🖥️</a> •
|
||||
<a href="#license">License 📓</a>
|
||||
</p>
|
||||
|
||||
@@ -32,13 +32,14 @@
|
||||
### What's included 🚀
|
||||
|
||||
- Super simple integration
|
||||
- even on huge repositories with hundreds of tags
|
||||
- ...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
|
||||
- Rich build log output
|
||||
|
||||
-------
|
||||
|
||||
@@ -51,26 +52,72 @@ Specify the action as part of your GitHub actions workflow:
|
||||
```yml
|
||||
- name: "Build Changelog"
|
||||
id: build_changelog
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: mikepenz/release-changelog-builder-action@{latest-release}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
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.
|
||||
By default the action will try to automatically retrieve the `tag` from the current commit and automatically resolve the `tag` before. Read more about this here.
|
||||
|
||||
### Action outputs
|
||||
|
||||
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`.
|
||||
After action execution it will return the `changelog` and additional information as step output. You can use it in any follow-up step by referencing the output by referencing it via the id of the step. For example `build_changelog`.
|
||||
|
||||
```yml
|
||||
# ${{steps.{CHANGELOG_STEP_ID}.outputs.changelog}}
|
||||
${{steps.build_changelog.outputs.changelog}}
|
||||
```
|
||||
|
||||
A full set list of possible output values for this action.
|
||||
|
||||
| **Output** | **Description** |
|
||||
|---------------------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| `outputs.changelog` | The built release changelog built from the merged pull requests |
|
||||
| `outputs.owner` | Specifies the owner of the repository processed |
|
||||
| `outputs.repo` | Describes the repository name, which was processed |
|
||||
| `outputs.fromTag` | Defines the `fromTag` which describes the lower bound to process pull requests for |
|
||||
| `outputs.toTag` | Defines the `toTag` which describes the upper bound to process pull request for |
|
||||
| `outputs.failed` | Defines if there was an issue with the action run, and the changelog may not have been generated correctly. [true, false] |
|
||||
|
||||
|
||||
## Full 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: Build Changelog
|
||||
id: github_release
|
||||
uses: mikepenz/release-changelog-builder-action@main
|
||||
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 }}
|
||||
```
|
||||
|
||||
|
||||
## Customization 🖍️
|
||||
|
||||
### Changelog Configuration
|
||||
### 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.
|
||||
|
||||
@@ -120,9 +167,11 @@ This configuration is a `.json` file in the following format.
|
||||
}
|
||||
```
|
||||
|
||||
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)
|
||||
Any section of the configuration can be omitted to have defaults apply.
|
||||
|
||||
Defaults for the configuration can be found in the [configuration.ts](https://github.com/mikepenz/release-changelog-builder-action/blob/develop/src/configuration.ts)
|
||||
|
||||
Please see the [Configuration Specification](#configuration-specification) for detailed descriptions on the offered configuration options.
|
||||
|
||||
### Advanced workflow specification
|
||||
|
||||
@@ -138,12 +187,26 @@ For advanced usecases additional settings can be provided to the action
|
||||
owner: "mikepenz"
|
||||
repo: "release-changelog-builder-action"
|
||||
ignorePreReleases: "false"
|
||||
fromTag: "0.0.2"
|
||||
toTag: "0.0.3"
|
||||
fromTag: "0.3.0"
|
||||
toTag: "0.5.0"
|
||||
token: ${{ secrets.PAT }}
|
||||
```
|
||||
|
||||
💡 `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).
|
||||
💡 All input values are optional. It is only required to provide the `token` either via the input, or as `env` variable.
|
||||
|
||||
| **Input** | **Description** |
|
||||
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `configuration` | Relative path, to the `configuration.json` file, providing additional configurations |
|
||||
| `owner` | The owner of the repository to generate the changelog for |
|
||||
| `repo` | Name of the repository we want to process |
|
||||
| `fromTag` | Defines the 'start' from where the changelog will consider merged pull requests |
|
||||
| `toTag` | Defines until which tag the changelog will consider merged pull requests |
|
||||
| `path` | Allows to specify an alternative sub directory, to use as base |
|
||||
| `token` | Alternative config to specify token. You should prefer `env.GITHUB_TOKEN` instead though |
|
||||
| `ignorePreReleases` | Allows to ignore pre-releases for changelog generation (E.g. for 1.0.1... 1.0.0-rc02 <- ignore, 1.0.0 <- pick). Only used if `fromTag` was not specified. Default: false |
|
||||
| `failOnError` | Defines if the action will result in a build failure if problems occurred. Default: false |
|
||||
|
||||
💡 `${{ secrets.GITHUB_TOKEN }}` only grants rights to the current repository, for other repositories please use a PAT (Personal Access Token).
|
||||
|
||||
### PR Template placeholders
|
||||
|
||||
@@ -171,47 +234,26 @@ Table of supported placeholders allowed to be used in the `pr_template` configur
|
||||
| `${{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 |
|
||||
|
||||
### Configuration Specification
|
||||
|
||||
## Complete Sample 🖥️
|
||||
Table of descriptions for the `configuration.json` options.
|
||||
|
||||
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 }}
|
||||
```
|
||||
| **Input** | **Description** |
|
||||
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| categories | An array of `category` specifications, offering a flexible way to group changes into categories |
|
||||
| category.title | The display name of a category in the changelog |
|
||||
| category.labels | An array of labels, to match pull request labels against. If any PR label, matches any category label, the pull request will show up under this category |
|
||||
| sort | The sort order of pull requests. [ASC, DESC] |
|
||||
| template | Specifies the global template to pick for creating the changelog. See [Template placeholders](#template-placeholders) for possible values |
|
||||
| pr_template | Defines the per pull request template. See [PR Template placeholders](#pr-template-placeholders) for possible values |
|
||||
| empty_template | Template to pick if no changes are detected. Does not support placeholders |
|
||||
| transformers | An array of `transform` specifications, offering a flexible API to modify the text per pull request. This is applied on the change text created with `pr_template`. `transformers` are executed per change, in the order specified |
|
||||
| transformer.pattern | A `regex` pattern, extracting values of the change message. |
|
||||
| transformer.target | The result pattern, the regex groups will be filled into. Allows for full transformation of a pull request message. Including potentially specified texts |
|
||||
| max_tags_to_fetch | The maximum amount of tags to load from the API to find the previous tag. Loaded paginated with 100 per page |
|
||||
| max_pull_requests | The maximum amount of pull requests to load from the API. Loaded paginated with 30 per page |
|
||||
| max_back_track_time_days | Defines the max amount of days to go back in time per changelog |
|
||||
| exclude_merge_branches | An array of branches to be ignored from processing as merge commits |
|
||||
|
||||
## Contribute 🧬
|
||||
|
||||
@@ -229,7 +271,7 @@ $ npm test
|
||||
$ npm run lint -- --fix
|
||||
```
|
||||
|
||||
It's suggested to export the token to your path before running the tests, so that 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
|
||||
@@ -248,7 +290,8 @@ Core parts of the PR fetching logic are based on [pull-release-notes](https://gi
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+48
-6
@@ -1,5 +1,5 @@
|
||||
import {ReleaseNotes} from '../src/releaseNotes'
|
||||
import {readConfiguration} from '../src/utils'
|
||||
import { resolveConfiguration } from '../src/utils';
|
||||
|
||||
// shows how the runner will run a javascript action with env / stdout protocol
|
||||
/*
|
||||
@@ -17,31 +17,33 @@ test('test runs', () => {
|
||||
it('Should have empty changelog (tags)', async () => {
|
||||
jest.setTimeout(180000)
|
||||
|
||||
const configuration = readConfiguration('configs/configuration.json')!!
|
||||
const configuration = resolveConfiguration('', 'configs/configuration.json')
|
||||
const releaseNotes = new ReleaseNotes({
|
||||
owner: 'mikepenz',
|
||||
repo: 'release-changelog-builder-action',
|
||||
fromTag: 'v0.0.1',
|
||||
toTag: 'v0.0.2',
|
||||
ignorePreReleases: false,
|
||||
failOnError: false,
|
||||
configuration: configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
console.log(changeLog)
|
||||
expect(changeLog).toStrictEqual(`- no changes`)
|
||||
expect(changeLog).toStrictEqual(null)
|
||||
})
|
||||
|
||||
it('Should match generated changelog (tags)', async () => {
|
||||
jest.setTimeout(180000)
|
||||
|
||||
const configuration = readConfiguration('configs/configuration.json')!!
|
||||
const configuration = resolveConfiguration('', 'configs/configuration.json')
|
||||
const releaseNotes = new ReleaseNotes({
|
||||
owner: 'mikepenz',
|
||||
repo: 'release-changelog-builder-action',
|
||||
fromTag: 'v0.0.1',
|
||||
toTag: 'v0.0.3',
|
||||
ignorePreReleases: false,
|
||||
failOnError: false,
|
||||
configuration: configuration
|
||||
})
|
||||
|
||||
@@ -58,13 +60,14 @@ it('Should match generated changelog (tags)', async () => {
|
||||
it('Should match generated changelog (unspecified fromTag)', async () => {
|
||||
jest.setTimeout(180000)
|
||||
|
||||
const configuration = readConfiguration('configs/configuration.json')!!
|
||||
const configuration = resolveConfiguration('', 'configs/configuration.json')
|
||||
const releaseNotes = new ReleaseNotes({
|
||||
owner: 'mikepenz',
|
||||
repo: 'release-changelog-builder-action',
|
||||
fromTag: null,
|
||||
toTag: 'v0.0.3',
|
||||
ignorePreReleases: false,
|
||||
failOnError: false,
|
||||
configuration: configuration
|
||||
})
|
||||
|
||||
@@ -81,13 +84,14 @@ it('Should match generated changelog (unspecified fromTag)', async () => {
|
||||
it('Should match generated changelog (refs)', async () => {
|
||||
jest.setTimeout(180000)
|
||||
|
||||
const configuration = readConfiguration('configs/configuration_all_placeholders.json')!!
|
||||
const configuration = resolveConfiguration('', 'configs_test/configuration_all_placeholders.json')
|
||||
const releaseNotes = new ReleaseNotes({
|
||||
owner: 'mikepenz',
|
||||
repo: 'release-changelog-builder-action',
|
||||
fromTag: '5ec7a2d86fe9f43fdd38d5e254a1117c8a51b4c3',
|
||||
toTag: 'fa3788c8c4b3373ef8424ce3eb008a5cd07cc5aa',
|
||||
ignorePreReleases: false,
|
||||
failOnError: false,
|
||||
configuration: configuration
|
||||
})
|
||||
|
||||
@@ -108,3 +112,41 @@ nhoelzl
|
||||
|
||||
`)
|
||||
})
|
||||
|
||||
it('Should match ordered ASC', async () => {
|
||||
jest.setTimeout(180000)
|
||||
|
||||
const configuration = resolveConfiguration('', 'configs_test/configuration_asc.json')
|
||||
const releaseNotes = new ReleaseNotes({
|
||||
owner: 'mikepenz',
|
||||
repo: 'release-changelog-builder-action',
|
||||
fromTag: 'v0.3.0',
|
||||
toTag: 'v0.5.0',
|
||||
ignorePreReleases: false,
|
||||
failOnError: false,
|
||||
configuration: configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
console.log(changeLog)
|
||||
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n22\n24\n25\n26\n28\n\n## 🐛 Fixes\n\n23\n\n`)
|
||||
})
|
||||
|
||||
it('Should match ordered DESC', async () => {
|
||||
jest.setTimeout(180000)
|
||||
|
||||
const configuration = resolveConfiguration('', 'configs_test/configuration_desc.json')
|
||||
const releaseNotes = new ReleaseNotes({
|
||||
owner: 'mikepenz',
|
||||
repo: 'release-changelog-builder-action',
|
||||
fromTag: 'v0.3.0',
|
||||
toTag: 'v0.5.0',
|
||||
ignorePreReleases: false,
|
||||
failOnError: false,
|
||||
configuration: configuration
|
||||
})
|
||||
|
||||
const changeLog = await releaseNotes.pull()
|
||||
console.log(changeLog)
|
||||
expect(changeLog).toStrictEqual(`## 🚀 Features\n\n28\n26\n25\n24\n22\n\n## 🐛 Fixes\n\n23\n\n`)
|
||||
})
|
||||
@@ -20,6 +20,9 @@ inputs:
|
||||
ignorePreReleases:
|
||||
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"
|
||||
failOnError:
|
||||
description: 'Defines if the action should result in a build failure, if an error was discovered'
|
||||
default: "false"
|
||||
token:
|
||||
description: 'Defines the token to use to execute the git API requests with, uses `env.GITHUB_TOKEN` by default'
|
||||
outputs:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sort": "ASC",
|
||||
"pr_template": "${{NUMBER}}"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sort": "DESC",
|
||||
"pr_template": "${{NUMBER}}"
|
||||
}
|
||||
+44
-55
@@ -1,98 +1,81 @@
|
||||
import * as core from '@actions/core'
|
||||
import {readConfiguration} from './utils'
|
||||
import {
|
||||
failOrError,
|
||||
retrieveRepositoryPath,
|
||||
resolveConfiguration
|
||||
} from './utils'
|
||||
import {ReleaseNotes} from './releaseNotes'
|
||||
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.setOutput('failed', false) // mark the action not failed by default
|
||||
|
||||
core.startGroup(`📘 Reading input values`)
|
||||
try {
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
throw new Error('GITHUB_WORKSPACE not defined')
|
||||
}
|
||||
githubWorkspacePath = path.resolve(githubWorkspacePath)
|
||||
core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`)
|
||||
|
||||
let repositoryPath = core.getInput('path') || '.'
|
||||
repositoryPath = path.resolve(githubWorkspacePath, repositoryPath)
|
||||
core.debug(`repositoryPath = '${repositoryPath}'`)
|
||||
// read in path specification, resolve github workspace, and repo path
|
||||
const inputPath = core.getInput('path')
|
||||
const repositoryPath = retrieveRepositoryPath(inputPath)
|
||||
|
||||
// read in configuration file if possible
|
||||
const configurationFile: string = core.getInput('configuration')
|
||||
let configuration = DefaultConfiguration
|
||||
if (configurationFile) {
|
||||
const configurationPath = path.resolve(
|
||||
githubWorkspacePath,
|
||||
const configuration = resolveConfiguration(
|
||||
repositoryPath,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// read in repository inputs
|
||||
const token = core.getInput('token')
|
||||
let owner = core.getInput('owner')
|
||||
let repo = core.getInput('repo')
|
||||
|
||||
const owner = core.getInput('owner') ?? github.context.repo.owner
|
||||
const repo = core.getInput('repo') ?? github.context.repo.repo
|
||||
// read in from, to tag inputs
|
||||
const fromTag = core.getInput('fromTag')
|
||||
let toTag = core.getInput('toTag')
|
||||
// read in flags
|
||||
const ignorePreReleases = core.getInput('ignorePreReleases') === 'true'
|
||||
const failOnError = core.getInput('failOnError') === 'true'
|
||||
|
||||
const ignorePreReleases = core.getInput('ignorePreReleases')
|
||||
|
||||
// ensure to resolve the toTag if it was not provided
|
||||
if (!toTag) {
|
||||
// if not specified try to retrieve tag from github.context.ref
|
||||
if (github.context.ref.startsWith('refs/tags/')) {
|
||||
toTag = github.context.ref.replace('refs/tags/', '')
|
||||
core.info(
|
||||
`🔖 Resolved current tag (${toTag}) from the 'github.context.ref'`
|
||||
)
|
||||
} else {
|
||||
// if not specified try to retrieve tag from git
|
||||
const gitHelper = await createCommandManager(repositoryPath)
|
||||
const latestTag = await gitHelper.latestTag()
|
||||
toTag = latestTag
|
||||
core.debug(`toTag = '${latestTag}'`)
|
||||
}
|
||||
|
||||
if (!owner || !repo) {
|
||||
// Qualified repository
|
||||
const qualifiedRepository =
|
||||
core.getInput('repository') ||
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`
|
||||
core.debug(`qualified repository = '${qualifiedRepository}'`)
|
||||
const splitRepository = qualifiedRepository.split('/')
|
||||
if (
|
||||
splitRepository.length !== 2 ||
|
||||
!splitRepository[0] ||
|
||||
!splitRepository[1]
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`
|
||||
core.info(
|
||||
`🔖 Resolved current tag (${toTag}) from 'git rev-list --tags --skip=0 --max-count=1'`
|
||||
)
|
||||
}
|
||||
owner = splitRepository[0]
|
||||
repo = splitRepository[1]
|
||||
}
|
||||
|
||||
if (!owner) {
|
||||
core.error(`💥 Missing or couldn't resolve 'owner'`)
|
||||
failOrError(`💥 Missing or couldn't resolve 'owner'`, failOnError)
|
||||
return
|
||||
} else {
|
||||
core.setOutput('owner', owner)
|
||||
core.debug(`Resolved 'owner' as ${owner}`)
|
||||
}
|
||||
|
||||
if (!repo) {
|
||||
core.error(`💥 Missing or couldn't resolve 'owner'`)
|
||||
failOrError(`💥 Missing or couldn't resolve 'owner'`, failOnError)
|
||||
return
|
||||
} else {
|
||||
core.setOutput('repo', repo)
|
||||
core.debug(`Resolved 'repo' as ${repo}`)
|
||||
}
|
||||
|
||||
if (!toTag) {
|
||||
core.error(`💥 Missing or couldn't resolve 'toTag'`)
|
||||
failOrError(`💥 Missing or couldn't resolve 'toTag'`, failOnError)
|
||||
return
|
||||
} else {
|
||||
core.setOutput('toTag', toTag)
|
||||
core.debug(`Resolved 'toTag' as ${toTag}`)
|
||||
}
|
||||
core.endGroup()
|
||||
@@ -102,11 +85,17 @@ async function run(): Promise<void> {
|
||||
repo,
|
||||
fromTag,
|
||||
toTag,
|
||||
ignorePreReleases: ignorePreReleases === 'true',
|
||||
ignorePreReleases,
|
||||
failOnError,
|
||||
configuration
|
||||
})
|
||||
|
||||
core.setOutput('changelog', await releaseNotes.pull(token))
|
||||
core.setOutput(
|
||||
'changelog',
|
||||
(await releaseNotes.pull(token)) ??
|
||||
configuration.empty_template ??
|
||||
DefaultConfiguration.empty_template
|
||||
)
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
}
|
||||
|
||||
+36
-9
@@ -5,6 +5,7 @@ import {buildChangelog} from './transform'
|
||||
import * as core from '@actions/core'
|
||||
import {Tags} from './tags'
|
||||
import {Configuration, DefaultConfiguration} from './configuration'
|
||||
import {failOrError} from './utils'
|
||||
|
||||
export interface ReleaseNotesOptions {
|
||||
owner: string // the owner of the repository
|
||||
@@ -12,18 +13,26 @@ export interface ReleaseNotesOptions {
|
||||
fromTag: string | null // the tag/ref to start from
|
||||
toTag: string // the tag/ref up to
|
||||
ignorePreReleases: boolean // defines if we should ignore any pre-releases for matching, only relevant if fromTag is null
|
||||
failOnError: boolean // defines if we should fail the action in case of an error
|
||||
configuration: Configuration // the configuration as defined in `configuration.ts`
|
||||
}
|
||||
|
||||
export class ReleaseNotes {
|
||||
constructor(private options: ReleaseNotesOptions) {}
|
||||
|
||||
async pull(token?: string): Promise<string> {
|
||||
async pull(token?: string): Promise<string | null> {
|
||||
const octokit = new Octokit({
|
||||
auth: `token ${token || process.env.GITHUB_TOKEN}`
|
||||
})
|
||||
|
||||
const {owner, repo, toTag, ignorePreReleases, configuration} = this.options
|
||||
const {
|
||||
owner,
|
||||
repo,
|
||||
toTag,
|
||||
ignorePreReleases,
|
||||
failOnError,
|
||||
configuration
|
||||
} = this.options
|
||||
|
||||
if (!this.options.fromTag) {
|
||||
core.startGroup(`🔖 Resolve previous tag`)
|
||||
@@ -39,23 +48,31 @@ export class ReleaseNotes {
|
||||
DefaultConfiguration.max_tags_to_fetch
|
||||
)
|
||||
if (previousTag == null) {
|
||||
core.error(`💥 Unable to retrieve previous tag given ${toTag}`)
|
||||
return (
|
||||
configuration.empty_template ?? DefaultConfiguration.empty_template
|
||||
failOrError(
|
||||
`💥 Unable to retrieve previous tag given ${toTag}`,
|
||||
failOnError
|
||||
)
|
||||
return null
|
||||
}
|
||||
this.options.fromTag = previousTag.name
|
||||
core.debug(`fromTag resolved via previousTag as: ${previousTag.name}`)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
if (!this.options.fromTag) {
|
||||
failOrError(`💥 Missing or couldn't resolve 'fromTag'`, failOnError)
|
||||
return null
|
||||
} else {
|
||||
core.setOutput('fromTag', this.options.fromTag)
|
||||
}
|
||||
|
||||
core.startGroup(`🚀 Load pull requests`)
|
||||
const mergedPullRequests = await this.getMergedPullRequests(octokit)
|
||||
core.endGroup()
|
||||
|
||||
if (mergedPullRequests.length === 0) {
|
||||
core.warning(`⚠️ No pull requests found`)
|
||||
return configuration.empty_template ?? DefaultConfiguration.empty_template
|
||||
return null
|
||||
}
|
||||
|
||||
core.startGroup('📦 Build changelog')
|
||||
@@ -67,7 +84,14 @@ export class ReleaseNotes {
|
||||
private async getMergedPullRequests(
|
||||
octokit: Octokit
|
||||
): Promise<PullRequestInfo[]> {
|
||||
const {owner, repo, fromTag, toTag, configuration} = this.options
|
||||
const {
|
||||
owner,
|
||||
repo,
|
||||
fromTag,
|
||||
toTag,
|
||||
failOnError,
|
||||
configuration
|
||||
} = this.options
|
||||
core.info(`ℹ️ Comparing ${owner}/${repo} - '${fromTag}...${toTag}'`)
|
||||
|
||||
const commitsApi = new Commits(octokit)
|
||||
@@ -75,11 +99,14 @@ export class ReleaseNotes {
|
||||
try {
|
||||
commits = await commitsApi.getDiff(owner, repo, fromTag!!, toTag)
|
||||
} catch (error) {
|
||||
core.error(`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`)
|
||||
failOrError(
|
||||
`💥 Failed to retrieve - Invalid tag? - Because of: ${error}`,
|
||||
failOnError
|
||||
)
|
||||
return []
|
||||
}
|
||||
if (commits.length === 0) {
|
||||
core.warning(`💥 No commits found between - ${fromTag}...${toTag}`)
|
||||
core.warning(`⚠️ No commits found between - ${fromTag}...${toTag}`)
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
+78
-8
@@ -1,7 +1,71 @@
|
||||
import * as fs from 'fs'
|
||||
import {Configuration} from './configuration'
|
||||
import {Configuration, DefaultConfiguration} from './configuration'
|
||||
import * as core from '@actions/core'
|
||||
import * as path from 'path'
|
||||
|
||||
export function readConfiguration(filename: string): Configuration | null {
|
||||
/**
|
||||
* Resolves the repository path, relatively to the GITHUB_WORKSPACE
|
||||
*/
|
||||
export function retrieveRepositoryPath(providedPath: string): string {
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
throw new Error('GITHUB_WORKSPACE not defined')
|
||||
}
|
||||
githubWorkspacePath = path.resolve(githubWorkspacePath)
|
||||
core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`)
|
||||
|
||||
let repositoryPath = providedPath || '.'
|
||||
repositoryPath = path.resolve(githubWorkspacePath, repositoryPath)
|
||||
core.debug(`repositoryPath = '${repositoryPath}'`)
|
||||
return repositoryPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Will automatically either report the message to the log, or mark the action as failed. Additionally defining the output failed, allowing it to be read in by other actions
|
||||
*/
|
||||
export function failOrError(
|
||||
message: string | Error,
|
||||
failOnError: boolean
|
||||
): void {
|
||||
// if we report any failure, consider the action to have failed, may not make the build fail
|
||||
core.setOutput('failed', true)
|
||||
if (failOnError) {
|
||||
core.setFailed(message)
|
||||
} else {
|
||||
core.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configuration given the file path, if not found it will fallback to the `DefaultConfiguration`
|
||||
*/
|
||||
export function resolveConfiguration(
|
||||
githubWorkspacePath: string,
|
||||
configurationFile: string
|
||||
): Configuration {
|
||||
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
|
||||
}
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in the configuration from the JSON file
|
||||
*/
|
||||
function readConfiguration(filename: string): Configuration | null {
|
||||
try {
|
||||
const rawdata = fs.readFileSync(filename, 'utf8')
|
||||
const configurationJSON: Configuration = JSON.parse(rawdata)
|
||||
@@ -11,25 +75,31 @@ export function readConfiguration(filename: string): Configuration | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function directoryExistsSync(path: string, required?: boolean): boolean {
|
||||
if (!path) {
|
||||
/**
|
||||
* Checks if a given directory exists
|
||||
*/
|
||||
export function directoryExistsSync(
|
||||
inputPath: string,
|
||||
required?: boolean
|
||||
): boolean {
|
||||
if (!inputPath) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
stats = fs.statSync(inputPath)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
throw new Error(`Directory '${inputPath}' does not exist`)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
`Encountered an error when checking whether path '${inputPath}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,5 +109,5 @@ export function directoryExistsSync(path: string, required?: boolean): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
throw new Error(`Directory '${inputPath}' does not exist`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user