1
0
Fork 0

Refactor base middlewares

pull/1562/head
Nikolai Laevskii 2023-10-12 06:58:22 +02:00
parent 36629a3962
commit b45c9d3801
5 changed files with 146 additions and 154 deletions

View File

@ -1,9 +1,6 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import {CommandRunnerBase} from './core' import {CommandRunnerBase} from './core'
import { import {
ErrorMatcher,
ExitCodeMatcher,
OutputMatcher,
failAction, failAction,
matchEvent, matchEvent,
matchExitCode, matchExitCode,
@ -16,7 +13,10 @@ import {
CommandRunnerActionType, CommandRunnerActionType,
CommandRunnerEventTypeExtended, CommandRunnerEventTypeExtended,
CommandRunnerMiddleware, CommandRunnerMiddleware,
CommandRunnerOptions CommandRunnerOptions,
ErrorMatcher,
ExitCodeMatcher,
OutputMatcher
} from './types' } from './types'
const commandRunnerActions = { const commandRunnerActions = {

View File

@ -3,39 +3,12 @@ import {StringDecoder} from 'string_decoder'
import { import {
CommandRunnerContext, CommandRunnerContext,
CommandRunnerMiddleware, CommandRunnerMiddleware,
CommandRunnerMiddlewarePromisified,
CommandRunnerOptions CommandRunnerOptions
} from './types' } from './types'
import {PromisifiedFn, promisifyFn} from './utils'
export const promisifyCommandRunnerMiddleware =
(middleware: CommandRunnerMiddleware): CommandRunnerMiddlewarePromisified =>
async (ctx, next) => {
return Promise.resolve(middleware(ctx, next))
}
export const composeCommandRunnerMiddleware =
(middleware: CommandRunnerMiddlewarePromisified[]) =>
async (context: CommandRunnerContext, nextGlobal: () => Promise<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 { export class CommandRunnerBase {
private middleware: CommandRunnerMiddlewarePromisified[] = [] private middleware: PromisifiedFn<CommandRunnerMiddleware>[] = []
constructor( constructor(
private commandLine: string, private commandLine: string,
@ -45,7 +18,7 @@ export class CommandRunnerBase {
) {} ) {}
use(middleware: CommandRunnerMiddleware): this { use(middleware: CommandRunnerMiddleware): this {
this.middleware.push(promisifyCommandRunnerMiddleware(middleware)) this.middleware.push(promisifyFn(middleware))
return this return this
} }
@ -104,8 +77,36 @@ export class CommandRunnerBase {
} }
const next = async (): Promise<void> => Promise.resolve() const next = async (): Promise<void> => Promise.resolve()
await composeCommandRunnerMiddleware(this.middleware)(context, next) await composeMiddleware(this.middleware)(context, next)
return context return context
} }
} }
export function composeMiddleware(
middleware: PromisifiedFn<CommandRunnerMiddleware>[]
): PromisifiedFn<CommandRunnerMiddleware> {
middleware = middleware.map(mw => promisifyFn(mw))
return 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()
}
}

View File

@ -1,15 +1,16 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import { import {
CommandRunnerAction,
CommandRunnerContext, CommandRunnerContext,
CommandRunnerEventType, CommandRunnerEventType,
CommandRunnerEventTypeExtended, CommandRunnerEventTypeExtended,
CommandRunnerMiddleware, CommandRunnerMiddleware,
CommandRunnerMiddlewarePromisified ErrorMatcher,
ExitCodeMatcher,
OutputMatcher
} from './types' } from './types'
import { import {composeMiddleware} from './core'
composeCommandRunnerMiddleware, import {gte, gt, lte, lt, eq, PromisifiedFn} from './utils'
promisifyCommandRunnerMiddleware
} from './core'
const getEventTypesFromContext = ( const getEventTypesFromContext = (
ctx: CommandRunnerContext ctx: CommandRunnerContext
@ -39,16 +40,15 @@ const getEventTypesFromContext = (
return [...eventTypes] return [...eventTypes]
} }
type CommandRunnerAction = (
message?:
| string
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
) => CommandRunnerMiddlewarePromisified
/** /**
* Basic middleware * 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. * Fails Github Action with the given message or with a default one depending on execution conditions.
*/ */
@ -202,31 +202,6 @@ export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
next() 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 passed middleware if matching event has occured.
* Will call the next middleware otherwise. * Will call the next middleware otherwise.
@ -234,13 +209,9 @@ export const filter: (
export const matchEvent = ( export const matchEvent = (
eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[], eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
middleware?: CommandRunnerMiddleware[] middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => { ): PromisifiedFn<CommandRunnerMiddleware> => {
if (!middleware?.length) { const composedMiddleware = composeMiddleware(
middleware = [passThrough()] !middleware?.length ? [passThrough()] : middleware
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
) )
const expectedEventsPositiveArray = ( const expectedEventsPositiveArray = (
@ -283,8 +254,6 @@ export const matchEvent = (
} }
} }
export type OutputMatcher = RegExp | string | ((output: string) => boolean)
/** /**
* Will call passed middleware if matching event has occured. * Will call passed middleware if matching event has occured.
* Will call the next middleware otherwise. * Will call the next middleware otherwise.
@ -292,13 +261,9 @@ export type OutputMatcher = RegExp | string | ((output: string) => boolean)
export const matchOutput = ( export const matchOutput = (
matcher: OutputMatcher, matcher: OutputMatcher,
middleware?: CommandRunnerMiddleware[] middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => { ): PromisifiedFn<CommandRunnerMiddleware> => {
if (!middleware?.length) { const composedMiddleware = composeMiddleware(
middleware = [passThrough()] !middleware?.length ? [passThrough()] : middleware
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
) )
return async (ctx, next) => { return async (ctx, next) => {
@ -328,30 +293,9 @@ export const matchOutput = (
} }
} }
export type ExitCodeMatcher = string | number const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
const lte = const MATCHERS = {
(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, '>=': gte,
'>': gt, '>': gt,
'<=': lte, '<=': lte,
@ -359,11 +303,9 @@ const matchers = {
'=': eq '=': eq
} as const } as const
const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
const parseExitCodeMatcher = ( const parseExitCodeMatcher = (
code: ExitCodeMatcher code: ExitCodeMatcher
): [keyof typeof matchers, number] => { ): [keyof typeof MATCHERS, number] => {
if (typeof code === 'number') { if (typeof code === 'number') {
return ['=', code] return ['=', code]
} }
@ -382,14 +324,7 @@ const parseExitCodeMatcher = (
} }
const [, operator, number] = match const [, operator, number] = match
return [operator as keyof typeof matchers, parseInt(number)] return [operator as keyof typeof MATCHERS, parseInt(number)]
}
const matcherToMatcherFn = (
matcher: ExitCodeMatcher
): ((exitCode: number) => boolean) => {
const [operator, number] = parseExitCodeMatcher(matcher)
return matchers[operator](number)
} }
/** /**
@ -399,20 +334,17 @@ const matcherToMatcherFn = (
export const matchExitCode = ( export const matchExitCode = (
code: ExitCodeMatcher, code: ExitCodeMatcher,
middleware?: CommandRunnerMiddleware[] middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => { ): PromisifiedFn<CommandRunnerMiddleware> => {
const matcher = matcherToMatcherFn(code) const [operator, number] = parseExitCodeMatcher(code)
const matcherFn = MATCHERS[operator](number)
if (!middleware?.length) { const composedMiddleware = composeMiddleware(
middleware = [passThrough()] !middleware?.length ? [passThrough()] : middleware
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
) )
return async (ctx, next) => { return async (ctx, next) => {
// if exit code is undefined, NaN will not match anything // if exit code is undefined, NaN will not match anything
if (matcher(ctx.exitCode ?? NaN)) { if (matcherFn(ctx.exitCode ?? NaN)) {
composedMiddleware(ctx, next) composedMiddleware(ctx, next)
return return
} }
@ -421,25 +353,14 @@ export const matchExitCode = (
} }
} }
export type ErrorMatcher =
| RegExp
| string
| ((error: {
type: 'stderr' | 'execerr'
error: Error | null
message: string
}) => boolean)
export const matchSpecificError = ( export const matchSpecificError = (
matcher: ErrorMatcher, matcher: ErrorMatcher,
middleware?: CommandRunnerMiddleware[] middleware?:
): CommandRunnerMiddlewarePromisified => { | CommandRunnerMiddleware[]
if (!middleware?.length) { | PromisifiedFn<CommandRunnerMiddleware>[]
middleware = [passThrough()] ): PromisifiedFn<CommandRunnerMiddleware> => {
} const composedMiddleware = composeMiddleware(
!middleware?.length ? [passThrough()] : middleware
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
) )
return async (ctx, next) => { return async (ctx, next) => {

View File

@ -1,4 +1,5 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import {PromisifiedFn} from './utils'
/* CommandRunner core */ /* CommandRunner core */
@ -15,20 +16,22 @@ export interface CommandRunnerContext {
exitCode: number | null exitCode: number | null
} }
/* Middlewares as used internally in CommandRunner */
export type CommandRunnerMiddlewarePromisified = (
ctx: CommandRunnerContext,
next: () => Promise<void>
) => Promise<void>
/* Middlewares as used by the user */ /* Middlewares as used by the user */
export type CommandRunnerMiddleware = ( type _CommandRunnerMiddleware = (
ctx: CommandRunnerContext, ctx: CommandRunnerContext,
next: () => Promise<void> next: () => Promise<void>
) => void | Promise<void> ) => void | Promise<void>
export type CommandRunnerMiddleware = PromisifiedFn<_CommandRunnerMiddleware>
/* Command runner events handling and command runner actions */ /* Command runner events handling and command runner actions */
export type CommandRunnerAction = (
message?:
| string
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
) => PromisifiedFn<CommandRunnerMiddleware>
/* Command runner default actions types on which preset middleware exists */ /* Command runner default actions types on which preset middleware exists */
export type CommandRunnerActionType = 'throw' | 'fail' | 'log' export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
@ -49,3 +52,18 @@ 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)

View File

@ -0,0 +1,52 @@
/**
* Promises
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type PromisifiedFn<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => ReturnType<T> extends Promise<unknown>
? ReturnType<T>
: Promise<ReturnType<T>>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const promisifyFn = <T extends (...args: any[]) => any>(
fn: T
): PromisifiedFn<T> => {
const result = async (...args: Parameters<T>): Promise<unknown> => {
return new Promise((resolve, reject) => {
try {
resolve(fn(...args))
} catch (error) {
reject(error)
}
})
}
return result as PromisifiedFn<T>
}
/**
* 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