mirror of https://github.com/actions/toolkit
Add middleware for command-runner
parent
45c2409453
commit
c00940a820
|
@ -0,0 +1,202 @@
|
|||
import * as exec from '@actions/exec'
|
||||
import {CommandRunner, commandPipeline} from '../src/helpers'
|
||||
|
||||
describe('command-runner', () => {
|
||||
describe('commandPipeline', () => {
|
||||
it('creates a command object', async () => {
|
||||
const command = commandPipeline('echo')
|
||||
expect(command).toBeDefined()
|
||||
expect(command).toBeInstanceOf(CommandRunner)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CommandRunner', () => {
|
||||
const execSpy = jest.spyOn(exec, 'getExecOutput')
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('runs basic commands', async () => {
|
||||
execSpy.mockImplementation(async () =>
|
||||
Promise.resolve({
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
})
|
||||
)
|
||||
|
||||
const command = commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
command.run()
|
||||
|
||||
expect(execSpy).toHaveBeenCalledTimes(1)
|
||||
expect(execSpy).toHaveBeenCalledWith('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
})
|
||||
|
||||
it('overrides args with addArgs and withArgs', async () => {
|
||||
execSpy.mockImplementation(async () =>
|
||||
Promise.resolve({
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
})
|
||||
)
|
||||
|
||||
const command = commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
|
||||
await command.withArgs('bye').run()
|
||||
|
||||
expect(execSpy).toHaveBeenCalledWith('echo', ['bye'], {
|
||||
silent: true
|
||||
})
|
||||
|
||||
execSpy.mockClear()
|
||||
|
||||
await command.addArgs('and stuff').run()
|
||||
|
||||
expect(execSpy).toHaveBeenCalledWith(
|
||||
'echo',
|
||||
['hello', 'world', 'and stuff'],
|
||||
{
|
||||
silent: true
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('allows to use middlewares', async () => {
|
||||
execSpy.mockImplementation(async () => {
|
||||
return {
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}
|
||||
})
|
||||
|
||||
const command = commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
|
||||
const middleware = jest.fn()
|
||||
|
||||
await command.use(middleware).run()
|
||||
|
||||
expect(middleware).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(middleware).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
commandLine: 'echo',
|
||||
args: ['hello', 'world'],
|
||||
options: {
|
||||
silent: true
|
||||
},
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
execerr: null,
|
||||
state: null
|
||||
}),
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
describe('CommandRunner.prototype.on', () => {
|
||||
it('passes control to next middleware if nothing has matched', async () => {
|
||||
execSpy.mockImplementation(async () => {
|
||||
return {
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}
|
||||
})
|
||||
|
||||
const willBeCalled = jest.fn()
|
||||
const willNotBeCalled = jest.fn()
|
||||
await commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
.on('no-stdout', willNotBeCalled)
|
||||
.use(willBeCalled)
|
||||
.run()
|
||||
|
||||
expect(willNotBeCalled).not.toHaveBeenCalled()
|
||||
expect(willBeCalled).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('runs a middleware if event matches', async () => {
|
||||
execSpy.mockImplementation(async () => {
|
||||
return {
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}
|
||||
})
|
||||
|
||||
const middleware = jest.fn()
|
||||
|
||||
await commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
.on('ok', middleware)
|
||||
.run()
|
||||
|
||||
expect(middleware).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('runs a middleware if event matches with negation', async () => {
|
||||
execSpy.mockImplementation(async () => {
|
||||
return {
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}
|
||||
})
|
||||
|
||||
const middleware = jest.fn()
|
||||
await commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
})
|
||||
.on('!no-stdout', middleware)
|
||||
.run()
|
||||
|
||||
expect(middleware).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('runs a middleware on multiple events', async () => {
|
||||
execSpy.mockImplementation(async () => {
|
||||
return {
|
||||
stdout: 'hello',
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
}
|
||||
})
|
||||
|
||||
const middleware = jest.fn()
|
||||
const command = commandPipeline('echo', ['hello', 'world'], {
|
||||
silent: true
|
||||
}).on(['!no-stdout', 'ok'], middleware)
|
||||
|
||||
await command.run()
|
||||
|
||||
expect(middleware).toHaveBeenCalledTimes(1)
|
||||
|
||||
execSpy.mockImplementation(async () => {
|
||||
return {
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 1
|
||||
}
|
||||
})
|
||||
|
||||
await command.run()
|
||||
|
||||
expect(middleware).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,157 @@
|
|||
import * as exec from '@actions/exec'
|
||||
import {CommandRunnerBase} from './core'
|
||||
import {
|
||||
ErrorMatcher,
|
||||
ExitCodeMatcher,
|
||||
OutputMatcher,
|
||||
failAction,
|
||||
matchEvent,
|
||||
matchExitCode,
|
||||
matchOutput,
|
||||
matchSpecificError,
|
||||
produceLog,
|
||||
throwError
|
||||
} from './middleware'
|
||||
import {
|
||||
CommandRunnerActionType,
|
||||
CommandRunnerEventTypeExtended,
|
||||
CommandRunnerMiddleware
|
||||
} from './types'
|
||||
|
||||
const commandRunnerActions = {
|
||||
throw: throwError,
|
||||
fail: failAction,
|
||||
log: produceLog
|
||||
} as const
|
||||
|
||||
export class CommandRunner<S = unknown> extends CommandRunnerBase<S> {
|
||||
on(
|
||||
event: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
const middleware =
|
||||
typeof action === 'string'
|
||||
? [commandRunnerActions[action](message)]
|
||||
: [action]
|
||||
|
||||
this.use(matchEvent(event, middleware as CommandRunnerMiddleware[]))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onEmptyOutput(
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
this.on('no-stdout', action, message)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onExecutionError(
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
const middleware =
|
||||
typeof action === 'string'
|
||||
? [commandRunnerActions[action](message)]
|
||||
: [action]
|
||||
|
||||
this.use(
|
||||
matchSpecificError(
|
||||
({type}) => type === 'execerr',
|
||||
middleware as CommandRunnerMiddleware[]
|
||||
)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onStdError(
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
const middleware =
|
||||
typeof action === 'string'
|
||||
? [commandRunnerActions[action](message)]
|
||||
: [action]
|
||||
|
||||
this.use(
|
||||
matchSpecificError(
|
||||
({type}) => type === 'stderr',
|
||||
middleware as CommandRunnerMiddleware[]
|
||||
)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onError(
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
return this.on(['execerr', 'stderr'], action, message)
|
||||
}
|
||||
|
||||
onSpecificError(
|
||||
matcher: ErrorMatcher,
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
const middleware =
|
||||
typeof action === 'string'
|
||||
? [commandRunnerActions[action](message)]
|
||||
: [action]
|
||||
|
||||
this.use(
|
||||
matchSpecificError(matcher, middleware as CommandRunnerMiddleware[])
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onSuccess(
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
return this.on('ok', action, message)
|
||||
}
|
||||
|
||||
onExitCode(
|
||||
matcher: ExitCodeMatcher,
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
const middleware =
|
||||
typeof action === 'string'
|
||||
? [commandRunnerActions[action](message)]
|
||||
: [action]
|
||||
|
||||
this.use(matchExitCode(matcher, middleware as CommandRunnerMiddleware[]))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onOutput(
|
||||
matcher: OutputMatcher,
|
||||
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
|
||||
message?: string
|
||||
): this {
|
||||
const middleware =
|
||||
typeof action === 'string'
|
||||
? [commandRunnerActions[action](message)]
|
||||
: [action]
|
||||
|
||||
this.use(matchOutput(matcher, middleware as CommandRunnerMiddleware[]))
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export const commandPipeline = <S = unknown>(
|
||||
commandLine: string,
|
||||
args: string[] = [],
|
||||
options: Record<string, unknown> = {}
|
||||
): CommandRunner<S> =>
|
||||
new CommandRunner(commandLine, args, options, exec.getExecOutput)
|
|
@ -0,0 +1,125 @@
|
|||
import * as exec from '@actions/exec'
|
||||
import {
|
||||
CommandRunnerContext,
|
||||
CommandRunnerMiddleware,
|
||||
CommandRunnerMiddlewarePromisified
|
||||
} from './types'
|
||||
|
||||
export const promisifyCommandRunnerMiddleware =
|
||||
(
|
||||
middleware: CommandRunnerMiddleware<unknown>
|
||||
): CommandRunnerMiddlewarePromisified =>
|
||||
async (ctx, next) => {
|
||||
return Promise.resolve(middleware(ctx, next))
|
||||
}
|
||||
|
||||
export const composeCommandRunnerMiddleware =
|
||||
(middleware: CommandRunnerMiddlewarePromisified[]) =>
|
||||
async (context: CommandRunnerContext, nextGlobal: () => Promise<void>) => {
|
||||
let index = 0
|
||||
|
||||
const nextLocal = async (): Promise<void> => {
|
||||
if (index < middleware.length) {
|
||||
const currentMiddleware = middleware[index++]
|
||||
if (middleware === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await currentMiddleware(context, nextLocal)
|
||||
}
|
||||
|
||||
await nextGlobal()
|
||||
}
|
||||
|
||||
await nextLocal()
|
||||
}
|
||||
|
||||
export class CommandRunnerBase<S = unknown> {
|
||||
private middleware: CommandRunnerMiddlewarePromisified[] = []
|
||||
private tmpArgs: string[] = []
|
||||
|
||||
constructor(
|
||||
private commandLine: string,
|
||||
private args: string[] = [],
|
||||
private options: exec.ExecOptions = {},
|
||||
private executor: typeof exec.getExecOutput = exec.getExecOutput
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Adds additional arguments to the command
|
||||
* for the one time execution.
|
||||
*/
|
||||
addArgs(...args: string[]): this {
|
||||
this.tmpArgs = [...this.args, ...args]
|
||||
return this
|
||||
}
|
||||
|
||||
/** Overrides command arguments for one time execution */
|
||||
withArgs(...args: string[]): this {
|
||||
this.tmpArgs = args
|
||||
return this
|
||||
}
|
||||
|
||||
/** Retrieves args for one-time execution and clears them afterwards */
|
||||
private getTmpArgs(): string[] | null {
|
||||
if (this.tmpArgs.length === 0) return null
|
||||
|
||||
const args = this.tmpArgs
|
||||
|
||||
this.tmpArgs = []
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
use(middleware: CommandRunnerMiddleware<S>): this {
|
||||
this.middleware.push(
|
||||
promisifyCommandRunnerMiddleware(
|
||||
middleware as CommandRunnerMiddleware<unknown>
|
||||
)
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
async run(
|
||||
/* overrides command for this specific execution if not undefined */
|
||||
commandLine?: string,
|
||||
|
||||
/* overrides args for this specific execution if not undefined */
|
||||
args?: string[],
|
||||
|
||||
/* overrides options for this specific execution if not undefined */
|
||||
options?: exec.ExecOptions
|
||||
): Promise<CommandRunnerContext<S>> {
|
||||
const tmpArgs = this.getTmpArgs()
|
||||
|
||||
const context: CommandRunnerContext<S> = {
|
||||
commandLine: commandLine ?? this.commandLine,
|
||||
args: args ?? tmpArgs ?? this.args,
|
||||
options: options ?? this.options,
|
||||
stdout: null,
|
||||
stderr: null,
|
||||
execerr: null,
|
||||
exitCode: null,
|
||||
state: null
|
||||
}
|
||||
|
||||
try {
|
||||
const {stdout, stderr, exitCode} = await this.executor(
|
||||
context.commandLine,
|
||||
context.args,
|
||||
context.options
|
||||
)
|
||||
|
||||
context.stdout = stdout
|
||||
context.stderr = stderr
|
||||
context.exitCode = exitCode
|
||||
} catch (error) {
|
||||
context.execerr = error as Error
|
||||
}
|
||||
|
||||
const next = async (): Promise<void> => Promise.resolve()
|
||||
await composeCommandRunnerMiddleware(this.middleware)(context, next)
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export {commandPipeline, CommandRunner} from './command-runner'
|
|
@ -0,0 +1,474 @@
|
|||
import * as core from '@actions/core'
|
||||
import {
|
||||
CommandRunnerContext,
|
||||
CommandRunnerEventType,
|
||||
CommandRunnerEventTypeExtended,
|
||||
CommandRunnerMiddleware,
|
||||
CommandRunnerMiddlewarePromisified
|
||||
} from './types'
|
||||
import {
|
||||
composeCommandRunnerMiddleware,
|
||||
promisifyCommandRunnerMiddleware
|
||||
} from './core'
|
||||
|
||||
const getEventTypesFromContext = (
|
||||
ctx: CommandRunnerContext
|
||||
): CommandRunnerEventType[] => {
|
||||
const eventTypes = new Set<CommandRunnerEventType>()
|
||||
|
||||
if (ctx.execerr) {
|
||||
eventTypes.add('execerr')
|
||||
}
|
||||
|
||||
if (ctx.stderr || ctx.exitCode !== 0) {
|
||||
eventTypes.add('stderr')
|
||||
}
|
||||
|
||||
if (ctx.stdout !== null && !ctx.stdout.trim()) {
|
||||
eventTypes.add('no-stdout')
|
||||
}
|
||||
|
||||
if (!ctx.stderr && !ctx.execerr && ctx.stdout !== null && !ctx.exitCode) {
|
||||
eventTypes.add('ok')
|
||||
}
|
||||
|
||||
return [...eventTypes]
|
||||
}
|
||||
|
||||
type CommandRunnerAction = (
|
||||
message?:
|
||||
| string
|
||||
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
|
||||
) => CommandRunnerMiddlewarePromisified
|
||||
|
||||
/**
|
||||
* Basic middleware
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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('no-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 => 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('no-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('no-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('no-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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<boolean>)
|
||||
) => 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.
|
||||
*/
|
||||
export const matchEvent = (
|
||||
eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
|
||||
middleware?: CommandRunnerMiddleware[]
|
||||
): CommandRunnerMiddlewarePromisified => {
|
||||
if (!middleware?.length) {
|
||||
middleware = [passThrough()]
|
||||
}
|
||||
|
||||
const composedMiddleware = composeCommandRunnerMiddleware(
|
||||
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
|
||||
)
|
||||
|
||||
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) {
|
||||
composedMiddleware(ctx, next)
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
export type OutputMatcher = RegExp | string | ((output: string) => boolean)
|
||||
|
||||
/**
|
||||
* Will call passed middleware if matching event has occured.
|
||||
* Will call the next middleware otherwise.
|
||||
*/
|
||||
export const matchOutput = (
|
||||
matcher: OutputMatcher,
|
||||
middleware?: CommandRunnerMiddleware[]
|
||||
): CommandRunnerMiddlewarePromisified => {
|
||||
if (!middleware?.length) {
|
||||
middleware = [passThrough()]
|
||||
}
|
||||
|
||||
const composedMiddleware = composeCommandRunnerMiddleware(
|
||||
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
composedMiddleware(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
export type ExitCodeMatcher = string | number
|
||||
|
||||
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 = {
|
||||
'>=': gte,
|
||||
'>': gt,
|
||||
'<=': lte,
|
||||
'<': lt,
|
||||
'=': eq
|
||||
} as const
|
||||
|
||||
const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
|
||||
|
||||
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)]
|
||||
}
|
||||
|
||||
const matcherToMatcherFn = (
|
||||
matcher: ExitCodeMatcher
|
||||
): ((exitCode: number) => boolean) => {
|
||||
const [operator, number] = parseExitCodeMatcher(matcher)
|
||||
return matchers[operator](number)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call passed middleware if matching exit code was returned.
|
||||
* Will call the next middleware otherwise.
|
||||
*/
|
||||
export const matchExitCode = (
|
||||
code: ExitCodeMatcher,
|
||||
middleware?: CommandRunnerMiddleware[]
|
||||
): CommandRunnerMiddlewarePromisified => {
|
||||
const matcher = matcherToMatcherFn(code)
|
||||
|
||||
if (!middleware?.length) {
|
||||
middleware = [passThrough()]
|
||||
}
|
||||
|
||||
const composedMiddleware = composeCommandRunnerMiddleware(
|
||||
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
|
||||
)
|
||||
|
||||
return async (ctx, next) => {
|
||||
// if exit code is undefined, NaN will not match anything
|
||||
if (matcher(ctx.exitCode ?? NaN)) {
|
||||
composedMiddleware(ctx, next)
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
composedMiddleware(ctx, next)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import {CommandRunner} from './command-runner'
|
||||
import * as io from '@actions/io'
|
||||
|
||||
;(async () => {
|
||||
const toolpath = await io.which('cmd', true)
|
||||
const args = ['/c', 'echo']
|
||||
|
||||
const echo = new CommandRunner('echo')
|
||||
|
||||
echo
|
||||
.on('exec-error', 'log')
|
||||
.use(async (ctx, next) => {
|
||||
console.log('success')
|
||||
next()
|
||||
})
|
||||
.addArgs('hello')
|
||||
.run()
|
||||
})()
|
|
@ -0,0 +1,44 @@
|
|||
import * as exec from '@actions/exec'
|
||||
|
||||
/* CommandRunner core */
|
||||
|
||||
export interface CommandRunnerContext<S = unknown> {
|
||||
/* Inputs with which command was executed */
|
||||
commandLine: string
|
||||
args: string[]
|
||||
options: exec.ExecOptions
|
||||
|
||||
/* Results of the execution */
|
||||
execerr: Error | null
|
||||
stderr: string | null
|
||||
stdout: string | null
|
||||
exitCode: number | null
|
||||
|
||||
/* Arbitrary state that can be change during middleware execution if needed */
|
||||
state: S | null
|
||||
}
|
||||
|
||||
/* Middlewares as used internally in CommandRunner */
|
||||
export type CommandRunnerMiddlewarePromisified = (
|
||||
ctx: CommandRunnerContext,
|
||||
next: () => Promise<void>
|
||||
) => Promise<void>
|
||||
|
||||
/* Middlewares as used by the user */
|
||||
export type CommandRunnerMiddleware<S = unknown> = (
|
||||
ctx: CommandRunnerContext<S>,
|
||||
next: () => Promise<void>
|
||||
) => void | Promise<void>
|
||||
|
||||
/* Command runner events handling and command runner actions */
|
||||
|
||||
/* Command runner default actions types on which preset middleware exists */
|
||||
export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
|
||||
|
||||
/* Command runner event types as used internally passed to middleware for the user */
|
||||
export type CommandRunnerEventType = 'execerr' | 'stderr' | 'no-stdout' | 'ok'
|
||||
|
||||
/* Command runner event types as used by the user for filtering results */
|
||||
export type CommandRunnerEventTypeExtended =
|
||||
| CommandRunnerEventType
|
||||
| `!${CommandRunnerEventType}`
|
|
@ -1 +1 @@
|
|||
|
||||
export * from './command-runner'
|
||||
|
|
Loading…
Reference in New Issue