diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7e3e555..16a52d4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }} diff --git a/README.md b/README.md index 6e822b9e..d9a7ec62 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- ... 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.

@@ -21,9 +21,9 @@

What's included 🚀Setup 🛠ī¸ • + Full Sample đŸ–Ĩī¸Customization 🖍ī¸Contribute đŸ§Ŧ • - Complete Sample đŸ–Ĩī¸License 📓

@@ -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,13 +167,15 @@ 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 -For advanced usecases additional settings can be provided to the action +For advanced use cases additional settings can be provided to the action ```yml - name: "Complex Configuration" @@ -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 | - -## 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 }} -``` +### Configuration Specification + +Table of descriptions for the `configuration.json` options. + +| **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 diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index d82074f0..cfa86bdf 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -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`) +}) \ No newline at end of file diff --git a/action.yml b/action.yml index d875347f..51c324e2 100644 --- a/action.yml +++ b/action.yml @@ -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: diff --git a/configs/configuration_all_placeholders.json b/configs_test/configuration_all_placeholders.json similarity index 100% rename from configs/configuration_all_placeholders.json rename to configs_test/configuration_all_placeholders.json diff --git a/configs_test/configuration_asc.json b/configs_test/configuration_asc.json new file mode 100644 index 00000000..6e1fa39f --- /dev/null +++ b/configs_test/configuration_asc.json @@ -0,0 +1,4 @@ +{ + "sort": "ASC", + "pr_template": "${{NUMBER}}" +} \ No newline at end of file diff --git a/configs_test/configuration_desc.json b/configs_test/configuration_desc.json new file mode 100644 index 00000000..8d81997f --- /dev/null +++ b/configs_test/configuration_desc.json @@ -0,0 +1,4 @@ +{ + "sort": "DESC", + "pr_template": "${{NUMBER}}" +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 6968453a..7c3f514d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { + 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, - configurationFile - ) - core.debug(`configurationPath = '${configurationPath}'`) - const providedConfiguration = readConfiguration(configurationPath) - if (!providedConfiguration) { - core.info( - `⚠ī¸ Configuration provided, but it couldn't be found, or failed to parse. Fallback to Defaults` - ) - } else { - configuration = providedConfiguration - } - } + const configuration = resolveConfiguration( + repositoryPath, + configurationFile + ) + // 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 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}.` + // 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.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 { 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) } diff --git a/src/releaseNotes.ts b/src/releaseNotes.ts index 003a21ca..3d3989e8 100755 --- a/src/releaseNotes.ts +++ b/src/releaseNotes.ts @@ -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 { + async pull(token?: string): Promise { 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 { - 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 [] } diff --git a/src/utils.ts b/src/utils.ts index a44fdc3b..997409d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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`) }