diff --git a/packages/toolkit/__tests__/toolkit.test.ts b/packages/toolkit/__tests__/toolkit.test.ts index 0ab2d244..4a7b233f 100644 --- a/packages/toolkit/__tests__/toolkit.test.ts +++ b/packages/toolkit/__tests__/toolkit.test.ts @@ -1,29 +1,50 @@ +import {Signale} from 'signale' +import {ExitCode} from '../src/exit' import {Toolkit} from '../src/toolkit' describe('Toolkit', () => { - it('runs a sync function', async () => { - const cb = jest.fn(() => true) - const value = await Toolkit.run(cb) - expect(cb).toHaveBeenCalledWith(expect.any(Toolkit)) - expect(value).toBe(true) - }) - - it('runs an async function', async () => { - const cb = jest.fn(async () => true) - const value = await Toolkit.run(cb) - expect(cb).toHaveBeenCalledWith(expect.any(Toolkit)) - expect(value).toBe(true) - }) - - it('logs and fails when an error occurs', async () => { - const err = new Error() - const exitFailure = jest.fn() - - await Toolkit.run(async tk => { - tk.exit.failure = exitFailure - throw err + describe('.run', () => { + it('runs a sync function', async () => { + const cb = jest.fn(() => true) + const value = await Toolkit.run(cb) + expect(cb).toHaveBeenCalledWith(expect.any(Toolkit)) + expect(value).toBe(true) }) - expect(exitFailure).toHaveBeenCalledWith(err) + it('runs an async function', async () => { + const cb = jest.fn(async () => true) + const value = await Toolkit.run(cb) + expect(cb).toHaveBeenCalledWith(expect.any(Toolkit)) + expect(value).toBe(true) + }) + + it('logs and fails when an error occurs', async () => { + const err = new Error() + const exitFailure = jest.fn() + + await Toolkit.run(async tk => { + tk.exit.failure = exitFailure + throw err + }) + + expect(exitFailure).toHaveBeenCalledWith(err) + }) + }) + + it('asserts required keys are present', async () => { + const missingKey = '__DOES_NOT_EXIST__' + + Reflect.deleteProperty(process.env, missingKey) + + const logger = new Signale() + logger.fatal = jest.fn() + jest.spyOn(process, 'exit').mockImplementation() + + new Toolkit({logger, requiredEnv: [missingKey]}) + + expect(process.exit).toHaveBeenCalledWith(ExitCode.Failure) + expect(logger.fatal) + .toHaveBeenCalledWith(`The following environment variables are required for this action to run: +- __DOES_NOT_EXIST__`) }) }) diff --git a/packages/toolkit/src/toolkit.ts b/packages/toolkit/src/toolkit.ts index 620757f7..255b9c48 100644 --- a/packages/toolkit/src/toolkit.ts +++ b/packages/toolkit/src/toolkit.ts @@ -3,6 +3,23 @@ import {Exit} from './exit' export type ActionFn = (tools: Toolkit) => unknown +/** + * Options used to customize an instance of [[Toolkit]] + */ +export type ToolkitOptions = { + /** + * A custom Signale instance to use + */ + logger?: Signale + + /** + * A list of environment variable names this action requires in order to run + * + * If any of them are missing, the action will fail and log the missing keys. + */ + requiredEnv?: string[] +} + /** * A set of tools for the Actions runtime */ @@ -13,8 +30,8 @@ export class Toolkit { * If an error occurs, the error will be logged and the action will exit as a * failure. */ - static async run(func: ActionFn) { - const tools = new Toolkit() + static async run(func: ActionFn, opts?: ToolkitOptions) { + const tools = new Toolkit(opts) try { const ret = func(tools) @@ -27,26 +44,45 @@ export class Toolkit { /** * A logger for the toolkit, an instance of [Signale](https://github.com/klaussinani/signale) */ - readonly logger: Signale & LoggerFunc = this.wrapLogger( - new Signale({ - config: { - underlineLabel: false - } - }) - ) + readonly logger: Signale & LoggerFunc /** * A wrapper around an instance of [[Exit]] */ - readonly exit: Exit = new Exit(this.logger) + readonly exit: Exit /** * The authentication token for the GitHub API */ readonly token: string = process.env.GITHUB_TOKEN || '' + constructor(opts: ToolkitOptions = {}) { + const logger = opts.logger || new Signale({config: {underlineLabel: false}}) + this.logger = this.wrapLogger(logger) + this.exit = new Exit(this.logger) + + if (opts.requiredEnv) { + this.checkRequiredEnv(opts.requiredEnv) + } + } + /** - * Wrap a Signale logger so that its a callable class + * Ensure that the given keys are in the environment. + */ + private checkRequiredEnv(keys: string[]) { + const missingEnv = keys.filter(key => !process.env.hasOwnProperty(key)) + + if (missingEnv.length === 0) return + + const list = missingEnv.map(key => `- ${key}`).join('\n') + + this.exit.failure( + `The following environment variables are required for this action to run:\n${list}` + ) + } + + /** + * Wrap a Signale logger so that its a callable class. */ private wrapLogger(logger: Signale) { // Create a callable function