diff --git a/packages/helpers/src/command-runner/command-runner.ts b/packages/helpers/src/command-runner/command-runner.ts index ac2120c4..a4622d4a 100644 --- a/packages/helpers/src/command-runner/command-runner.ts +++ b/packages/helpers/src/command-runner/command-runner.ts @@ -1,9 +1,6 @@ import * as exec from '@actions/exec' import {CommandRunnerBase} from './core' import { - ErrorMatcher, - ExitCodeMatcher, - OutputMatcher, failAction, matchEvent, matchExitCode, @@ -16,7 +13,10 @@ import { CommandRunnerActionType, CommandRunnerEventTypeExtended, CommandRunnerMiddleware, - CommandRunnerOptions + CommandRunnerOptions, + ErrorMatcher, + ExitCodeMatcher, + OutputMatcher } from './types' const commandRunnerActions = { diff --git a/packages/helpers/src/command-runner/core.ts b/packages/helpers/src/command-runner/core.ts index b93dd26d..631a80bc 100644 --- a/packages/helpers/src/command-runner/core.ts +++ b/packages/helpers/src/command-runner/core.ts @@ -3,39 +3,12 @@ import {StringDecoder} from 'string_decoder' import { CommandRunnerContext, CommandRunnerMiddleware, - CommandRunnerMiddlewarePromisified, CommandRunnerOptions } from './types' - -export const promisifyCommandRunnerMiddleware = - (middleware: CommandRunnerMiddleware): CommandRunnerMiddlewarePromisified => - async (ctx, next) => { - return Promise.resolve(middleware(ctx, next)) - } - -export const composeCommandRunnerMiddleware = - (middleware: CommandRunnerMiddlewarePromisified[]) => - async (context: CommandRunnerContext, nextGlobal: () => Promise) => { - let index = 0 - - const nextLocal = async (): Promise => { - if (index < middleware.length) { - const currentMiddleware = middleware[index++] - if (middleware === undefined) { - return - } - - await currentMiddleware(context, nextLocal) - } - - await nextGlobal() - } - - await nextLocal() - } +import {PromisifiedFn, promisifyFn} from './utils' export class CommandRunnerBase { - private middleware: CommandRunnerMiddlewarePromisified[] = [] + private middleware: PromisifiedFn[] = [] constructor( private commandLine: string, @@ -45,7 +18,7 @@ export class CommandRunnerBase { ) {} use(middleware: CommandRunnerMiddleware): this { - this.middleware.push(promisifyCommandRunnerMiddleware(middleware)) + this.middleware.push(promisifyFn(middleware)) return this } @@ -104,8 +77,36 @@ export class CommandRunnerBase { } const next = async (): Promise => Promise.resolve() - await composeCommandRunnerMiddleware(this.middleware)(context, next) + await composeMiddleware(this.middleware)(context, next) return context } } + +export function composeMiddleware( + middleware: PromisifiedFn[] +): PromisifiedFn { + middleware = middleware.map(mw => promisifyFn(mw)) + + return async ( + context: CommandRunnerContext, + nextGlobal: () => Promise + ) => { + let index = 0 + + const nextLocal = async (): Promise => { + if (index < middleware.length) { + const currentMiddleware = middleware[index++] + if (middleware === undefined) { + return + } + + await currentMiddleware(context, nextLocal) + } + + await nextGlobal() + } + + await nextLocal() + } +} diff --git a/packages/helpers/src/command-runner/middleware.ts b/packages/helpers/src/command-runner/middleware.ts index 87b27bd0..aa8632ba 100644 --- a/packages/helpers/src/command-runner/middleware.ts +++ b/packages/helpers/src/command-runner/middleware.ts @@ -1,15 +1,16 @@ import * as core from '@actions/core' import { + CommandRunnerAction, CommandRunnerContext, CommandRunnerEventType, CommandRunnerEventTypeExtended, CommandRunnerMiddleware, - CommandRunnerMiddlewarePromisified + ErrorMatcher, + ExitCodeMatcher, + OutputMatcher } from './types' -import { - composeCommandRunnerMiddleware, - promisifyCommandRunnerMiddleware -} from './core' +import {composeMiddleware} from './core' +import {gte, gt, lte, lt, eq, PromisifiedFn} from './utils' const getEventTypesFromContext = ( ctx: CommandRunnerContext @@ -39,16 +40,15 @@ const getEventTypesFromContext = ( return [...eventTypes] } -type CommandRunnerAction = ( - message?: - | string - | ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string) -) => CommandRunnerMiddlewarePromisified - /** * Basic middleware */ +/** Calls next middleware */ +export const passThrough: () => PromisifiedFn = + () => async (_, next) => + next() + /** * Fails Github Action with the given message or with a default one depending on execution conditions. */ @@ -202,31 +202,6 @@ export const produceLog: CommandRunnerAction = message => async (ctx, next) => { next() } -/** - * Filtering middleware - */ - -/** Calls next middleware */ -export const passThrough: () => CommandRunnerMiddlewarePromisified = - () => async (_, next) => - next() - -/** - * Either calls next middleware or not depending on the result of the given condition. - */ -export const filter: ( - shouldPass: - | boolean - | ((ctx: CommandRunnerContext) => boolean | Promise) -) => CommandRunnerMiddlewarePromisified = shouldPass => async (ctx, next) => { - if (typeof shouldPass === 'function') { - if (await shouldPass(ctx)) { - next() - return - } - } -} - /** * Will call passed middleware if matching event has occured. * Will call the next middleware otherwise. @@ -234,13 +209,9 @@ export const filter: ( export const matchEvent = ( eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[], middleware?: CommandRunnerMiddleware[] -): CommandRunnerMiddlewarePromisified => { - if (!middleware?.length) { - middleware = [passThrough()] - } - - const composedMiddleware = composeCommandRunnerMiddleware( - middleware.map(mw => promisifyCommandRunnerMiddleware(mw)) +): PromisifiedFn => { + const composedMiddleware = composeMiddleware( + !middleware?.length ? [passThrough()] : middleware ) const expectedEventsPositiveArray = ( @@ -283,8 +254,6 @@ export const matchEvent = ( } } -export type OutputMatcher = RegExp | string | ((output: string) => boolean) - /** * Will call passed middleware if matching event has occured. * Will call the next middleware otherwise. @@ -292,13 +261,9 @@ export type OutputMatcher = RegExp | string | ((output: string) => boolean) export const matchOutput = ( matcher: OutputMatcher, middleware?: CommandRunnerMiddleware[] -): CommandRunnerMiddlewarePromisified => { - if (!middleware?.length) { - middleware = [passThrough()] - } - - const composedMiddleware = composeCommandRunnerMiddleware( - middleware.map(mw => promisifyCommandRunnerMiddleware(mw)) +): PromisifiedFn => { + const composedMiddleware = composeMiddleware( + !middleware?.length ? [passThrough()] : middleware ) return async (ctx, next) => { @@ -328,30 +293,9 @@ export const matchOutput = ( } } -export type ExitCodeMatcher = string | number +const removeWhitespaces = (str: string): string => str.replace(/\s/g, '') -const lte = - (a: number) => - (b: number): boolean => - b <= a -const gte = - (a: number) => - (b: number): boolean => - b >= a -const lt = - (a: number) => - (b: number): boolean => - b < a -const gt = - (a: number) => - (b: number): boolean => - b > a -const eq = - (a: number) => - (b: number): boolean => - b === a - -const matchers = { +const MATCHERS = { '>=': gte, '>': gt, '<=': lte, @@ -359,11 +303,9 @@ const matchers = { '=': eq } as const -const removeWhitespaces = (str: string): string => str.replace(/\s/g, '') - const parseExitCodeMatcher = ( code: ExitCodeMatcher -): [keyof typeof matchers, number] => { +): [keyof typeof MATCHERS, number] => { if (typeof code === 'number') { return ['=', code] } @@ -382,14 +324,7 @@ const parseExitCodeMatcher = ( } const [, operator, number] = match - return [operator as keyof typeof matchers, parseInt(number)] -} - -const matcherToMatcherFn = ( - matcher: ExitCodeMatcher -): ((exitCode: number) => boolean) => { - const [operator, number] = parseExitCodeMatcher(matcher) - return matchers[operator](number) + return [operator as keyof typeof MATCHERS, parseInt(number)] } /** @@ -399,20 +334,17 @@ const matcherToMatcherFn = ( export const matchExitCode = ( code: ExitCodeMatcher, middleware?: CommandRunnerMiddleware[] -): CommandRunnerMiddlewarePromisified => { - const matcher = matcherToMatcherFn(code) +): PromisifiedFn => { + const [operator, number] = parseExitCodeMatcher(code) + const matcherFn = MATCHERS[operator](number) - if (!middleware?.length) { - middleware = [passThrough()] - } - - const composedMiddleware = composeCommandRunnerMiddleware( - middleware.map(mw => promisifyCommandRunnerMiddleware(mw)) + const composedMiddleware = composeMiddleware( + !middleware?.length ? [passThrough()] : middleware ) return async (ctx, next) => { // if exit code is undefined, NaN will not match anything - if (matcher(ctx.exitCode ?? NaN)) { + if (matcherFn(ctx.exitCode ?? NaN)) { composedMiddleware(ctx, next) return } @@ -421,25 +353,14 @@ export const matchExitCode = ( } } -export type ErrorMatcher = - | RegExp - | string - | ((error: { - type: 'stderr' | 'execerr' - error: Error | null - message: string - }) => boolean) - export const matchSpecificError = ( matcher: ErrorMatcher, - middleware?: CommandRunnerMiddleware[] -): CommandRunnerMiddlewarePromisified => { - if (!middleware?.length) { - middleware = [passThrough()] - } - - const composedMiddleware = composeCommandRunnerMiddleware( - middleware.map(mw => promisifyCommandRunnerMiddleware(mw)) + middleware?: + | CommandRunnerMiddleware[] + | PromisifiedFn[] +): PromisifiedFn => { + const composedMiddleware = composeMiddleware( + !middleware?.length ? [passThrough()] : middleware ) return async (ctx, next) => { diff --git a/packages/helpers/src/command-runner/types.ts b/packages/helpers/src/command-runner/types.ts index 12e0eb7f..bb4a987d 100644 --- a/packages/helpers/src/command-runner/types.ts +++ b/packages/helpers/src/command-runner/types.ts @@ -1,4 +1,5 @@ import * as exec from '@actions/exec' +import {PromisifiedFn} from './utils' /* CommandRunner core */ @@ -15,20 +16,22 @@ export interface CommandRunnerContext { exitCode: number | null } -/* Middlewares as used internally in CommandRunner */ -export type CommandRunnerMiddlewarePromisified = ( - ctx: CommandRunnerContext, - next: () => Promise -) => Promise - /* Middlewares as used by the user */ -export type CommandRunnerMiddleware = ( +type _CommandRunnerMiddleware = ( ctx: CommandRunnerContext, next: () => Promise ) => void | Promise +export type CommandRunnerMiddleware = PromisifiedFn<_CommandRunnerMiddleware> + /* Command runner events handling and command runner actions */ +export type CommandRunnerAction = ( + message?: + | string + | ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string) +) => PromisifiedFn + /* Command runner default actions types on which preset middleware exists */ export type CommandRunnerActionType = 'throw' | 'fail' | 'log' @@ -49,3 +52,18 @@ export type CommandRunnerOptions = Omit< exec.ExecOptions, 'failOnStdErr' | 'ignoreReturnCode' > + +/* Matchers */ + +export type OutputMatcher = RegExp | string | ((output: string) => boolean) + +export type ExitCodeMatcher = string | number + +export type ErrorMatcher = + | RegExp + | string + | ((error: { + type: 'stderr' | 'execerr' + error: Error | null + message: string + }) => boolean) diff --git a/packages/helpers/src/command-runner/utils.ts b/packages/helpers/src/command-runner/utils.ts new file mode 100644 index 00000000..279f5c49 --- /dev/null +++ b/packages/helpers/src/command-runner/utils.ts @@ -0,0 +1,52 @@ +/** + * Promises + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type PromisifiedFn any> = ( + ...args: Parameters +) => ReturnType extends Promise + ? ReturnType + : Promise> + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const promisifyFn = any>( + fn: T +): PromisifiedFn => { + const result = async (...args: Parameters): Promise => { + return new Promise((resolve, reject) => { + try { + resolve(fn(...args)) + } catch (error) { + reject(error) + } + }) + } + + return result as PromisifiedFn +} + +/** + * Comparators + */ + +export const lte = + (a: number) => + (b: number): boolean => + b <= a +export const gte = + (a: number) => + (b: number): boolean => + b >= a +export const lt = + (a: number) => + (b: number): boolean => + b < a +export const gt = + (a: number) => + (b: number): boolean => + b > a +export const eq = + (a: number) => + (b: number): boolean => + b === a