diff --git a/cmd/workflow.go b/cmd/workflow.go index c19241dc1..206382eed 100644 --- a/cmd/workflow.go +++ b/cmd/workflow.go @@ -16,7 +16,10 @@ var workflowCmd = &cobra.Command{ Example: "atmos workflow\n" + "atmos workflow -f \n" + "atmos workflow -f -s \n" + - "atmos workflow -f --from-step ", + "atmos workflow -f --from-step \n\n" + + "To resume the workflow from this step, run:\n" + + "atmos workflow deploy-infra -f workflow1 --from-step deploy-vpc\n\n" + + "For more details refer to https://atmos.tools/cli/commands/workflow/", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { err := e.ExecuteWorkflowCmd(cmd, args) diff --git a/internal/exec/workflow_utils.go b/internal/exec/workflow_utils.go index 08fed48cd..b788db98d 100644 --- a/internal/exec/workflow_utils.go +++ b/internal/exec/workflow_utils.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "path/filepath" "sort" "strings" @@ -70,11 +71,10 @@ func ExecuteWorkflow( commandType = "atmos" } + var err error if commandType == "shell" { commandName := fmt.Sprintf("%s-step-%d", workflow, stepIdx) - if err := ExecuteShell(cliConfig, command, commandName, ".", []string{}, dryRun); err != nil { - return err - } + err = ExecuteShell(cliConfig, command, commandName, ".", []string{}, dryRun) } else if commandType == "atmos" { args := strings.Fields(command) @@ -101,12 +101,29 @@ func ExecuteWorkflow( logFunc(cliConfig, fmt.Sprintf("Stack: %s", finalStack)) } - if err := ExecuteShellCommand(cliConfig, "atmos", args, ".", []string{}, dryRun, ""); err != nil { - return err - } + err = ExecuteShellCommand(cliConfig, "atmos", args, ".", []string{}, dryRun, "") } else { return fmt.Errorf("invalid workflow step type '%s'. Supported types are 'atmos' and 'shell'", commandType) } + + if err != nil { + workflowFileName := filepath.Base(workflowPath) + workflowFileName = strings.TrimSuffix(workflowFileName, filepath.Ext(workflowFileName)) + + failedMsg := color.New(color.FgRed).Sprintf("\nStep '%s' failed!", step.Name) + + u.LogDebug(cliConfig, fmt.Sprintf("\nCommand failed: %s", command)) + u.LogDebug(cliConfig, fmt.Sprintf("Error: %v", err)) + + resumeMsg := color.New(color.FgGreen).Sprintf( + "\nTo resume the workflow from this step, run:\natmos workflow %s -f %s --from-step %s", + workflow, + workflowFileName, + step.Name, + ) + + return fmt.Errorf("%s\n%s", failedMsg, resumeMsg) + } } return nil diff --git a/website/docs/core-concepts/workflows/workflows.mdx b/website/docs/core-concepts/workflows/workflows.mdx index c419c6555..f13a6b2e7 100644 --- a/website/docs/core-concepts/workflows/workflows.mdx +++ b/website/docs/core-concepts/workflows/workflows.mdx @@ -371,6 +371,40 @@ The Atmos stack used by the workflow commands of type `atmos` can be specified i atmos workflow my-workflow -f workflow1 -s tenant1-ue2-dev ``` +## Workflow Failure Handling and Resume + +When a workflow step fails, Atmos will: +1. Display which step failed +2. Show the exact command that failed +3. Provide a ready-to-use command to resume the workflow from the failed step + +Given this workflow: + +```yaml title="stacks/workflows/networking.yaml" +workflows: + provision-vpcs: + description: "Deploy vpc components" + steps: + - command: terraform plan vpc -s plat-ue2-dev + name: step-1 + - command: terraform plan vpc -s plat-ue2-staging + name: step-2 + - command: terraform plan vpc -s plat-ue2-prod + name: step-3 +``` + +If step-2 fails, you'll see: + +```console +Step 'step-2' failed! + +Command failed: +terraform plan vpc -s plat-ue2-staging + +To resume the workflow from this step, run: +atmos workflow provision-vpcs -f networking --from-step step-2 +``` + ### Stack Precedence The stack defined inline in the command itself has the lowest priority, it can and will be overridden by any other stack definition.