From 56e6b9a3e27098909873384d30c00399c54e80a2 Mon Sep 17 00:00:00 2001 From: Alex Johnson Date: Thu, 13 Jun 2024 11:12:10 -0500 Subject: [PATCH] feat: add an allowFailure field to WorkflowStepSpec * do not fail the entire workflow if a step labeled with allowFailure throws an error Signed-off-by: Alex Johnson --- core/src/commands/workflow.ts | 13 ++++++ core/src/config/workflow.ts | 2 + core/test/unit/src/commands/workflow.ts | 29 +++++++++++++ core/test/unit/src/config/workflow.ts | 57 +++++++++++++++++++------ docs/reference/commands.md | 3 ++ docs/reference/workflow-config.md | 13 ++++++ 6 files changed, 105 insertions(+), 12 deletions(-) diff --git a/core/src/commands/workflow.ts b/core/src/commands/workflow.ts index 87999ad9466..690d3febb81 100644 --- a/core/src/commands/workflow.ts +++ b/core/src/commands/workflow.ts @@ -182,6 +182,16 @@ export class WorkflowCommand extends Command { } } catch (rawErr) { const err = toGardenError(rawErr) + if (step.continueOnError) { + result.steps[stepName] = { + number: index + 1, + outputs: { + stderr: err.toString(), + }, + log: stepBodyLog.toString((entry) => entry.level <= LogLevel.info), + } + continue + } garden.events.emit("workflowStepError", getStepEndEvent(index, stepStartedAt)) stepErrors[index] = [err] printStepDuration({ ...stepParams, success: false }) @@ -201,6 +211,9 @@ export class WorkflowCommand extends Command { stepBodyLog.root.storeEntries = initSaveLogState if (stepResult.errors && stepResult.errors.length > 0) { + if (step.continueOnError) { + continue + } garden.events.emit("workflowStepError", getStepEndEvent(index, stepStartedAt)) logErrors(outerLog, stepResult.errors, index, steps.length, step.description) stepErrors[index] = stepResult.errors diff --git a/core/src/config/workflow.ts b/core/src/config/workflow.ts index 25ed712d794..f6a1f435fb6 100644 --- a/core/src/config/workflow.ts +++ b/core/src/config/workflow.ts @@ -200,6 +200,7 @@ export interface WorkflowStepSpec { script?: string skip?: boolean when?: workflowStepModifier + continueOnError?: boolean } export const workflowStepSchema = createSchema({ @@ -265,6 +266,7 @@ export const workflowStepSchema = createSchema({ See the [workflows guide](${DOCS_BASE_URL}/using-garden/workflows#the-skip-and-when-options) for details and examples. `), + continueOnError: joi.boolean().description(`Set to true to continue if the step errors.`).default(false), }), xor: [["command", "script"]], }) diff --git a/core/test/unit/src/commands/workflow.ts b/core/test/unit/src/commands/workflow.ts index fd571b56cf4..dccfcd35be8 100644 --- a/core/test/unit/src/commands/workflow.ts +++ b/core/test/unit/src/commands/workflow.ts @@ -82,6 +82,35 @@ describe("RunWorkflowCommand", () => { expect(result.errors || []).to.eql([]) }) + it("should allow failure for error in step", async () => { + garden.setWorkflowConfigs([ + { + apiVersion: GardenApiVersion.v0, + name: "workflow-a", + kind: "Workflow", + internal: { + basePath: garden.projectRoot, + }, + files: [], + envVars: {}, + resources: defaultWorkflowResources, + steps: [ + { script: "echo error!; exit 1", continueOnError: true }, // <-- error thrown here + { command: ["echo", "success!"] }, + ], + }, + ]) + + const { result, errors } = await cmd.action({ ...defaultParams, args: { workflow: "workflow-a" } }) + + expect(result).to.exist + expect(errors).to.not.exist + expect(result?.steps).to.have.property("step-1") + expect(result?.steps["step-1"].outputs).to.have.property("stderr") + expect(result?.steps).to.have.property("step-2") + expect(result?.steps["step-2"].outputs).to.not.have.property("stderr") + }) + it("should add workflowStep metadata to log entries provided to steps", async () => { const _garden = await makeTestGardenA(undefined) // Ensure log entries are empty diff --git a/core/test/unit/src/config/workflow.ts b/core/test/unit/src/config/workflow.ts index 6a92171a259..d0869d42a68 100644 --- a/core/test/unit/src/config/workflow.ts +++ b/core/test/unit/src/config/workflow.ts @@ -9,7 +9,7 @@ import { expect } from "chai" import type { TestGarden } from "../../../helpers.js" import { expectError, getDataDir, makeTestGarden, makeTestGardenA } from "../../../helpers.js" -import type { WorkflowConfig, TriggerSpec } from "../../../../src/config/workflow.js" +import type { WorkflowConfig, WorkflowStepSpec, TriggerSpec } from "../../../../src/config/workflow.js" import { resolveWorkflowConfig, populateNamespaceForTriggers, @@ -39,6 +39,12 @@ describe("resolveWorkflowConfig", () => { keepAliveHours: 48, } + const defaultWorkflowStep: WorkflowStepSpec = { + skip: false, + when: "onSuccess", + continueOnError: false, + } + before(async () => { garden = await makeTestGardenA() garden["secrets"] = { foo: "bar", bar: "baz", baz: "banana" } @@ -55,8 +61,15 @@ describe("resolveWorkflowConfig", () => { description: "Sample workflow", envVars: {}, steps: [ - { description: "Deploy the stack", command: ["deploy"], skip: false, when: "onSuccess", envVars: {} }, - { command: ["test"], skip: false, when: "onSuccess", envVars: {} }, + { + ...defaultWorkflowStep, + description: "Deploy the stack", + command: ["deploy"], + skip: false, + when: "onSuccess", + envVars: {}, + }, + { ...defaultWorkflowStep, command: ["test"], skip: false, when: "onSuccess", envVars: {} }, ], triggers: [ { @@ -85,8 +98,15 @@ describe("resolveWorkflowConfig", () => { envVars: {}, limits: minimumWorkflowLimits, // <---- steps: [ - { description: "Deploy the stack", command: ["deploy"], skip: false, when: "onSuccess", envVars: {} }, - { command: ["test"], skip: false, when: "onSuccess", envVars: {} }, + { + ...defaultWorkflowStep, + description: "Deploy the stack", + command: ["deploy"], + skip: false, + when: "onSuccess", + envVars: {}, + }, + { ...defaultWorkflowStep, command: ["test"], skip: false, when: "onSuccess", envVars: {} }, ], triggers: [ { @@ -145,12 +165,13 @@ describe("resolveWorkflowConfig", () => { envVars: {}, steps: [ { + ...defaultWorkflowStep, description: "Deploy the stack", command: ["deploy", "${var.foo}"], skip: false, when: "onSuccess", }, - { script: "echo ${var.foo}", skip: false, when: "onSuccess" }, + { ...defaultWorkflowStep, script: "echo ${var.foo}", skip: false, when: "onSuccess" }, ], } @@ -169,8 +190,14 @@ describe("resolveWorkflowConfig", () => { envVars: {}, steps: [ - { description: "Deploy the stack", command: ["deploy"], skip: false, when: "onSuccess" }, - { command: ["test"], skip: false, when: "onSuccess" }, + { + ...defaultWorkflowStep, + description: "Deploy the stack", + command: ["deploy"], + skip: false, + when: "onSuccess", + }, + { ...defaultWorkflowStep, command: ["test"], skip: false, when: "onSuccess" }, ], } @@ -186,8 +213,14 @@ describe("resolveWorkflowConfig", () => { envVars: {}, steps: [ - { description: "Deploy the stack", command: ["deploy"], skip: false, when: "onSuccess" }, - { command: ["test"], skip: false, when: "onSuccess" }, + { + ...defaultWorkflowStep, + description: "Deploy the stack", + command: ["deploy"], + skip: false, + when: "onSuccess", + }, + { ...defaultWorkflowStep, command: ["test"], skip: false, when: "onSuccess" }, ], triggers: [ { @@ -221,8 +254,8 @@ describe("resolveWorkflowConfig", () => { ...defaults, ...config, steps: [ - { description: "Deploy the stack", command: ["deploy"], skip: false, when: "onSuccess", envVars: {} }, - { command: ["test"], skip: false, when: "onSuccess", envVars: {} }, + { ...defaultWorkflowStep, description: "Deploy the stack", command: ["deploy"], envVars: {} }, + { ...defaultWorkflowStep, command: ["test"], envVars: {} }, ], }) }) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 1e9d8602db2..cc0bb332dfa 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -2794,6 +2794,9 @@ workflowConfigs: # and examples. when: + # Set to true to continue if the step errors. + continueOnError: + # A list of triggers that determine when the workflow should be run, and which environment should be used (Garden # Cloud only). triggers: diff --git a/docs/reference/workflow-config.md b/docs/reference/workflow-config.md index b0537f1fdf6..0902c55655c 100644 --- a/docs/reference/workflow-config.md +++ b/docs/reference/workflow-config.md @@ -129,6 +129,9 @@ steps: # and examples. when: onSuccess + # Set to true to continue if the step errors. + continueOnError: false + # A list of triggers that determine when the workflow should be run, and which environment should be used (Garden # Cloud only). triggers: @@ -484,6 +487,16 @@ and examples. | -------- | ------------- | -------- | | `string` | `"onSuccess"` | No | +### `steps[].continueOnError` + +[steps](#steps) > continueOnError + +Set to true to continue if the step errors. + +| Type | Default | Required | +| --------- | ------- | -------- | +| `boolean` | `false` | No | + ### `triggers[]` A list of triggers that determine when the workflow should be run, and which environment should be used (Garden Cloud only).