diff --git a/src/cmd/run.go b/src/cmd/run.go index 11f0e31..3efc47e 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -56,6 +56,9 @@ func (i *listFlag) Set(value string) error { return nil } +// dryRun is a flag to only load / validate tasks without running commands +var dryRun bool + // setRunnerVariables provides a map of set variables from the command line var setRunnerVariables map[string]string @@ -140,7 +143,7 @@ var runCmd = &cobra.Command{ if len(args) > 0 { taskName = args[0] } - if err := runner.Run(tasksFile, taskName, setRunnerVariables); err != nil { + if err := runner.Run(tasksFile, taskName, setRunnerVariables, dryRun); err != nil { message.Fatalf(err, "Failed to run action: %s", err.Error()) } }, @@ -241,6 +244,7 @@ func init() { rootCmd.AddCommand(runCmd) runFlags := runCmd.Flags() runFlags.StringVarP(&config.TaskFileLocation, "file", "f", config.TasksYAML, lang.CmdRunFlag) + runFlags.BoolVar(&dryRun, "dry-run", false, lang.CmdRunDryRun) // Setup the --list flag flag.Var(&listTasks, "list", lang.CmdRunList) diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 1690316..bae376d 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -51,6 +51,7 @@ const ( CmdRunWithVarFlag = "Set the inputs for a task from the command line (KEY=value)" CmdRunList = "List available tasks in a task file" CmdRunListAll = "List all available tasks in a task file, including tasks from included files" + CmdRunDryRun = "Validate the task without actually running any commands" ) // Common Errors diff --git a/src/pkg/runner/actions.go b/src/pkg/runner/actions.go index 723d62a..4f0b91e 100644 --- a/src/pkg/runner/actions.go +++ b/src/pkg/runner/actions.go @@ -63,11 +63,10 @@ func (r *Runner) performAction(action types.Action, withs map[string]string, inp return err } } else { - err := RunAction(action.BaseAction, r.envFilePath, r.variableConfig) + err := RunAction(action.BaseAction, r.envFilePath, r.variableConfig, r.dryRun) if err != nil { return err } - } return nil } @@ -106,7 +105,7 @@ func getUniqueTaskActions(actions []types.Action) []types.Action { } // RunAction executes a specific action command, either wait or cmd. It handles variable loading environment variables and manages retries and timeouts -func RunAction[T any](action *types.BaseAction[T], envFilePath string, variableConfig *variables.VariableConfig[T]) error { +func RunAction[T any](action *types.BaseAction[T], envFilePath string, variableConfig *variables.VariableConfig[T], dryRun bool) error { var ( ctx context.Context cancel context.CancelFunc @@ -145,6 +144,19 @@ func RunAction[T any](action *types.BaseAction[T], envFilePath string, variableC action.SetVariables = []variables.Variable[T]{} } + if action.Description != "" { + cmdEscaped = action.Description + } else { + cmdEscaped = helpers.Truncate(cmd, 60, false) + } + + // if this is a dry run, print the command that would run and return + if dryRun { + message.SLog.Info(fmt.Sprintf("Dry-running %q", cmdEscaped)) + fmt.Println(cmd) + return nil + } + // load the contents of the env file into the Action + the MARU_ARCH if envFilePath != "" { envFilePath := filepath.Join(filepath.Dir(config.TaskFileLocation), envFilePath) @@ -155,19 +167,13 @@ func RunAction[T any](action *types.BaseAction[T], envFilePath string, variableC action.Env = append(action.Env, strings.Split(string(envFileContents), "\n")...) } - if action.Description != "" { - cmdEscaped = action.Description - } else { - cmdEscaped = helpers.Truncate(cmd, 60, false) - } - - spinner := message.NewProgressSpinner("Running \"%s\"", cmdEscaped) + spinner := message.NewProgressSpinner("Running %q", cmdEscaped) cfg := GetBaseActionCfg(types.ActionDefaults{}, *action, variableConfig.GetSetVariables()) if cmd = exec.MutateCommand(cmd, cfg.Shell); err != nil { message.SLog.Debug(err.Error()) - spinner.Failf("Error mutating command: %s", cmdEscaped) + spinner.Failf("Error mutating command: %q", cmdEscaped) } // Template dir string diff --git a/src/pkg/runner/runner.go b/src/pkg/runner/runner.go index 8348c60..0a1d686 100644 --- a/src/pkg/runner/runner.go +++ b/src/pkg/runner/runner.go @@ -27,10 +27,14 @@ type Runner struct { TaskNameMap map[string]bool envFilePath string variableConfig *variables.VariableConfig[variables.ExtraVariableInfo] + dryRun bool } // Run runs a task from tasks file -func Run(tasksFile types.TasksFile, taskName string, setVariables map[string]string) error { +func Run(tasksFile types.TasksFile, taskName string, setVariables map[string]string, dryRun bool) error { + if dryRun { + message.SLog.Info("Dry-run has been set - only printing the commands that would run:") + } // Populate the variables loaded in the root task file rootVariables := tasksFile.Variables @@ -61,6 +65,7 @@ func Run(tasksFile types.TasksFile, taskName string, setVariables map[string]str TasksFile: tasksFile, TaskNameMap: map[string]bool{}, variableConfig: combinedVariableConfig, + dryRun: dryRun, } task, err := runner.getTask(taskName) diff --git a/src/test/e2e/runner_test.go b/src/test/e2e/runner_test.go index e9bd4fd..6248bbf 100644 --- a/src/test/e2e/runner_test.go +++ b/src/test/e2e/runner_test.go @@ -423,7 +423,7 @@ func TestTaskRunner(t *testing.T) { t.Parallel() stdOut, stdErr, err := e2e.Maru("run", "true-conditional-nested-nested-nested-task-comp-var-inputs", "--file", "src/test/tasks/conditionals/tasks.yaml") require.NoError(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "\"input val2 equals 5 and variable VAL1 equals 5\"") + require.Contains(t, stdErr, "Completed \"echo \\\"input val2 equals 5 and variable VAL1 equals 5\\\"\"") }) t.Run("test calling a task with nested task calling a task with false conditional comparing variables and inputs", func(t *testing.T) { t.Parallel() @@ -436,14 +436,14 @@ func TestTaskRunner(t *testing.T) { t.Parallel() stdOut, stdErr, err := e2e.Maru("run", "true-condition-var-as-input-original-syntax-nested-nested-with-comp", "--file", "src/test/tasks/conditionals/tasks.yaml") require.NoError(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "\"input val2 equals 5 and variable VAL1 equals 5\"") + require.Contains(t, stdErr, "Completed \"echo \\\"input val2 equals 5 and variable VAL1 equals 5\\\"\"") }) t.Run("test calling a task with nested task calling a task with new style var as input true conditional comparing variables and inputs", func(t *testing.T) { t.Parallel() stdOut, stdErr, err := e2e.Maru("run", "true-condition-var-as-input-new-syntax-nested-nested-with-comp", "--file", "src/test/tasks/conditionals/tasks.yaml") require.NoError(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "\"input val2 equals 5 and variable VAL1 equals 5\"") + require.Contains(t, stdErr, "Completed \"echo \\\"input val2 equals 5 and variable VAL1 equals 5\\\"\"") }) t.Run("run successful pattern", func(t *testing.T) { @@ -461,4 +461,13 @@ func TestTaskRunner(t *testing.T) { require.Error(t, err, stdOut, stdErr) require.Contains(t, stdErr, "\"HELLO\" does not match pattern \"^HELLO$\"") }) + + t.Run("dry run", func(t *testing.T) { + t.Parallel() + + stdOut, stdErr, err := e2e.Maru("run", "--dry-run", "--file", "src/test/tasks/tasks.yaml", "env-from-file") + require.NoError(t, err, stdOut, stdErr) + require.Contains(t, stdErr, "Dry-running \"echo $MARU_ARCH\"") + require.Contains(t, stdOut, "echo env var from calling task - $SECRET_KEY") + }) }