mirror of https://github.com/actions/toolkit
Rearrange code and add comprehensive comments
parent
b7fcb99778
commit
bfa86cc586
|
@ -1,6 +1,9 @@
|
||||||
import * as exec from '../exec'
|
import * as exec from '../exec'
|
||||||
import {CommandRunnerBase} from './core'
|
import {CommandRunnerBase} from './core'
|
||||||
import {
|
import {
|
||||||
|
ErrorMatcher,
|
||||||
|
ExitCodeMatcher,
|
||||||
|
OutputMatcher,
|
||||||
failAction,
|
failAction,
|
||||||
matchEvent,
|
matchEvent,
|
||||||
matchExitCode,
|
matchExitCode,
|
||||||
|
@ -8,15 +11,12 @@ import {
|
||||||
matchSpecificError,
|
matchSpecificError,
|
||||||
produceLog,
|
produceLog,
|
||||||
throwError
|
throwError
|
||||||
} from './middleware'
|
} from './middlware'
|
||||||
import {
|
import {
|
||||||
CommandRunnerActionType,
|
CommandRunnerActionType,
|
||||||
CommandRunnerEventTypeExtended,
|
CommandRunnerEventTypeExtended,
|
||||||
CommandRunnerMiddleware,
|
CommandRunnerMiddleware,
|
||||||
CommandRunnerOptions,
|
CommandRunnerOptions
|
||||||
ErrorMatcher,
|
|
||||||
ExitCodeMatcher,
|
|
||||||
OutputMatcher
|
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
const commandRunnerActions = {
|
const commandRunnerActions = {
|
||||||
|
@ -26,6 +26,30 @@ const commandRunnerActions = {
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export class CommandRunner extends CommandRunnerBase {
|
export class CommandRunner extends CommandRunnerBase {
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed on command runner run
|
||||||
|
* @param event allows to set middleware on certain event
|
||||||
|
* - `execerr` - when error happens during command execution
|
||||||
|
* - `stderr` - when stderr is not empty
|
||||||
|
* - `stdout` - when stdout is not empty
|
||||||
|
* - `exitcode` - when exit code is not 0
|
||||||
|
* - `ok` - when exit code is 0 and stderr is empty
|
||||||
|
* Each event can also be negated by prepending `!` to it, e.g. `!ok`
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .on('ok', 'log', 'Command executed successfully')
|
||||||
|
* .on('!ok', 'throw')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
on(
|
on(
|
||||||
event: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
|
event: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
|
@ -40,6 +64,23 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command executed
|
||||||
|
* with empty stdout.
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .onEmptyOutput('throw', 'Command did not produce an output')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onEmptyOutput(
|
onEmptyOutput(
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
message?: string
|
message?: string
|
||||||
|
@ -48,6 +89,23 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command failed
|
||||||
|
* to execute (either did not find such command or failed to spawn it).
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .onExecutionError('throw', 'Command failed to execute')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onExecutionError(
|
onExecutionError(
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
message?: string
|
message?: string
|
||||||
|
@ -62,6 +120,23 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command produced
|
||||||
|
* non-empty stderr.
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .onStdError('throw', 'Command produced an error')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onStdError(
|
onStdError(
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
message?: string
|
message?: string
|
||||||
|
@ -76,6 +151,23 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command produced
|
||||||
|
* non-empty stderr or failed to execute (either did not find such command or failed to spawn it).
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .onError('throw', 'Command produced an error or failed to execute')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onError(
|
onError(
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
message?: string
|
message?: string
|
||||||
|
@ -83,6 +175,28 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this.on(['execerr', 'stderr'], action, message)
|
return this.on(['execerr', 'stderr'], action, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command produced
|
||||||
|
* an error that matches provided matcher.
|
||||||
|
* @param matcher allows to match specific error, can be either a string (to match error message exactly),
|
||||||
|
* a regular expression (to match error message with it) or a function (to match error object with it)
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* await createCommandRunner()
|
||||||
|
* .setCommand('curl')
|
||||||
|
* .setArgs(['-f', 'http://example.com/'])
|
||||||
|
* .onSpecificError('Failed to connect to example.com port 80: Connection refused', 'throw', 'Failed to connect to example.com')
|
||||||
|
* .onSpecificError(/429/, log, 'Too many requests, retrying in 4 seconds')
|
||||||
|
* .onSpecificError(/429/, () => retryIn(4000))
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onSpecificError(
|
onSpecificError(
|
||||||
matcher: ErrorMatcher,
|
matcher: ErrorMatcher,
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
|
@ -98,6 +212,23 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command produced
|
||||||
|
* zero exit code and empty stderr.
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .onSuccess('log', 'Command executed successfully')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onSuccess(
|
onSuccess(
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
message?: string
|
message?: string
|
||||||
|
@ -105,6 +236,27 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this.on('ok', action, message)
|
return this.on('ok', action, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command produced an
|
||||||
|
* exit code that matches provided matcher.
|
||||||
|
* @param matcher allows to match specific exit code, can be either a number (to match exit code exactly)
|
||||||
|
* or a string to match exit code against operator and number, e.g. `'>= 0'`
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from event type)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from event type)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* await createCommandRunner()
|
||||||
|
* .setCommand('curl')
|
||||||
|
* .setArgs(['-f', 'http://example.com/'])
|
||||||
|
* .onExitCode(0, 'log', 'Command executed successfully')
|
||||||
|
* .onExitCode('>= 400', 'throw', 'Command failed to execute')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onExitCode(
|
onExitCode(
|
||||||
matcher: ExitCodeMatcher,
|
matcher: ExitCodeMatcher,
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
|
@ -120,6 +272,27 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets middleware (default or custom) to be executed when command produced
|
||||||
|
* the stdout that matches provided matcher.
|
||||||
|
* @param matcher allows to match specific stdout, can be either a string (to match stdout exactly),
|
||||||
|
* a regular expression (to match stdout with it) or a function (to match stdout with it)
|
||||||
|
* @param action allows to set action to be executed on event, it can be
|
||||||
|
* either default action (passed as string) or a custom middleware, default
|
||||||
|
* actions are:
|
||||||
|
* - `throw` - throws an error with message passed as second argument or a default one (inferred from matcher)
|
||||||
|
* - `fail` - fails the command with message passed as second argument or a default one (inferred from matcher)
|
||||||
|
* - `log` - logs the message passed as second argument or a default one (inferred from matcher)
|
||||||
|
* @param message optional message to be passed to action, is not relevant when action is a custom middleware
|
||||||
|
* @example ```typescript
|
||||||
|
* const runner = createCommandRunner('echo', ['hello'])
|
||||||
|
* await runner
|
||||||
|
* .onOutput('hello', 'log', 'Command executed successfully')
|
||||||
|
* .onOutput(/hello\S+/, 'log', 'What?')
|
||||||
|
* .onOutput(stdout => stdout.includes('world'), 'log', 'Huh')
|
||||||
|
* .run()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
onOutput(
|
onOutput(
|
||||||
matcher: OutputMatcher,
|
matcher: OutputMatcher,
|
||||||
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
action: CommandRunnerActionType | CommandRunnerMiddleware,
|
||||||
|
@ -136,6 +309,13 @@ export class CommandRunner extends CommandRunnerBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a command runner with provided command line, arguments and options
|
||||||
|
* @param commandLine command line to execute
|
||||||
|
* @param args arguments to pass to command
|
||||||
|
* @param options options to pass to command executor
|
||||||
|
* @returns command runner instance
|
||||||
|
*/
|
||||||
export const createCommandRunner = (
|
export const createCommandRunner = (
|
||||||
commandLine = '',
|
commandLine = '',
|
||||||
args: string[] = [],
|
args: string[] = [],
|
||||||
|
|
|
@ -17,6 +17,10 @@ export class CommandRunnerBase {
|
||||||
private executor: typeof exec.exec = exec.exec
|
private executor: typeof exec.exec = exec.exec
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets command to be executed, passing a callback
|
||||||
|
* allows to modify command based on currently set command
|
||||||
|
*/
|
||||||
setCommand(commandLine: string | ((commandLine: string) => string)): this {
|
setCommand(commandLine: string | ((commandLine: string) => string)): this {
|
||||||
this.commandLine =
|
this.commandLine =
|
||||||
typeof commandLine === 'function'
|
typeof commandLine === 'function'
|
||||||
|
@ -26,6 +30,10 @@ export class CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets command arguments, passing a callback
|
||||||
|
* allows to modify arguments based on currently set arguments
|
||||||
|
*/
|
||||||
setArgs(args: string[] | ((args: string[]) => string[])): this {
|
setArgs(args: string[] | ((args: string[]) => string[])): this {
|
||||||
this.args =
|
this.args =
|
||||||
typeof args === 'function' ? args(this.args) : [...this.args, ...args]
|
typeof args === 'function' ? args(this.args) : [...this.args, ...args]
|
||||||
|
@ -33,6 +41,10 @@ export class CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets options for command executor (exec.exec by default), passing a callback
|
||||||
|
* allows to modify options based on currently set options
|
||||||
|
*/
|
||||||
setOptions(
|
setOptions(
|
||||||
options:
|
options:
|
||||||
| CommandRunnerOptions
|
| CommandRunnerOptions
|
||||||
|
@ -44,11 +56,37 @@ export class CommandRunnerBase {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets arbitrary middleware to be executed on command runner run
|
||||||
|
* middleware is executed in the order it was added
|
||||||
|
* @param middleware middleware to be executed
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const runner = new CommandRunner()
|
||||||
|
* runner.use(async (ctx, next) => {
|
||||||
|
* console.log('before')
|
||||||
|
* const {
|
||||||
|
* exitCode // exit code of the command
|
||||||
|
* stdout // stdout of the command
|
||||||
|
* stderr // stderr of the command
|
||||||
|
* execerr // error thrown by the command executor
|
||||||
|
* commandLine // command line that was executed
|
||||||
|
* args // arguments that were passed to the command
|
||||||
|
* options // options that were passed to the command
|
||||||
|
* } = ctx
|
||||||
|
* await next()
|
||||||
|
* console.log('after')
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
use(middleware: CommandRunnerMiddleware): this {
|
use(middleware: CommandRunnerMiddleware): this {
|
||||||
this.middleware.push(promisifyFn(middleware))
|
this.middleware.push(promisifyFn(middleware))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs command with currently set options and arguments
|
||||||
|
*/
|
||||||
async run(
|
async run(
|
||||||
/* overrides command for this specific execution if not undefined */
|
/* overrides command for this specific execution if not undefined */
|
||||||
commandLine?: string,
|
commandLine?: string,
|
||||||
|
@ -117,9 +155,21 @@ export class CommandRunnerBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes multiple middleware into a single middleware
|
||||||
|
* implements a chain of responsibility pattern
|
||||||
|
* with next function passed to each middleware
|
||||||
|
* and each middleware being able to call next() to pass control to the next middleware
|
||||||
|
* or not call next() to stop the chain,
|
||||||
|
* it is also possible to run code after the next was called by using async/await
|
||||||
|
* for a cleanup or other purposes.
|
||||||
|
* This behavior is mostly implemented to be similar to express, koa or other middleware based frameworks
|
||||||
|
* in order to avoid confusion. Executing code after next() usually would not be needed.
|
||||||
|
*/
|
||||||
export function composeMiddleware(
|
export function composeMiddleware(
|
||||||
middleware: CommandRunnerMiddleware[]
|
middleware: CommandRunnerMiddleware[]
|
||||||
): PromisifiedFn<CommandRunnerMiddleware> {
|
): PromisifiedFn<CommandRunnerMiddleware> {
|
||||||
|
// promisify all passed middleware
|
||||||
middleware = middleware.map(mw => promisifyFn(mw))
|
middleware = middleware.map(mw => promisifyFn(mw))
|
||||||
|
|
||||||
return async (
|
return async (
|
||||||
|
@ -128,6 +178,12 @@ export function composeMiddleware(
|
||||||
) => {
|
) => {
|
||||||
let index = 0
|
let index = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks the first not-yet-executed middleware from the list and
|
||||||
|
* runs it, passing itself as next function for
|
||||||
|
* that middleware to call, therefore would be called
|
||||||
|
* by each middleware in the chain
|
||||||
|
*/
|
||||||
const nextLocal = async (): Promise<void> => {
|
const nextLocal = async (): Promise<void> => {
|
||||||
if (index < middleware.length) {
|
if (index < middleware.length) {
|
||||||
const currentMiddleware = middleware[index++]
|
const currentMiddleware = middleware[index++]
|
||||||
|
@ -138,9 +194,18 @@ export function composeMiddleware(
|
||||||
await currentMiddleware(context, nextLocal)
|
await currentMiddleware(context, nextLocal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no middlware left to be executed
|
||||||
|
* will call the next funtion passed to the
|
||||||
|
* composed middleware
|
||||||
|
*/
|
||||||
await nextGlobal()
|
await nextGlobal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the chain of middleware execution by
|
||||||
|
* calling nextLocal directly
|
||||||
|
*/
|
||||||
await nextLocal()
|
await nextLocal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import {CommandRunnerContext, CommandRunnerEventType} from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of already computed events for context
|
||||||
|
* to avoid recomputing them
|
||||||
|
*/
|
||||||
|
let contextEvents: WeakMap<
|
||||||
|
CommandRunnerContext,
|
||||||
|
CommandRunnerEventType[]
|
||||||
|
> | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns event types that were triggered by the command execution
|
||||||
|
*/
|
||||||
|
export const getEvents = (
|
||||||
|
ctx: CommandRunnerContext
|
||||||
|
): CommandRunnerEventType[] => {
|
||||||
|
const existingEvents = contextEvents?.get(ctx)
|
||||||
|
|
||||||
|
if (existingEvents) {
|
||||||
|
return existingEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTypes = new Set<CommandRunnerEventType>()
|
||||||
|
|
||||||
|
if (ctx.execerr) {
|
||||||
|
eventTypes.add('execerr')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.stderr) {
|
||||||
|
eventTypes.add('stderr')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.exitCode) {
|
||||||
|
eventTypes.add('exitcode')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.stdout) {
|
||||||
|
eventTypes.add('stdout')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.stderr && !ctx.execerr && ctx.stdout !== null && !ctx.exitCode) {
|
||||||
|
eventTypes.add('ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [...eventTypes]
|
||||||
|
|
||||||
|
if (!contextEvents) {
|
||||||
|
contextEvents = new WeakMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
contextEvents.set(ctx, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -1,401 +0,0 @@
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {
|
|
||||||
CommandRunnerAction,
|
|
||||||
CommandRunnerContext,
|
|
||||||
CommandRunnerEventType,
|
|
||||||
CommandRunnerEventTypeExtended,
|
|
||||||
CommandRunnerMiddleware,
|
|
||||||
ErrorMatcher,
|
|
||||||
ExitCodeMatcher,
|
|
||||||
OutputMatcher
|
|
||||||
} from './types'
|
|
||||||
import {composeMiddleware} from './core'
|
|
||||||
import {gte, gt, lte, lt, eq, PromisifiedFn} from './utils'
|
|
||||||
|
|
||||||
const getEventTypesFromContext = (
|
|
||||||
ctx: CommandRunnerContext
|
|
||||||
): CommandRunnerEventType[] => {
|
|
||||||
const eventTypes = new Set<CommandRunnerEventType>()
|
|
||||||
|
|
||||||
if (ctx.execerr) {
|
|
||||||
eventTypes.add('execerr')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.stderr) {
|
|
||||||
eventTypes.add('stderr')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.exitCode) {
|
|
||||||
eventTypes.add('exitcode')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.stdout) {
|
|
||||||
eventTypes.add('stdout')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.stderr && !ctx.execerr && ctx.stdout !== null && !ctx.exitCode) {
|
|
||||||
eventTypes.add('ok')
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...eventTypes]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic middleware
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Calls next middleware */
|
|
||||||
export const passThrough: () => PromisifiedFn<CommandRunnerMiddleware> =
|
|
||||||
() => async (_, next) =>
|
|
||||||
next()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fails Github Action with the given message or with a default one depending on execution conditions.
|
|
||||||
*/
|
|
||||||
export const failAction: CommandRunnerAction = message => async ctx => {
|
|
||||||
const events = getEventTypesFromContext(ctx)
|
|
||||||
|
|
||||||
if (message !== undefined) {
|
|
||||||
core.setFailed(typeof message === 'string' ? message : message(ctx, events))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('execerr')) {
|
|
||||||
core.setFailed(
|
|
||||||
`The command "${ctx.commandLine}" failed to run: ${ctx.execerr?.message}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('stderr')) {
|
|
||||||
core.setFailed(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an error: ${ctx.stderr}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!events.includes('stdout')) {
|
|
||||||
core.setFailed(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.setFailed(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws an error with the given message or with a default one depending on execution conditions.
|
|
||||||
*/
|
|
||||||
export const throwError: CommandRunnerAction = message => {
|
|
||||||
return async ctx => {
|
|
||||||
const events = getEventTypesFromContext(ctx)
|
|
||||||
|
|
||||||
if (message !== undefined) {
|
|
||||||
throw new Error(
|
|
||||||
typeof message === 'string' ? message : message(ctx, events)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('execerr')) {
|
|
||||||
throw new Error(
|
|
||||||
`The command "${ctx.commandLine}" failed to run: ${ctx.execerr?.message}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('stderr')) {
|
|
||||||
throw new Error(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an error: ${ctx.stderr}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!events.includes('stdout')) {
|
|
||||||
throw new Error(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs a message with the given message or with a default one depending on execution conditions.
|
|
||||||
*/
|
|
||||||
export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
|
|
||||||
const events = getEventTypesFromContext(ctx)
|
|
||||||
|
|
||||||
if (message !== undefined) {
|
|
||||||
// core.info(typeof message === 'string' ? message : message(ctx, []))
|
|
||||||
const messageText =
|
|
||||||
typeof message === 'string' ? message : message(ctx, events)
|
|
||||||
|
|
||||||
if (events.includes('execerr')) {
|
|
||||||
core.error(messageText)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('stderr')) {
|
|
||||||
core.error(messageText)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!events.includes('stdout')) {
|
|
||||||
core.warning(messageText)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('ok')) {
|
|
||||||
core.notice(messageText)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(messageText)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('execerr')) {
|
|
||||||
core.error(
|
|
||||||
`The command "${ctx.commandLine}" failed to run: ${ctx.execerr?.message}`
|
|
||||||
)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('stderr')) {
|
|
||||||
core.error(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an error: ${ctx.stderr}`
|
|
||||||
)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!events.includes('stdout')) {
|
|
||||||
core.warning(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
|
|
||||||
)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.includes('ok')) {
|
|
||||||
core.notice(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
|
||||||
)
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(
|
|
||||||
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
|
||||||
)
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will call passed middleware if matching event has occured.
|
|
||||||
* Will call the next middleware otherwise.
|
|
||||||
*/
|
|
||||||
export const matchEvent = (
|
|
||||||
eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
|
|
||||||
middleware?: CommandRunnerMiddleware[]
|
|
||||||
): PromisifiedFn<CommandRunnerMiddleware> => {
|
|
||||||
const composedMiddleware = composeMiddleware(
|
|
||||||
!middleware?.length ? [passThrough()] : middleware
|
|
||||||
)
|
|
||||||
|
|
||||||
const expectedEventsPositiveArray = (
|
|
||||||
Array.isArray(eventType) ? eventType : [eventType]
|
|
||||||
).filter(e => !e.startsWith('!')) as CommandRunnerEventType[]
|
|
||||||
|
|
||||||
const expectedEventsNegativeArray = (
|
|
||||||
Array.isArray(eventType) ? eventType : [eventType]
|
|
||||||
)
|
|
||||||
.filter(e => e.startsWith('!'))
|
|
||||||
.map(e => e.slice(1)) as CommandRunnerEventType[]
|
|
||||||
|
|
||||||
const expectedEventsPositive = new Set(expectedEventsPositiveArray)
|
|
||||||
const expectedEventsNegative = new Set(expectedEventsNegativeArray)
|
|
||||||
|
|
||||||
return async (ctx, next) => {
|
|
||||||
const currentEvents = getEventTypesFromContext(ctx)
|
|
||||||
let shouldRun = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
expectedEventsPositive.size &&
|
|
||||||
currentEvents.some(e => expectedEventsPositive.has(e))
|
|
||||||
) {
|
|
||||||
shouldRun = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
expectedEventsNegative.size &&
|
|
||||||
currentEvents.every(e => !expectedEventsNegative.has(e))
|
|
||||||
) {
|
|
||||||
shouldRun = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRun) {
|
|
||||||
await composedMiddleware(ctx, next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will call passed middleware if matching event has occured.
|
|
||||||
* Will call the next middleware otherwise.
|
|
||||||
*/
|
|
||||||
export const matchOutput = (
|
|
||||||
matcher: OutputMatcher,
|
|
||||||
middleware?: CommandRunnerMiddleware[]
|
|
||||||
): PromisifiedFn<CommandRunnerMiddleware> => {
|
|
||||||
const composedMiddleware = composeMiddleware(
|
|
||||||
!middleware?.length ? [passThrough()] : middleware
|
|
||||||
)
|
|
||||||
|
|
||||||
return async (ctx, next) => {
|
|
||||||
const output = ctx.stdout
|
|
||||||
|
|
||||||
if (output === null) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof matcher === 'function' && !matcher(output)) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof matcher === 'string' && output !== matcher) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matcher instanceof RegExp && !matcher.test(output)) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await composedMiddleware(ctx, next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
|
|
||||||
|
|
||||||
const MATCHERS = {
|
|
||||||
'>=': gte,
|
|
||||||
'>': gt,
|
|
||||||
'<=': lte,
|
|
||||||
'<': lt,
|
|
||||||
'=': eq
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const parseExitCodeMatcher = (
|
|
||||||
code: ExitCodeMatcher
|
|
||||||
): [keyof typeof MATCHERS, number] => {
|
|
||||||
if (typeof code === 'number') {
|
|
||||||
return ['=', code]
|
|
||||||
}
|
|
||||||
|
|
||||||
code = removeWhitespaces(code)
|
|
||||||
|
|
||||||
// just shortcuts for the most common cases
|
|
||||||
if (code.startsWith('=')) return ['=', Number(code.slice(1))]
|
|
||||||
if (code === '>0') return ['>', 0]
|
|
||||||
if (code === '<1') return ['<', 1]
|
|
||||||
|
|
||||||
const match = code.match(/^([><]=?)(\d+)$/)
|
|
||||||
|
|
||||||
if (match === null) {
|
|
||||||
throw new Error(`Invalid exit code matcher: ${code}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, operator, number] = match
|
|
||||||
return [operator as keyof typeof MATCHERS, parseInt(number)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will call passed middleware if matching exit code was returned.
|
|
||||||
* Will call the next middleware otherwise.
|
|
||||||
*/
|
|
||||||
export const matchExitCode = (
|
|
||||||
code: ExitCodeMatcher,
|
|
||||||
middleware?: CommandRunnerMiddleware[]
|
|
||||||
): PromisifiedFn<CommandRunnerMiddleware> => {
|
|
||||||
const [operator, number] = parseExitCodeMatcher(code)
|
|
||||||
const matcherFn = MATCHERS[operator](number)
|
|
||||||
|
|
||||||
const composedMiddleware = composeMiddleware(
|
|
||||||
!middleware?.length ? [passThrough()] : middleware
|
|
||||||
)
|
|
||||||
|
|
||||||
return async (ctx, next) => {
|
|
||||||
// if exit code is undefined, NaN will not match anything
|
|
||||||
if (matcherFn(ctx.exitCode ?? NaN)) {
|
|
||||||
await composedMiddleware(ctx, next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const matchSpecificError = (
|
|
||||||
matcher: ErrorMatcher,
|
|
||||||
middleware?:
|
|
||||||
| CommandRunnerMiddleware[]
|
|
||||||
| PromisifiedFn<CommandRunnerMiddleware>[]
|
|
||||||
): PromisifiedFn<CommandRunnerMiddleware> => {
|
|
||||||
const composedMiddleware = composeMiddleware(
|
|
||||||
!middleware?.length ? [passThrough()] : middleware
|
|
||||||
)
|
|
||||||
|
|
||||||
return async (ctx, next) => {
|
|
||||||
if (ctx.execerr === null && ctx.stderr === null) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const error: {
|
|
||||||
type: 'stderr' | 'execerr'
|
|
||||||
error: Error | null
|
|
||||||
message: string
|
|
||||||
} = {
|
|
||||||
type: ctx.execerr ? 'execerr' : 'stderr',
|
|
||||||
error: ctx.execerr ? ctx.execerr : null,
|
|
||||||
message: ctx.execerr ? ctx.execerr.message : ctx.stderr ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof matcher === 'function' && !matcher(error)) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof matcher === 'string' && error.message !== matcher) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matcher instanceof RegExp && !matcher.test(error.message)) {
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await composedMiddleware(ctx, next)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {CommandRunnerAction} from '../types'
|
||||||
|
import {getEvents} from '../get-events'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fails Github Action with the given message or with a default one depending on execution conditions.
|
||||||
|
*/
|
||||||
|
export const failAction: CommandRunnerAction = message => async ctx => {
|
||||||
|
const events = getEvents(ctx)
|
||||||
|
|
||||||
|
if (message !== undefined) {
|
||||||
|
core.setFailed(typeof message === 'string' ? message : message(ctx, events))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('execerr')) {
|
||||||
|
core.setFailed(
|
||||||
|
`The command "${ctx.commandLine}" failed to run: ${ctx.execerr?.message}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('stderr')) {
|
||||||
|
core.setFailed(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an error: ${ctx.stderr}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.includes('stdout')) {
|
||||||
|
core.setFailed(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setFailed(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an error with the given message or with a default one depending on execution conditions.
|
||||||
|
*/
|
||||||
|
export const throwError: CommandRunnerAction = message => {
|
||||||
|
return async ctx => {
|
||||||
|
const events = getEvents(ctx)
|
||||||
|
|
||||||
|
if (message !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
typeof message === 'string' ? message : message(ctx, events)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('execerr')) {
|
||||||
|
throw new Error(
|
||||||
|
`The command "${ctx.commandLine}" failed to run: ${ctx.execerr?.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('stderr')) {
|
||||||
|
throw new Error(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an error: ${ctx.stderr}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.includes('stdout')) {
|
||||||
|
throw new Error(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message with the given message or with a default one depending on execution conditions.
|
||||||
|
*/
|
||||||
|
export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
|
||||||
|
const events = getEvents(ctx)
|
||||||
|
|
||||||
|
if (message !== undefined) {
|
||||||
|
// core.info(typeof message === 'string' ? message : message(ctx, []))
|
||||||
|
const messageText =
|
||||||
|
typeof message === 'string' ? message : message(ctx, events)
|
||||||
|
|
||||||
|
if (events.includes('execerr')) {
|
||||||
|
core.error(messageText)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('stderr')) {
|
||||||
|
core.error(messageText)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.includes('stdout')) {
|
||||||
|
core.warning(messageText)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('ok')) {
|
||||||
|
core.notice(messageText)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(messageText)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('execerr')) {
|
||||||
|
core.error(
|
||||||
|
`The command "${ctx.commandLine}" failed to run: ${ctx.execerr?.message}`
|
||||||
|
)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('stderr')) {
|
||||||
|
core.error(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an error: ${ctx.stderr}`
|
||||||
|
)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.includes('stdout')) {
|
||||||
|
core.warning(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
|
||||||
|
)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.includes('ok')) {
|
||||||
|
core.notice(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
||||||
|
)
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced the following output: ${ctx.stdout}`
|
||||||
|
)
|
||||||
|
next()
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './action-middleware'
|
||||||
|
export * from './match-error'
|
||||||
|
export * from './match-event'
|
||||||
|
export * from './match-exitcode'
|
||||||
|
export * from './match-output'
|
||||||
|
export * from './pass-through'
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {composeMiddleware} from '../core'
|
||||||
|
import {passThrough} from './pass-through'
|
||||||
|
import {CommandRunnerMiddleware} from '../types'
|
||||||
|
import {PromisifiedFn} from '../utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher types that are available to user to match error against
|
||||||
|
* and set middleware on
|
||||||
|
*/
|
||||||
|
export type ErrorMatcher =
|
||||||
|
| RegExp
|
||||||
|
| string
|
||||||
|
| ((error: {
|
||||||
|
type: 'stderr' | 'execerr'
|
||||||
|
error: Error | null
|
||||||
|
message: string
|
||||||
|
}) => boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call passed middleware if matching error has occured.
|
||||||
|
* If matching error has occured will call passed middleware. Will call the next middleware otherwise.
|
||||||
|
*/
|
||||||
|
export const matchSpecificError = (
|
||||||
|
matcher: ErrorMatcher,
|
||||||
|
middleware?:
|
||||||
|
| CommandRunnerMiddleware[]
|
||||||
|
| PromisifiedFn<CommandRunnerMiddleware>[]
|
||||||
|
): PromisifiedFn<CommandRunnerMiddleware> => {
|
||||||
|
/**
|
||||||
|
* Composes passed middleware if any or replaces them
|
||||||
|
* with passThrough middleware if none were passed
|
||||||
|
* to avoid errors when calling composed middleware
|
||||||
|
*/
|
||||||
|
const composedMiddleware = composeMiddleware(
|
||||||
|
!middleware?.length ? [passThrough()] : middleware
|
||||||
|
)
|
||||||
|
|
||||||
|
return async (ctx, next) => {
|
||||||
|
if (ctx.execerr === null && ctx.stderr === null) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const error: {
|
||||||
|
type: 'stderr' | 'execerr'
|
||||||
|
error: Error | null
|
||||||
|
message: string
|
||||||
|
} = {
|
||||||
|
type: ctx.execerr ? 'execerr' : 'stderr',
|
||||||
|
error: ctx.execerr ? ctx.execerr : null,
|
||||||
|
message: ctx.execerr ? ctx.execerr.message : ctx.stderr ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof matcher === 'function' && !matcher(error)) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof matcher === 'string' && error.message !== matcher) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher instanceof RegExp && !matcher.test(error.message)) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await composedMiddleware(ctx, next)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {composeMiddleware} from '../core'
|
||||||
|
import {getEvents} from '../get-events'
|
||||||
|
import {
|
||||||
|
CommandRunnerEventTypeExtended,
|
||||||
|
CommandRunnerMiddleware,
|
||||||
|
CommandRunnerEventType
|
||||||
|
} from '../types'
|
||||||
|
import {PromisifiedFn} from '../utils'
|
||||||
|
import {passThrough} from './pass-through'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call passed middleware if matching event has occured.
|
||||||
|
* Will call the next middleware otherwise.
|
||||||
|
*/
|
||||||
|
export const matchEvent = (
|
||||||
|
eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
|
||||||
|
middleware?: CommandRunnerMiddleware[]
|
||||||
|
): PromisifiedFn<CommandRunnerMiddleware> => {
|
||||||
|
/**
|
||||||
|
* Composes passed middleware if any or replaces them
|
||||||
|
* with passThrough middleware if none were passed
|
||||||
|
* to avoid errors when calling composed middleware
|
||||||
|
*/
|
||||||
|
const composedMiddleware = composeMiddleware(
|
||||||
|
!middleware?.length ? [passThrough()] : middleware
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedEventsPositiveArray = (
|
||||||
|
Array.isArray(eventType) ? eventType : [eventType]
|
||||||
|
).filter(e => !e.startsWith('!')) as CommandRunnerEventType[]
|
||||||
|
|
||||||
|
const expectedEventsNegativeArray = (
|
||||||
|
Array.isArray(eventType) ? eventType : [eventType]
|
||||||
|
)
|
||||||
|
.filter(e => e.startsWith('!'))
|
||||||
|
.map(e => e.slice(1)) as CommandRunnerEventType[]
|
||||||
|
|
||||||
|
const expectedEventsPositive = new Set(expectedEventsPositiveArray)
|
||||||
|
const expectedEventsNegative = new Set(expectedEventsNegativeArray)
|
||||||
|
|
||||||
|
return async (ctx, next) => {
|
||||||
|
const currentEvents = getEvents(ctx)
|
||||||
|
let shouldRun = false
|
||||||
|
|
||||||
|
if (
|
||||||
|
expectedEventsPositive.size &&
|
||||||
|
currentEvents.some(e => expectedEventsPositive.has(e))
|
||||||
|
) {
|
||||||
|
shouldRun = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
expectedEventsNegative.size &&
|
||||||
|
currentEvents.every(e => !expectedEventsNegative.has(e))
|
||||||
|
) {
|
||||||
|
shouldRun = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRun) {
|
||||||
|
await composedMiddleware(ctx, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import {composeMiddleware} from '../core'
|
||||||
|
import {passThrough} from './pass-through'
|
||||||
|
import {CommandRunnerMiddleware} from '../types'
|
||||||
|
import {PromisifiedFn, removeWhitespaces} from '../utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher types that are available to user to match exit code against
|
||||||
|
* and set middleware on
|
||||||
|
*/
|
||||||
|
export type ExitCodeMatcher = string | number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
const MATCHERS = {
|
||||||
|
'>=': gte,
|
||||||
|
'>': gt,
|
||||||
|
'<=': lte,
|
||||||
|
'<': lt,
|
||||||
|
'=': eq
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const parseExitCodeMatcher = (
|
||||||
|
code: ExitCodeMatcher
|
||||||
|
): [keyof typeof MATCHERS, number] => {
|
||||||
|
if (typeof code === 'number') {
|
||||||
|
return ['=', code]
|
||||||
|
}
|
||||||
|
|
||||||
|
code = removeWhitespaces(code)
|
||||||
|
|
||||||
|
// just shortcuts for the most common cases
|
||||||
|
if (code.startsWith('=')) return ['=', Number(code.slice(1))]
|
||||||
|
if (code === '>0') return ['>', 0]
|
||||||
|
if (code === '<1') return ['<', 1]
|
||||||
|
|
||||||
|
const match = code.match(/^([><]=?)(\d+)$/)
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
throw new Error(`Invalid exit code matcher: ${code}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, operator, number] = match
|
||||||
|
return [operator as keyof typeof MATCHERS, parseInt(number)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call passed middleware if matching exit code was returned.
|
||||||
|
* Will call the next middleware otherwise. Will also call next middleware
|
||||||
|
* if exit code is null (command did not run).
|
||||||
|
*/
|
||||||
|
export const matchExitCode = (
|
||||||
|
code: ExitCodeMatcher,
|
||||||
|
middleware?: CommandRunnerMiddleware[]
|
||||||
|
): PromisifiedFn<CommandRunnerMiddleware> => {
|
||||||
|
const [operator, number] = parseExitCodeMatcher(code)
|
||||||
|
|
||||||
|
// sets appropriate matching function
|
||||||
|
const matcherFn = MATCHERS[operator](number)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes passed middleware if any or replaces them
|
||||||
|
* with passThrough middleware if none were passed
|
||||||
|
* to avoid errors when calling composed middleware
|
||||||
|
*/
|
||||||
|
const composedMiddleware = composeMiddleware(
|
||||||
|
!middleware?.length ? [passThrough()] : middleware
|
||||||
|
)
|
||||||
|
|
||||||
|
return async (ctx, next) => {
|
||||||
|
// if exit code is undefined, NaN will not match anything
|
||||||
|
if (matcherFn(ctx.exitCode ?? NaN)) {
|
||||||
|
await composedMiddleware(ctx, next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import {composeMiddleware} from '../core'
|
||||||
|
import {passThrough} from './pass-through'
|
||||||
|
import {CommandRunnerMiddleware} from '../types'
|
||||||
|
import {PromisifiedFn} from '../utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher types that are available to user to match output against
|
||||||
|
* and set middleware on
|
||||||
|
*/
|
||||||
|
export type OutputMatcher = RegExp | string | ((output: string) => boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will call passed middleware if command produced a matching stdout.
|
||||||
|
* Will call the next middleware otherwise. Will also call the next middleware
|
||||||
|
* if stdout is null (command did not run).
|
||||||
|
*/
|
||||||
|
export const matchOutput = (
|
||||||
|
matcher: OutputMatcher,
|
||||||
|
middleware?: CommandRunnerMiddleware[]
|
||||||
|
): PromisifiedFn<CommandRunnerMiddleware> => {
|
||||||
|
/**
|
||||||
|
* Composes passed middleware if any or replaces them
|
||||||
|
* with passThrough middleware if none were passed
|
||||||
|
* to avoid errors when calling composed middleware
|
||||||
|
*/
|
||||||
|
const composedMiddleware = composeMiddleware(
|
||||||
|
!middleware?.length ? [passThrough()] : middleware
|
||||||
|
)
|
||||||
|
|
||||||
|
return async (ctx, next) => {
|
||||||
|
const output = ctx.stdout
|
||||||
|
|
||||||
|
if (output === null) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof matcher === 'function' && !matcher(output)) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof matcher === 'string' && output !== matcher) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher instanceof RegExp && !matcher.test(output)) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await composedMiddleware(ctx, next)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import {CommandRunnerMiddleware} from '../types'
|
||||||
|
import {PromisifiedFn} from '../utils'
|
||||||
|
|
||||||
|
/** Calls next middleware */
|
||||||
|
export const passThrough: () => PromisifiedFn<CommandRunnerMiddleware> =
|
||||||
|
() => async (_, next) =>
|
||||||
|
next()
|
|
@ -1,41 +1,64 @@
|
||||||
import * as exec from '../exec'
|
import * as exec from '../exec'
|
||||||
import {PromisifiedFn} from './utils'
|
import {PromisifiedFn} from './utils'
|
||||||
|
|
||||||
/* CommandRunner core */
|
/**
|
||||||
|
* CommandRunner.prototype.run() outpout and context
|
||||||
|
* that is passed to each middleware
|
||||||
|
*/
|
||||||
export interface CommandRunnerContext {
|
export interface CommandRunnerContext {
|
||||||
/* Inputs with which command was executed */
|
/** Command that was executed */
|
||||||
commandLine: string
|
commandLine: string
|
||||||
|
|
||||||
|
/** Arguments with which command was executed */
|
||||||
args: string[]
|
args: string[]
|
||||||
|
|
||||||
|
/** Command options with which command executor was ran */
|
||||||
options: exec.ExecOptions
|
options: exec.ExecOptions
|
||||||
|
|
||||||
/* Results of the execution */
|
/** Error that was thrown when attempting to execute command */
|
||||||
execerr: Error | null
|
execerr: Error | null
|
||||||
|
|
||||||
|
/** Command's output that was passed to stderr if command did run, null otherwise */
|
||||||
stderr: string | null
|
stderr: string | null
|
||||||
|
|
||||||
|
/** Command's output that was passed to stdout if command did run, null otherwise */
|
||||||
stdout: string | null
|
stdout: string | null
|
||||||
|
|
||||||
|
/** Command's exit code if command did run, null otherwise */
|
||||||
exitCode: number | null
|
exitCode: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Middlewares as used by the user */
|
/**
|
||||||
|
* Base middleware shape
|
||||||
|
*/
|
||||||
type _CommandRunnerMiddleware = (
|
type _CommandRunnerMiddleware = (
|
||||||
ctx: CommandRunnerContext,
|
ctx: CommandRunnerContext,
|
||||||
next: () => Promise<void>
|
next: () => Promise<void>
|
||||||
) => void | Promise<void>
|
) => void | Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized middleware shape that is always promisified
|
||||||
|
*/
|
||||||
export type CommandRunnerMiddleware = PromisifiedFn<_CommandRunnerMiddleware>
|
export type CommandRunnerMiddleware = PromisifiedFn<_CommandRunnerMiddleware>
|
||||||
|
|
||||||
/* Command runner events handling and command runner actions */
|
/**
|
||||||
|
* Shape for the command runner default middleware creators
|
||||||
|
*/
|
||||||
export type CommandRunnerAction = (
|
export type CommandRunnerAction = (
|
||||||
message?:
|
message?:
|
||||||
| string
|
| string
|
||||||
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
|
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
|
||||||
) => PromisifiedFn<CommandRunnerMiddleware>
|
) => PromisifiedFn<CommandRunnerMiddleware>
|
||||||
|
|
||||||
/* Command runner default actions types on which preset middleware exists */
|
/**
|
||||||
|
* Default middleware identifires that can be uset to set respective action
|
||||||
|
* in copmposing middleware
|
||||||
|
*/
|
||||||
export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
|
export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
|
||||||
|
|
||||||
/* Command runner event types as used internally passed to middleware for the user */
|
/**
|
||||||
|
* Command runner event types on which middleware can be set
|
||||||
|
*/
|
||||||
export type CommandRunnerEventType =
|
export type CommandRunnerEventType =
|
||||||
| 'execerr'
|
| 'execerr'
|
||||||
| 'stderr'
|
| 'stderr'
|
||||||
|
@ -43,27 +66,19 @@ export type CommandRunnerEventType =
|
||||||
| 'exitcode'
|
| 'exitcode'
|
||||||
| 'ok'
|
| 'ok'
|
||||||
|
|
||||||
/* Command runner event types as used by the user for filtering results */
|
/**
|
||||||
|
* Extended event type that can be used to set middleware on event not happening
|
||||||
|
*/
|
||||||
export type CommandRunnerEventTypeExtended =
|
export type CommandRunnerEventTypeExtended =
|
||||||
| CommandRunnerEventType
|
| CommandRunnerEventType
|
||||||
| `!${CommandRunnerEventType}`
|
| `!${CommandRunnerEventType}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* options that would be passed to the command executor (exec.exec by default)
|
||||||
|
* failOnStdErr and ignoreReturnCode are excluded as they are
|
||||||
|
* handled by the CommandRunner itself
|
||||||
|
*/
|
||||||
export type CommandRunnerOptions = Omit<
|
export type CommandRunnerOptions = Omit<
|
||||||
exec.ExecOptions,
|
exec.ExecOptions,
|
||||||
'failOnStdErr' | 'ignoreReturnCode'
|
'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)
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Promises
|
* Promisifies a a function type
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type PromisifiedFn<T extends (...args: any[]) => any> = (
|
export type PromisifiedFn<T extends (...args: any[]) => any> = (
|
||||||
...args: Parameters<T>
|
...args: Parameters<T>
|
||||||
|
@ -9,6 +8,9 @@ export type PromisifiedFn<T extends (...args: any[]) => any> = (
|
||||||
? ReturnType<T>
|
? ReturnType<T>
|
||||||
: Promise<ReturnType<T>>
|
: Promise<ReturnType<T>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisifies a function
|
||||||
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const promisifyFn = <T extends (...args: any[]) => any>(
|
export const promisifyFn = <T extends (...args: any[]) => any>(
|
||||||
fn: T
|
fn: T
|
||||||
|
@ -27,26 +29,6 @@ export const promisifyFn = <T extends (...args: any[]) => any>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparators
|
* Removes all whitespaces from a string
|
||||||
*/
|
*/
|
||||||
|
export const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
|
||||||
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
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import {StringDecoder} from 'string_decoder'
|
||||||
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
|
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
|
||||||
import * as tr from './toolrunner'
|
import * as tr from './toolrunner'
|
||||||
|
|
||||||
|
export {CommandRunner, createCommandRunner} from './command-runner'
|
||||||
|
|
||||||
export {ExecOptions, ExecOutput, ExecListeners}
|
export {ExecOptions, ExecOutput, ExecListeners}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue