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