From 3fd4677f8af771b636180f911670d0adb57582f7 Mon Sep 17 00:00:00 2001 From: Mike Penz Date: Sun, 18 Oct 2020 11:06:02 +0200 Subject: [PATCH] - introduce new capability to enforce a build failure - introduce config to fail on error instead of just logging - simplify main logic, move functionality in other classes --- action.yml | 3 ++ src/main.ts | 92 +++++++++++++++++---------------------------- src/releaseNotes.ts | 42 +++++++++++++++------ src/utils.ts | 86 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 146 insertions(+), 77 deletions(-) diff --git a/action.yml b/action.yml index d875347..51c324e 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/src/main.ts b/src/main.ts index 88033e9..7c3f514 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,52 +1,42 @@ 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 github.context.ref if (github.context.ref.startsWith('refs/tags/')) { @@ -65,28 +55,8 @@ async function run(): Promise { } } - 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}.` - ) - } - 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) @@ -94,7 +64,7 @@ async function run(): Promise { } if (!repo) { - core.error(`đŸ’Ĩ Missing or couldn't resolve 'owner'`) + failOrError(`đŸ’Ĩ Missing or couldn't resolve 'owner'`, failOnError) return } else { core.setOutput('repo', repo) @@ -102,7 +72,7 @@ async function run(): Promise { } if (!toTag) { - core.error(`đŸ’Ĩ Missing or couldn't resolve 'toTag'`) + failOrError(`đŸ’Ĩ Missing or couldn't resolve 'toTag'`, failOnError) return } else { core.setOutput('toTag', toTag) @@ -115,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 705978d..3d3989e 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,10 +48,11 @@ 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}`) @@ -50,8 +60,8 @@ export class ReleaseNotes { } if (!this.options.fromTag) { - core.error(`đŸ’Ĩ Missing or couldn't resolve 'fromTag'`) - return configuration.empty_template ?? DefaultConfiguration.empty_template + failOrError(`đŸ’Ĩ Missing or couldn't resolve 'fromTag'`, failOnError) + return null } else { core.setOutput('fromTag', this.options.fromTag) } @@ -62,7 +72,7 @@ export class ReleaseNotes { if (mergedPullRequests.length === 0) { core.warning(`âš ī¸ No pull requests found`) - return configuration.empty_template ?? DefaultConfiguration.empty_template + return null } core.startGroup('đŸ“Ļ Build changelog') @@ -74,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) @@ -82,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 a44fdc3..997409d 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`) }