From cb2139a8b4c93f4acbcb5a58959674f0b58e46d3 Mon Sep 17 00:00:00 2001 From: DmitryAnansky Date: Fri, 24 Jul 2026 17:56:39 +0300 Subject: [PATCH 1/2] fix(respect): referenced workflow execution flow inconsistency --- .../child-workflow-failure-stops-parent.md | 6 ++ .../__tests__/flow-runner/run-step.test.ts | 82 +++++++++++++++++++ .../src/modules/flow-runner/run-step.ts | 20 +++++ .../__snapshots__/max-steps.test.ts.snap | 3 + tests/e2e/respect/max-steps/arazzo.yaml | 4 + 5 files changed, 115 insertions(+) create mode 100644 .changeset/child-workflow-failure-stops-parent.md diff --git a/.changeset/child-workflow-failure-stops-parent.md b/.changeset/child-workflow-failure-stops-parent.md new file mode 100644 index 0000000000..1020f8051e --- /dev/null +++ b/.changeset/child-workflow-failure-stops-parent.md @@ -0,0 +1,6 @@ +--- +'@redocly/respect-core': patch +'@redocly/cli': patch +--- + +Fixed `respect` to stop parent workflow execution when a step that references another workflow fails. Previously, the next steps of the parent workflow were still executed after the referenced workflow failed. diff --git a/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts b/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts index 89189658f4..cffaca68e1 100644 --- a/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts +++ b/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts @@ -2613,6 +2613,11 @@ describe('runStep', () => { } as unknown as TestContext; vi.mocked(resolveWorkflowContext).mockResolvedValueOnce(localCTX); + vi.mocked(runWorkflow).mockResolvedValueOnce({ + type: 'workflow', + workflowId: 'reusable-workflow', + executedSteps: [], + } as unknown as WorkflowExecutionResult); await runStep({ step, @@ -2625,6 +2630,72 @@ describe('runStep', () => { expect(localCTX.$steps['get-bird'].outputs).toEqual({ stepOutput: 'Hello, world!' }); }); + it('should end parent workflow when child workflow step has failed steps', async () => { + const step: Step = { + stepId: 'call-child-workflow', + workflowId: 'child-workflow', + checks: [], + response: {} as any, + }; + const localCTX = { + executedSteps: [], + $steps: {}, + workflows: [ + { + workflowId: 'parent-workflow', + steps: [step], + }, + { + workflowId: 'child-workflow', + steps: [ + { + stepId: 'child-step', + 'x-operation': { + url: 'http://localhost:3000/delete-mock', + method: 'delete', + }, + successCriteria: [{ condition: '$statusCode == 204' }], + checks: [], + }, + ], + }, + ], + options: { + filePath: 'runStepTest.yml', + logger: logger, + }, + severity: DEFAULT_SEVERITY_CONFIGURATION, + } as unknown as TestContext; + + vi.mocked(resolveWorkflowContext).mockResolvedValueOnce(localCTX); + vi.mocked(runWorkflow).mockResolvedValueOnce({ + type: 'workflow', + workflowId: 'child-workflow', + executedSteps: [ + { + stepId: 'child-step', + checks: [ + { + name: CHECKS.SUCCESS_CRITERIA_CHECK, + message: 'Checking simple criteria: {"condition":"$statusCode == 204"}', + passed: false, + severity: 'error', + }, + ], + }, + ], + } as unknown as WorkflowExecutionResult); + + const result = await runStep({ + step, + ctx: localCTX, + workflowId: 'parent-workflow', + executedStepsCount: { value: 0 }, + }); + + expect(result).toEqual({ shouldEnd: true }); + }); + it('should run step with workflowId from external workflowSpec', async () => { const step: Step = { stepId: 'get-bird', @@ -3016,6 +3087,12 @@ describe('runStep', () => { $outputs: {}, } as unknown as TestContext; + vi.mocked(runWorkflow).mockResolvedValueOnce({ + type: 'workflow', + workflowId: 'reusable-external-workflow', + executedSteps: [], + } as unknown as WorkflowExecutionResult); + await runStep({ step, ctx: localCTX, @@ -3462,6 +3539,11 @@ describe('runStep', () => { vi.mocked(resolveWorkflowContext).mockImplementation((): any => { return { ...localCTX }; }); + vi.mocked(runWorkflow).mockResolvedValueOnce({ + type: 'workflow', + workflowId: 'reusable-external-workflow', + executedSteps: [], + } as unknown as WorkflowExecutionResult); await runStep({ step, diff --git a/packages/respect-core/src/modules/flow-runner/run-step.ts b/packages/respect-core/src/modules/flow-runner/run-step.ts index 7493a89acc..6fe228e045 100644 --- a/packages/respect-core/src/modules/flow-runner/run-step.ts +++ b/packages/respect-core/src/modules/flow-runner/run-step.ts @@ -26,6 +26,7 @@ import { printActionsSeparator, printUnknownStep, } from '../logger-output/helpers.js'; +import { calculateTotals } from '../logger-output/index.js'; import { evaluateRuntimeExpressionPayload } from '../runtime-expressions/index.js'; import { Timer } from '../timeout-timer/timer.js'; import { callAPIAndAnalyzeResults } from './call-api-and-analyze-results.js'; @@ -166,6 +167,25 @@ export async function runStep({ } } + const childWorkflowFailed = calculateTotals([stepWorkflowResult]).steps.failed > 0; + + if (childWorkflowFailed) { + const result = await runActions(failureActionsToRun, 'failure', executedStepsCount); + if (result?.retriesLeft && result.retriesLeft > 0) { + return result.stepResult; + } + if (result?.shouldEnd) { + return { shouldEnd: true }; + } + } + + if (successActionsToRun.length && !childWorkflowFailed) { + const result = await runActions(successActionsToRun, 'success', executedStepsCount); + if (result?.shouldEnd) { + return { shouldEnd: true }; + } + } + return { shouldEnd: false }; } ctx.executedSteps.push(step); diff --git a/tests/e2e/respect/max-steps/__snapshots__/max-steps.test.ts.snap b/tests/e2e/respect/max-steps/__snapshots__/max-steps.test.ts.snap index 3d8c5da860..5f35086ebf 100644 --- a/tests/e2e/respect/max-steps/__snapshots__/max-steps.test.ts.snap +++ b/tests/e2e/respect/max-steps/__snapshots__/max-steps.test.ts.snap @@ -32,6 +32,8 @@ exports[`should quit an infinite loop on REDOCLY_CLI_RESPECT_MAX_STEPS 1`] = ` + Running failure action continue for the step with-nested + ✗ GET /ping - step ping     Request URL: https://bad-api-url.com/api/ping @@ -96,6 +98,7 @@ exports[`should quit an infinite loop on REDOCLY_CLI_RESPECT_MAX_STEPS 1`] = ` +   Failed tests info: diff --git a/tests/e2e/respect/max-steps/arazzo.yaml b/tests/e2e/respect/max-steps/arazzo.yaml index 55ab2214e8..2d2b33d469 100644 --- a/tests/e2e/respect/max-steps/arazzo.yaml +++ b/tests/e2e/respect/max-steps/arazzo.yaml @@ -21,6 +21,10 @@ workflows: - stepId: with-nested description: With nested workflow workflowId: nested-workflow + onFailure: + - name: continue + type: goto + stepId: ping - stepId: ping description: Ping the API From 4c88bfb01162a6e084e37b867fd88a0072626ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81=C4=99kawa?= <164185257+JLekawa@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:13:33 +0200 Subject: [PATCH 2/2] Apply suggestion from @JLekawa --- .changeset/child-workflow-failure-stops-parent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/child-workflow-failure-stops-parent.md b/.changeset/child-workflow-failure-stops-parent.md index 1020f8051e..a2879f59b0 100644 --- a/.changeset/child-workflow-failure-stops-parent.md +++ b/.changeset/child-workflow-failure-stops-parent.md @@ -3,4 +3,4 @@ '@redocly/cli': patch --- -Fixed `respect` to stop parent workflow execution when a step that references another workflow fails. Previously, the next steps of the parent workflow were still executed after the referenced workflow failed. +Fixed an issue in `respect` where the execution of parent workflow's steps didn't halt after a step that referenced another workflow had failed.