UNPKG

@redocly/respect-core

Version:
289 lines 13.1 kB
import { blue, white, bold, red } from 'colorette'; import { callAPIAndAnalyzeResults } from './call-api-and-analyze-results.js'; import { checkCriteria } from './success-criteria/index.js'; import { delay } from '../../utils/delay.js'; import { CHECKS } from '../checks/index.js'; import { runWorkflow, resolveWorkflowContext } from './runner.js'; import { prepareRequest } from './prepare-request.js'; import { printChildWorkflowSeparator, printStepDetails, printActionsSeparator, printUnknownStep, } from '../logger-output/helpers.js'; import { getValueFromContext, isParameterWithoutIn, resolveReusableComponentItem, } from '../context-parser/index.js'; import { evaluateRuntimeExpressionPayload } from '../runtime-expressions/index.js'; import { Timer } from '../timeout-timer/timer.js'; let stepsRun = 0; export async function runStep({ step, ctx, workflowId, retriesLeft, }) { step = { ...step }; // shallow copy step to avoid mutating the original step step.retriesLeft = retriesLeft; const workflow = ctx.workflows.find((w) => w.workflowId === workflowId); const { stepId, onFailure, onSuccess, workflowId: targetWorkflowRef, parameters } = step; const failureActionsToRun = (onFailure || workflow?.failureActions || []).map((action) => resolveReusableComponentItem(action, ctx)); const successActionsToRun = (onSuccess || workflow?.successActions || []).map((action) => resolveReusableComponentItem(action, ctx)); const resolvedParameters = parameters?.map((parameter) => resolveReusableComponentItem(parameter, ctx)); if (targetWorkflowRef) { const targetWorkflow = ctx.workflows.find((w) => w.workflowId === targetWorkflowRef) || getValueFromContext({ value: targetWorkflowRef, ctx, logger: ctx.options.logger }); if (!targetWorkflow) { const failedCall = { name: CHECKS.UNEXPECTED_ERROR, message: `Workflow ${red(targetWorkflowRef)} not found.`, passed: false, severity: ctx.severity['UNEXPECTED_ERROR'], }; step.checks.push(failedCall); return; } const workflowCtx = await resolveWorkflowContext(targetWorkflowRef, targetWorkflow, ctx, ctx.options.config); if (resolvedParameters && resolvedParameters.length) { // When the step in context specifies a workflowId, then all parameters without `in` maps to workflow inputs. const workflowInputParameters = resolvedParameters .filter(isParameterWithoutIn) .reduce((acc, parameter) => { // Ensure parameter is of type ParameterWithoutIn acc[parameter.name] = getValueFromContext({ value: parameter.value, ctx, logger: ctx.options.logger, }); return acc; }, {}); workflowCtx.$workflows[targetWorkflow.workflowId].inputs = workflowInputParameters; } printChildWorkflowSeparator(stepId, ctx.options.logger); const stepWorkflowResult = await runWorkflow({ workflowInput: targetWorkflow, ctx: workflowCtx, skipLineSeparator: true, parentStepId: stepId, invocationContext: `Child workflow of step ${stepId}`, }); ctx.executedSteps.push(stepWorkflowResult); const outputs = {}; if (step?.outputs) { try { for (const [outputKey, outputValue] of Object.entries(step.outputs)) { // need to partially emulate $outputs context outputs[outputKey] = evaluateRuntimeExpressionPayload({ payload: outputValue, context: { $outputs: workflowCtx.$outputs?.[targetWorkflow.workflowId] || {}, }, logger: ctx.options.logger, }); } } catch (error) { const failedCall = { name: CHECKS.UNEXPECTED_ERROR, message: error.message, passed: false, severity: ctx.severity['UNEXPECTED_ERROR'], }; step.checks.push(failedCall); } // save local $steps context ctx.$steps[stepId] = { outputs, }; // save local $steps context to parent workflow if (workflow?.workflowId) { ctx.$workflows[workflow.workflowId].steps[stepId] = { outputs, request: undefined, response: undefined, }; } } return { shouldEnd: false }; } ctx.executedSteps.push(step); stepsRun++; if (stepsRun > ctx.options.maxSteps) { step.checks.push({ name: CHECKS.MAX_STEPS_REACHED_ERROR, message: `Max steps (${ctx.options.maxSteps}) reached`, passed: false, severity: ctx.severity['MAX_STEPS_REACHED_ERROR'], }); return { shouldEnd: true }; } if (ctx.options.executionTimeout && Timer.getInstance(ctx.options.executionTimeout).isTimedOut()) { step.checks.push({ name: CHECKS.GLOBAL_TIMEOUT_ERROR, message: `Global Respect timer reached`, passed: false, severity: ctx.severity['GLOBAL_TIMEOUT_ERROR'], }); return { shouldEnd: true }; } if (resolvedParameters && resolvedParameters.length) { // When the step in context does not specify a workflowId the `in` field MUST be specified. const parameterWithoutIn = resolvedParameters.find((parameter) => { const resolvedParameter = resolveReusableComponentItem(parameter, ctx); return !('in' in resolvedParameter); }); if (parameterWithoutIn) { throw new Error(`Parameter "in" is required for ${stepId} step parameter ${parameterWithoutIn.name}`); } } let allChecksPassed = false; let requestData; try { if (!workflowId) { throw new Error('Workflow name is required to run a step'); } requestData = await prepareRequest(ctx, step, workflowId); const checksResult = await callAPIAndAnalyzeResults({ ctx, workflowId, step, requestData, }); allChecksPassed = Object.values(checksResult).every((check) => check); } catch (e) { step.verboseLog = ctx.apiClient.getVerboseResponseLogs(); const failedCall = { name: CHECKS.UNEXPECTED_ERROR, message: e.message, passed: false, severity: ctx.severity['UNEXPECTED_ERROR'], }; step.checks.push(failedCall); } const verboseLogs = ctx.options.verbose ? ctx.apiClient.getVerboseLogs() : undefined; const verboseResponseLogs = ctx.options.verbose ? ctx.apiClient.getVerboseResponseLogs() : undefined; const requestUrl = requestData?.path || requestData?.serverUrl?.url; if (requestUrl) { printStepDetails({ testNameToDisplay: `${requestData?.method.toUpperCase()} ${white(requestUrl)}${step.stepId ? ` ${blue('- step')} ${white(bold(step.stepId))}` : ''}`, checks: step.checks, verboseLogs, verboseResponseLogs, logger: ctx.options.logger, }); } else { printUnknownStep(step, ctx.options.logger); } if (!allChecksPassed) { const result = await runActions(failureActionsToRun, 'failure'); if (result?.retriesLeft && result.retriesLeft > 0) { // if retriesLeft > 0, it means that the step was retried successfully and we need to // return step result to the outer flow return result.stepResult; } if (result?.shouldEnd) { return { shouldEnd: true }; } } if (successActionsToRun.length && allChecksPassed) { const result = await runActions(successActionsToRun, 'success'); if (result?.shouldEnd) { return { shouldEnd: true }; } } // Internal function to run actions async function runActions(actions = [], kind) { for (const action of actions) { const { type, criteria } = action; if (action.workflowId && action.stepId) { throw new Error(`Cannot use both workflowId: ${action.workflowId} and stepId: ${action.stepId} in ${action.type} action`); } const matchesCriteria = checkCriteria({ workflowId: workflowId, step, criteria, ctx, }).every((check) => check.passed); if (matchesCriteria) { const targetWorkflow = action.workflowId ? getValueFromContext({ value: action.workflowId, ctx, logger: ctx.options.logger }) : undefined; const targetCtx = action.workflowId ? await resolveWorkflowContext(action.workflowId, targetWorkflow, ctx, ctx.options.config) : { ...ctx, executedSteps: [] }; const targetStep = action.stepId ? action.stepId : undefined; if (type === 'retry') { const { retryAfter, retryLimit = 0 } = action; retriesLeft = retriesLeft ?? retryLimit; step.retriesLeft = retriesLeft; if (retriesLeft === 0) { return { retriesLeft: 0, shouldEnd: false }; } await delay(retryAfter); if (targetWorkflow || targetStep) { printActionsSeparator({ stepId, actionName: action.name, kind, logger: ctx.options.logger, }); } if (targetWorkflow) { const stepWorkflowResult = await runWorkflow({ workflowInput: targetWorkflow, ctx: targetCtx, skipLineSeparator: true, invocationContext: `Retry action for step ${stepId}`, }); ctx.executedSteps.push(stepWorkflowResult); } else if (targetStep) { const stepToRun = workflow?.steps.find((s) => s.stepId === targetStep); if (!stepToRun) { throw new Error(`Step ${targetStep} not found in workflow ${workflowId}`); } await runStep({ step: stepToRun, ctx: targetCtx, workflowId, }); } ctx.options.logger.output(`\n Retrying step ${blue(stepId)} (${retryLimit - retriesLeft + 1}/${retryLimit})\n`); return { stepResult: await runStep({ step, ctx, workflowId, retriesLeft: retriesLeft - 1, }), retriesLeft, }; } else if (type === 'end') { return { shouldEnd: true }; } else if (type === 'goto') { if (!targetWorkflow && !targetStep) { throw new Error('Either workflowId or stepId must be provided in goto action'); } if (targetWorkflow || targetStep) { printActionsSeparator({ stepId, actionName: action.name, kind, logger: ctx.options.logger, }); } const stepWorkflowResult = await runWorkflow({ workflowInput: targetWorkflow || workflow, ctx: targetCtx, fromStepId: targetStep, skipLineSeparator: true, invocationContext: `Goto from step ${stepId}`, }); ctx.executedSteps.push(stepWorkflowResult); return { shouldEnd: true }; } // stop at first matching action break; } } if (kind === 'failure') { return { shouldEnd: true }; } } } //# sourceMappingURL=run-step.js.map