From 39f7667028449b60a6c57967c27e6e3bf45c062f Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Fri, 19 Apr 2019 17:00:42 -0400 Subject: [PATCH] Add Toolkit class w/logging & exits --- .gitignore | 3 + package-lock.json | 9 ++ packages/toolkit/__tests__/exit.test.ts | 36 +++++ packages/toolkit/__tests__/toolkit.test.ts | 30 +++- packages/toolkit/package-lock.json | 172 +++++++++++++++++++++ packages/toolkit/package.json | 6 +- packages/toolkit/src/exit.ts | 44 ++++++ packages/toolkit/src/toolkit.ts | 56 ++++++- 8 files changed, 348 insertions(+), 8 deletions(-) create mode 100644 .gitignore create mode 100644 packages/toolkit/__tests__/exit.test.ts create mode 100644 packages/toolkit/src/exit.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea4d4ad1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +packages/*/node_modules/ +packages/*/lib/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d4303330..d379065d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1434,6 +1434,15 @@ "integrity": "sha512-/OMMBnjVtDuwX1tg2pkYVSqRIDSmNTnvVvmvP/2xiMAAWf4a5+JozrApCrO4WCAILmXVxfNoQ3E+0HJbNpFVGg==", "dev": true }, + "@types/signale": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/signale/-/signale-1.2.1.tgz", + "integrity": "sha512-mV6s2VgcBC16Jb+1EwulgRrrZBT93V4JCILkNPg31rvvSK6LRQQGU8R/SUivgHjDZ5LJZu/yL2kMF8j85YQTnA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", diff --git a/packages/toolkit/__tests__/exit.test.ts b/packages/toolkit/__tests__/exit.test.ts new file mode 100644 index 00000000..6318f62d --- /dev/null +++ b/packages/toolkit/__tests__/exit.test.ts @@ -0,0 +1,36 @@ +import {Signale} from 'signale' +import {Exit, ExitCode} from '../src/exit' + +describe('Exit', () => { + const tests: [keyof Exit, keyof Signale, ExitCode][] = [ + ['success', 'success', ExitCode.Success], + ['neutral', 'info', ExitCode.Neutral], + ['failure', 'fatal', ExitCode.Failure] + ] + + describe.each(tests)('%s', (method, log, code) => { + let logger: Signale + let exit: Exit + + beforeEach(() => { + // Create a logger to mock + logger = new Signale() + logger.success = jest.fn() + logger.info = jest.fn() + logger.fatal = jest.fn() + + process.exit = jest.fn() + exit = new Exit(logger) + }) + + it('exits with the expected code', () => { + exit[method]() + expect(process.exit).toHaveBeenCalledWith(code) + }) + + it('logs the expected message', () => { + exit[method]('hello') + expect(logger[log]).toHaveBeenCalledWith('hello') + }) + }) +}) diff --git a/packages/toolkit/__tests__/toolkit.test.ts b/packages/toolkit/__tests__/toolkit.test.ts index 5a7b7d96..0ab2d244 100644 --- a/packages/toolkit/__tests__/toolkit.test.ts +++ b/packages/toolkit/__tests__/toolkit.test.ts @@ -1,7 +1,29 @@ -import {github} from '../src/toolkit' +import {Toolkit} from '../src/toolkit' -describe('@actions/toolkit', () => { - it('needs tests', () => { - expect(github()).toBe(true) +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 + }) + + expect(exitFailure).toHaveBeenCalledWith(err) }) }) diff --git a/packages/toolkit/package-lock.json b/packages/toolkit/package-lock.json index d165eaf4..49a8b82c 100644 --- a/packages/toolkit/package-lock.json +++ b/packages/toolkit/package-lock.json @@ -47,6 +47,14 @@ "url-template": "^2.0.8" } }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, "atob-lite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", @@ -62,6 +70,29 @@ "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=" }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -92,6 +123,19 @@ "once": "^1.4.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -106,6 +150,22 @@ "strip-eof": "^1.0.0" } }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -114,6 +174,21 @@ "pump": "^3.0.0" } }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -137,6 +212,31 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -202,11 +302,60 @@ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", + "requires": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -239,11 +388,34 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "signale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", + "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", + "requires": { + "chalk": "^2.3.2", + "figures": "^2.0.0", + "pkg-conf": "^2.1.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, "universal-user-agent": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.0.3.tgz", diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 8abc4bbf..874b01cd 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -31,6 +31,8 @@ "url": "https://github.com/actions/toolkit/issues" }, "dependencies": { - "@octokit/rest": "^16.25.0" - } + "@octokit/rest": "^16.25.0", + "signale": "^1.4.0" + }, + "devDependencies": {} } diff --git a/packages/toolkit/src/exit.ts b/packages/toolkit/src/exit.ts new file mode 100644 index 00000000..4d1d5169 --- /dev/null +++ b/packages/toolkit/src/exit.ts @@ -0,0 +1,44 @@ +import {Signale} from 'signale' + +/** + * The code to exit an action + */ +export enum ExitCode { + Success = 0, + Failure = 1, + Neutral = 78 +} + +// TODO: These exit codes may not behave as expected on the new runtime, due to +// complexities of async logging and sync exiting. + +/** + * A class that wraps some basic methods of exiting from an action + */ +export class Exit { + constructor(private readonly logger: Signale) {} + + /** + * Stop the action with a "success" status. + */ + success(message?: string) { + if (message) this.logger.success(message) + process.exit(ExitCode.Success) + } + + /** + * Stop the action with a "neutral" status. + */ + neutral(message?: string) { + if (message) this.logger.info(message) + process.exit(ExitCode.Neutral) + } + + /** + * Stop the action with a "failed" status. + */ + failure(message?: string) { + if (message) this.logger.fatal(message) + process.exit(ExitCode.Failure) + } +} diff --git a/packages/toolkit/src/toolkit.ts b/packages/toolkit/src/toolkit.ts index a34eacdc..700c921b 100644 --- a/packages/toolkit/src/toolkit.ts +++ b/packages/toolkit/src/toolkit.ts @@ -1,3 +1,55 @@ -export function github() { - return true +import {LoggerFunc, Signale} from 'signale' +import {Exit} from './exit' + +export type ActionFn = (tools: Toolkit) => unknown + +/** + * A set of tools for the Actions runtime + */ +export class Toolkit { + /** + * Run an asynchronous function that accepts a toolkit as its argument. + * + * 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() + + try { + const ret = func(tools) + return ret instanceof Promise ? await ret : ret + } catch (err) { + tools.exit.failure(err) + } + } + + /** + * 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 + } + }) + ) + + /** + * A wrapper around an instance of [[Exit]] + */ + readonly exit: Exit = new Exit(this.logger) + + /** + * Wrap a Signale logger so that its a callable class + */ + private wrapLogger(logger: Signale) { + // Create a callable function + const fn = logger.info.bind(logger) + // Add the log methods onto the function + const wrapped = Object.assign(fn, logger) + // Clone the prototype + Object.setPrototypeOf(wrapped, logger) + return wrapped + } }