From 61f9844f6be04a518c10c12abeda960672bead5f Mon Sep 17 00:00:00 2001 From: "ben.hansen" Date: Thu, 3 Sep 2026 21:59:29 +0000 Subject: [PATCH 1/5] Add verbose error guidance to AIR commands --- .nextchanges/cli/verbose-error-tip.md | 1 + cmd/root/root.go | 66 +++++++++++++++++++++- cmd/root/root_test.go | 80 +++++++++++++++++++++++++++ experimental/air/cmd/air.go | 2 + experimental/air/cmd/air_test.go | 8 +++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 .nextchanges/cli/verbose-error-tip.md diff --git a/.nextchanges/cli/verbose-error-tip.md b/.nextchanges/cli/verbose-error-tip.md new file mode 100644 index 00000000000..10ebe092316 --- /dev/null +++ b/.nextchanges/cli/verbose-error-tip.md @@ -0,0 +1 @@ +* Add an AIR-scoped `-v` alias for `--debug` and show failed AIR commands how to rerun with detailed diagnostics. diff --git a/cmd/root/root.go b/cmd/root/root.go index 55e78e58633..b5e92c4ceef 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -58,6 +58,9 @@ func New(ctx context.Context) *cobra.Command { var err error ctx := cmd.Context() + if verboseCommand := verboseErrorTipCommand(cmd); verboseCommand != nil { + logFlags.debug = logFlags.debug || cmd.Flag("verbose").Value.String() == "true" + } // Configure command IO ctx, err = outputFlag.initializeIO(ctx, cmd) @@ -74,7 +77,7 @@ func New(ctx context.Context) *cobra.Command { logger := log.GetLogger(ctx) logger.Info("start", slog.String("version", build.GetInfo().Version), - slog.String("args", strings.Join(os.Args, ", "))) + slog.String("args", strings.Join(commandArgsForLogging(cmd, os.Args), ", "))) // Configure our user agent with the command that's about to be executed. ctx = withCommandInUserAgent(ctx, cmd) @@ -164,6 +167,7 @@ Stack Trace: err = auth.AppendAccountHostHint(cmdctx.WorkspaceClient(cmd.Context()).Config, err) } fmt.Fprintf(cmd.ErrOrStderr(), "Error: %s\n", err.Error()) + printVerboseErrorTip(cmd) } // Log exit status and error @@ -209,6 +213,66 @@ Stack Trace: return err } +const verboseErrorTipAnnotation = "databricks.cli.verbose-error-tip" + +// EnableVerboseErrorTip adds an AIR-scoped verbose flag and opts the command +// into rerun guidance when one of its descendants fails. +func EnableVerboseErrorTip(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + cmd.Annotations[verboseErrorTipAnnotation] = "true" + cmd.PersistentFlags().BoolP("verbose", "v", false, "enable debug logging") +} + +func verboseErrorTipCommand(cmd *cobra.Command) *cobra.Command { + for current := cmd; current != nil; current = current.Parent() { + if current.Annotations[verboseErrorTipAnnotation] == "true" { + return current + } + } + return nil +} + +func commandArgsForLogging(cmd *cobra.Command, args []string) []string { + if verboseErrorTipCommand(cmd) == nil { + return args + } + + redacted := append([]string(nil), args...) + for i := 0; i < len(redacted); i++ { + switch { + case redacted[i] == "--override" && i+1 < len(redacted): + redacted[i+1] = "" + i++ + case strings.HasPrefix(redacted[i], "--override="): + redacted[i] = "--override=" + } + } + return redacted +} + +func printVerboseErrorTip(cmd *cobra.Command) { + verboseCommand := verboseErrorTipCommand(cmd) + if verboseCommand == nil { + return + } + + debugFlag := cmd.Root().PersistentFlags().Lookup("debug") + verboseFlag := cmd.Flag("verbose") + if debugFlag.Value.String() == "true" || verboseFlag.Value.String() == "true" { + return + } + + commandPrefix := verboseCommand.CommandPath() + command := commandPrefix + " -v" + strings.TrimPrefix(cmd.CommandPath(), commandPrefix) + fmt.Fprintf( + cmd.ErrOrStderr(), + "\nTip: use the -v (verbose) flag immediately after air to see more details and a trace of this error:\n %s …\n", + command, + ) +} + // This function is used to report an unknown subcommand. // It is used in the [cobra.Command.RunE] field of commands that have subcommands. // If user provided a valid subcommand, RunE for the diff --git a/cmd/root/root_test.go b/cmd/root/root_test.go index ce1584eacd4..991f5e2a517 100644 --- a/cmd/root/root_test.go +++ b/cmd/root/root_test.go @@ -177,3 +177,83 @@ func TestExecuteErrAlreadyPrintedNotEnriched(t *testing.T) { require.Error(t, err) assert.Empty(t, stderr.String()) } + +func TestExecuteVerboseErrorTip(t *testing.T) { + tests := []struct { + name string + args []string + wantTip bool + wantCmd string + }{ + { + name: "default", + args: []string{"experimental", "air", "fail", "secret-value"}, + wantTip: true, + wantCmd: "databricks experimental air -v fail …", + }, + { + name: "short verbose flag", + args: []string{"experimental", "air", "-v", "fail"}, + }, + { + name: "long debug flag", + args: []string{"--debug", "experimental", "air", "fail"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stderr := &bytes.Buffer{} + cmd := New(t.Context()) + failCommand := &cobra.Command{ + Use: "fail [arg]", + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("failed") + }, + } + airCommand := &cobra.Command{Use: "air"} + EnableVerboseErrorTip(airCommand) + airCommand.AddCommand(failCommand) + experimentalCommand := &cobra.Command{Use: "experimental"} + experimentalCommand.AddCommand(airCommand) + cmd.AddCommand(experimentalCommand) + cmd.SetArgs(tt.args) + cmd.SetErr(stderr) + + err := Execute(t.Context(), cmd) + require.Error(t, err) + if tt.wantTip { + assert.Contains(t, stderr.String(), "use the -v (verbose) flag") + assert.Contains(t, stderr.String(), tt.wantCmd) + assert.NotContains(t, stderr.String(), "secret-value") + } else { + assert.NotContains(t, stderr.String(), "use the -v (verbose) flag") + } + }) + } +} + +func TestVersionShorthandUnaffectedByAirVerboseFlag(t *testing.T) { + stdout := &bytes.Buffer{} + cmd := New(t.Context()) + cmd.SetArgs([]string{"-v"}) + cmd.SetOut(stdout) + + err := Execute(t.Context(), cmd) + + require.NoError(t, err) + assert.Contains(t, stdout.String(), "Databricks CLI v") +} + +func TestCommandArgsForLoggingRedactsAirOverrides(t *testing.T) { + airCommand := &cobra.Command{Use: "air"} + EnableVerboseErrorTip(airCommand) + runCommand := &cobra.Command{Use: "run"} + airCommand.AddCommand(runCommand) + + args := []string{"databricks", "experimental", "air", "run", "--override", "api_token=secret", "--override=other_secret=value"} + redacted := commandArgsForLogging(runCommand, args) + + assert.Equal(t, []string{"databricks", "experimental", "air", "run", "--override", "", "--override="}, redacted) + assert.Equal(t, "api_token=secret", args[5]) +} diff --git a/experimental/air/cmd/air.go b/experimental/air/cmd/air.go index 3ea28404b6b..5b3b4d1e2be 100644 --- a/experimental/air/cmd/air.go +++ b/experimental/air/cmd/air.go @@ -1,6 +1,7 @@ package aircmd import ( + "github.com/databricks/cli/cmd/root" "github.com/spf13/cobra" ) @@ -14,6 +15,7 @@ func New() *cobra.Command { This command set is the Go port of the standalone Python "air" CLI. It is experimental and may change in future versions.`, } + root.EnableVerboseErrorTip(cmd) cmd.AddCommand(newRunCommand()) cmd.AddCommand(newGetCommand()) diff --git a/experimental/air/cmd/air_test.go b/experimental/air/cmd/air_test.go index 1843acfe900..d4a3d0a1b04 100644 --- a/experimental/air/cmd/air_test.go +++ b/experimental/air/cmd/air_test.go @@ -20,3 +20,11 @@ func TestNewRegistersAllSubcommands(t *testing.T) { } assert.Len(t, registered, len(want), "unexpected number of subcommands") } + +func TestNewRegistersVerboseFlag(t *testing.T) { + flag := New().PersistentFlags().Lookup("verbose") + + if assert.NotNil(t, flag) { + assert.Equal(t, "v", flag.Shorthand) + } +} From 9f230327ab41f45b9236f3f29ff791f8f12de59d Mon Sep 17 00:00:00 2001 From: "ben.hansen" Date: Thu, 3 Sep 2026 23:28:06 +0000 Subject: [PATCH 2/5] Use debug flag for Databricks CLI diagnostics --- .nextchanges/cli/verbose-error-tip.md | 2 +- cmd/root/root.go | 41 +++++++++++---------------- cmd/root/root_test.go | 28 ++++-------------- experimental/air/cmd/air.go | 2 +- experimental/air/cmd/air_test.go | 8 ------ 5 files changed, 25 insertions(+), 56 deletions(-) diff --git a/.nextchanges/cli/verbose-error-tip.md b/.nextchanges/cli/verbose-error-tip.md index 10ebe092316..3c66909db88 100644 --- a/.nextchanges/cli/verbose-error-tip.md +++ b/.nextchanges/cli/verbose-error-tip.md @@ -1 +1 @@ -* Add an AIR-scoped `-v` alias for `--debug` and show failed AIR commands how to rerun with detailed diagnostics. +* Show failed AIR commands how to rerun with `--debug` for detailed diagnostics. diff --git a/cmd/root/root.go b/cmd/root/root.go index b5e92c4ceef..8808fc44084 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -58,10 +58,6 @@ func New(ctx context.Context) *cobra.Command { var err error ctx := cmd.Context() - if verboseCommand := verboseErrorTipCommand(cmd); verboseCommand != nil { - logFlags.debug = logFlags.debug || cmd.Flag("verbose").Value.String() == "true" - } - // Configure command IO ctx, err = outputFlag.initializeIO(ctx, cmd) if err != nil { @@ -167,7 +163,7 @@ Stack Trace: err = auth.AppendAccountHostHint(cmdctx.WorkspaceClient(cmd.Context()).Config, err) } fmt.Fprintf(cmd.ErrOrStderr(), "Error: %s\n", err.Error()) - printVerboseErrorTip(cmd) + printDebugErrorTip(cmd) } // Log exit status and error @@ -213,29 +209,28 @@ Stack Trace: return err } -const verboseErrorTipAnnotation = "databricks.cli.verbose-error-tip" +const debugErrorTipAnnotation = "databricks.cli.debug-error-tip" -// EnableVerboseErrorTip adds an AIR-scoped verbose flag and opts the command -// into rerun guidance when one of its descendants fails. -func EnableVerboseErrorTip(cmd *cobra.Command) { +// EnableDebugErrorTip opts the command into debug rerun guidance when one of +// its descendants fails. +func EnableDebugErrorTip(cmd *cobra.Command) { if cmd.Annotations == nil { cmd.Annotations = make(map[string]string) } - cmd.Annotations[verboseErrorTipAnnotation] = "true" - cmd.PersistentFlags().BoolP("verbose", "v", false, "enable debug logging") + cmd.Annotations[debugErrorTipAnnotation] = "true" } -func verboseErrorTipCommand(cmd *cobra.Command) *cobra.Command { +func debugErrorTipEnabled(cmd *cobra.Command) bool { for current := cmd; current != nil; current = current.Parent() { - if current.Annotations[verboseErrorTipAnnotation] == "true" { - return current + if current.Annotations[debugErrorTipAnnotation] == "true" { + return true } } - return nil + return false } func commandArgsForLogging(cmd *cobra.Command, args []string) []string { - if verboseErrorTipCommand(cmd) == nil { + if !debugErrorTipEnabled(cmd) { return args } @@ -252,23 +247,21 @@ func commandArgsForLogging(cmd *cobra.Command, args []string) []string { return redacted } -func printVerboseErrorTip(cmd *cobra.Command) { - verboseCommand := verboseErrorTipCommand(cmd) - if verboseCommand == nil { +func printDebugErrorTip(cmd *cobra.Command) { + if !debugErrorTipEnabled(cmd) { return } debugFlag := cmd.Root().PersistentFlags().Lookup("debug") - verboseFlag := cmd.Flag("verbose") - if debugFlag.Value.String() == "true" || verboseFlag.Value.String() == "true" { + if debugFlag.Value.String() == "true" { return } - commandPrefix := verboseCommand.CommandPath() - command := commandPrefix + " -v" + strings.TrimPrefix(cmd.CommandPath(), commandPrefix) + commandPrefix := cmd.Root().CommandPath() + command := commandPrefix + " --debug" + strings.TrimPrefix(cmd.CommandPath(), commandPrefix) fmt.Fprintf( cmd.ErrOrStderr(), - "\nTip: use the -v (verbose) flag immediately after air to see more details and a trace of this error:\n %s …\n", + "\nTip: use the --debug flag to see more details and a trace of this error:\n %s …\n", command, ) } diff --git a/cmd/root/root_test.go b/cmd/root/root_test.go index 991f5e2a517..9d59c3edea0 100644 --- a/cmd/root/root_test.go +++ b/cmd/root/root_test.go @@ -178,7 +178,7 @@ func TestExecuteErrAlreadyPrintedNotEnriched(t *testing.T) { assert.Empty(t, stderr.String()) } -func TestExecuteVerboseErrorTip(t *testing.T) { +func TestExecuteDebugErrorTip(t *testing.T) { tests := []struct { name string args []string @@ -189,11 +189,7 @@ func TestExecuteVerboseErrorTip(t *testing.T) { name: "default", args: []string{"experimental", "air", "fail", "secret-value"}, wantTip: true, - wantCmd: "databricks experimental air -v fail …", - }, - { - name: "short verbose flag", - args: []string{"experimental", "air", "-v", "fail"}, + wantCmd: "databricks --debug experimental air fail …", }, { name: "long debug flag", @@ -212,7 +208,7 @@ func TestExecuteVerboseErrorTip(t *testing.T) { }, } airCommand := &cobra.Command{Use: "air"} - EnableVerboseErrorTip(airCommand) + EnableDebugErrorTip(airCommand) airCommand.AddCommand(failCommand) experimentalCommand := &cobra.Command{Use: "experimental"} experimentalCommand.AddCommand(airCommand) @@ -223,31 +219,19 @@ func TestExecuteVerboseErrorTip(t *testing.T) { err := Execute(t.Context(), cmd) require.Error(t, err) if tt.wantTip { - assert.Contains(t, stderr.String(), "use the -v (verbose) flag") + assert.Contains(t, stderr.String(), "use the --debug flag") assert.Contains(t, stderr.String(), tt.wantCmd) assert.NotContains(t, stderr.String(), "secret-value") } else { - assert.NotContains(t, stderr.String(), "use the -v (verbose) flag") + assert.NotContains(t, stderr.String(), "use the --debug flag") } }) } } -func TestVersionShorthandUnaffectedByAirVerboseFlag(t *testing.T) { - stdout := &bytes.Buffer{} - cmd := New(t.Context()) - cmd.SetArgs([]string{"-v"}) - cmd.SetOut(stdout) - - err := Execute(t.Context(), cmd) - - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Databricks CLI v") -} - func TestCommandArgsForLoggingRedactsAirOverrides(t *testing.T) { airCommand := &cobra.Command{Use: "air"} - EnableVerboseErrorTip(airCommand) + EnableDebugErrorTip(airCommand) runCommand := &cobra.Command{Use: "run"} airCommand.AddCommand(runCommand) diff --git a/experimental/air/cmd/air.go b/experimental/air/cmd/air.go index 5b3b4d1e2be..ff7c3449793 100644 --- a/experimental/air/cmd/air.go +++ b/experimental/air/cmd/air.go @@ -15,7 +15,7 @@ func New() *cobra.Command { This command set is the Go port of the standalone Python "air" CLI. It is experimental and may change in future versions.`, } - root.EnableVerboseErrorTip(cmd) + root.EnableDebugErrorTip(cmd) cmd.AddCommand(newRunCommand()) cmd.AddCommand(newGetCommand()) diff --git a/experimental/air/cmd/air_test.go b/experimental/air/cmd/air_test.go index d4a3d0a1b04..1843acfe900 100644 --- a/experimental/air/cmd/air_test.go +++ b/experimental/air/cmd/air_test.go @@ -20,11 +20,3 @@ func TestNewRegistersAllSubcommands(t *testing.T) { } assert.Len(t, registered, len(want), "unexpected number of subcommands") } - -func TestNewRegistersVerboseFlag(t *testing.T) { - flag := New().PersistentFlags().Lookup("verbose") - - if assert.NotNil(t, flag) { - assert.Equal(t, "v", flag.Shorthand) - } -} From fe34f8ffc722c6490df4ead3f5124ab600ff5cac Mon Sep 17 00:00:00 2001 From: "ben.hansen" Date: Thu, 3 Sep 2026 23:44:19 +0000 Subject: [PATCH 3/5] Scope error guidance to AIR commands --- .nextchanges/cli/verbose-error-tip.md | 1 - cmd/root/root.go | 61 +------------------------ cmd/root/root_test.go | 64 --------------------------- experimental/air/cmd/air.go | 36 ++++++++++++++- experimental/air/cmd/air_test.go | 48 ++++++++++++++++++++ 5 files changed, 84 insertions(+), 126 deletions(-) delete mode 100644 .nextchanges/cli/verbose-error-tip.md diff --git a/.nextchanges/cli/verbose-error-tip.md b/.nextchanges/cli/verbose-error-tip.md deleted file mode 100644 index 3c66909db88..00000000000 --- a/.nextchanges/cli/verbose-error-tip.md +++ /dev/null @@ -1 +0,0 @@ -* Show failed AIR commands how to rerun with `--debug` for detailed diagnostics. diff --git a/cmd/root/root.go b/cmd/root/root.go index 8808fc44084..55e78e58633 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -58,6 +58,7 @@ func New(ctx context.Context) *cobra.Command { var err error ctx := cmd.Context() + // Configure command IO ctx, err = outputFlag.initializeIO(ctx, cmd) if err != nil { @@ -73,7 +74,7 @@ func New(ctx context.Context) *cobra.Command { logger := log.GetLogger(ctx) logger.Info("start", slog.String("version", build.GetInfo().Version), - slog.String("args", strings.Join(commandArgsForLogging(cmd, os.Args), ", "))) + slog.String("args", strings.Join(os.Args, ", "))) // Configure our user agent with the command that's about to be executed. ctx = withCommandInUserAgent(ctx, cmd) @@ -163,7 +164,6 @@ Stack Trace: err = auth.AppendAccountHostHint(cmdctx.WorkspaceClient(cmd.Context()).Config, err) } fmt.Fprintf(cmd.ErrOrStderr(), "Error: %s\n", err.Error()) - printDebugErrorTip(cmd) } // Log exit status and error @@ -209,63 +209,6 @@ Stack Trace: return err } -const debugErrorTipAnnotation = "databricks.cli.debug-error-tip" - -// EnableDebugErrorTip opts the command into debug rerun guidance when one of -// its descendants fails. -func EnableDebugErrorTip(cmd *cobra.Command) { - if cmd.Annotations == nil { - cmd.Annotations = make(map[string]string) - } - cmd.Annotations[debugErrorTipAnnotation] = "true" -} - -func debugErrorTipEnabled(cmd *cobra.Command) bool { - for current := cmd; current != nil; current = current.Parent() { - if current.Annotations[debugErrorTipAnnotation] == "true" { - return true - } - } - return false -} - -func commandArgsForLogging(cmd *cobra.Command, args []string) []string { - if !debugErrorTipEnabled(cmd) { - return args - } - - redacted := append([]string(nil), args...) - for i := 0; i < len(redacted); i++ { - switch { - case redacted[i] == "--override" && i+1 < len(redacted): - redacted[i+1] = "" - i++ - case strings.HasPrefix(redacted[i], "--override="): - redacted[i] = "--override=" - } - } - return redacted -} - -func printDebugErrorTip(cmd *cobra.Command) { - if !debugErrorTipEnabled(cmd) { - return - } - - debugFlag := cmd.Root().PersistentFlags().Lookup("debug") - if debugFlag.Value.String() == "true" { - return - } - - commandPrefix := cmd.Root().CommandPath() - command := commandPrefix + " --debug" + strings.TrimPrefix(cmd.CommandPath(), commandPrefix) - fmt.Fprintf( - cmd.ErrOrStderr(), - "\nTip: use the --debug flag to see more details and a trace of this error:\n %s …\n", - command, - ) -} - // This function is used to report an unknown subcommand. // It is used in the [cobra.Command.RunE] field of commands that have subcommands. // If user provided a valid subcommand, RunE for the diff --git a/cmd/root/root_test.go b/cmd/root/root_test.go index 9d59c3edea0..ce1584eacd4 100644 --- a/cmd/root/root_test.go +++ b/cmd/root/root_test.go @@ -177,67 +177,3 @@ func TestExecuteErrAlreadyPrintedNotEnriched(t *testing.T) { require.Error(t, err) assert.Empty(t, stderr.String()) } - -func TestExecuteDebugErrorTip(t *testing.T) { - tests := []struct { - name string - args []string - wantTip bool - wantCmd string - }{ - { - name: "default", - args: []string{"experimental", "air", "fail", "secret-value"}, - wantTip: true, - wantCmd: "databricks --debug experimental air fail …", - }, - { - name: "long debug flag", - args: []string{"--debug", "experimental", "air", "fail"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - stderr := &bytes.Buffer{} - cmd := New(t.Context()) - failCommand := &cobra.Command{ - Use: "fail [arg]", - RunE: func(cmd *cobra.Command, args []string) error { - return errors.New("failed") - }, - } - airCommand := &cobra.Command{Use: "air"} - EnableDebugErrorTip(airCommand) - airCommand.AddCommand(failCommand) - experimentalCommand := &cobra.Command{Use: "experimental"} - experimentalCommand.AddCommand(airCommand) - cmd.AddCommand(experimentalCommand) - cmd.SetArgs(tt.args) - cmd.SetErr(stderr) - - err := Execute(t.Context(), cmd) - require.Error(t, err) - if tt.wantTip { - assert.Contains(t, stderr.String(), "use the --debug flag") - assert.Contains(t, stderr.String(), tt.wantCmd) - assert.NotContains(t, stderr.String(), "secret-value") - } else { - assert.NotContains(t, stderr.String(), "use the --debug flag") - } - }) - } -} - -func TestCommandArgsForLoggingRedactsAirOverrides(t *testing.T) { - airCommand := &cobra.Command{Use: "air"} - EnableDebugErrorTip(airCommand) - runCommand := &cobra.Command{Use: "run"} - airCommand.AddCommand(runCommand) - - args := []string{"databricks", "experimental", "air", "run", "--override", "api_token=secret", "--override=other_secret=value"} - redacted := commandArgsForLogging(runCommand, args) - - assert.Equal(t, []string{"databricks", "experimental", "air", "run", "--override", "", "--override="}, redacted) - assert.Equal(t, "api_token=secret", args[5]) -} diff --git a/experimental/air/cmd/air.go b/experimental/air/cmd/air.go index ff7c3449793..be2274a1bcf 100644 --- a/experimental/air/cmd/air.go +++ b/experimental/air/cmd/air.go @@ -1,7 +1,9 @@ package aircmd import ( - "github.com/databricks/cli/cmd/root" + "fmt" + "strings" + "github.com/spf13/cobra" ) @@ -15,7 +17,6 @@ func New() *cobra.Command { This command set is the Go port of the standalone Python "air" CLI. It is experimental and may change in future versions.`, } - root.EnableDebugErrorTip(cmd) cmd.AddCommand(newRunCommand()) cmd.AddCommand(newGetCommand()) @@ -24,6 +25,37 @@ experimental and may change in future versions.`, cmd.AddCommand(newCancelCommand()) cmd.AddCommand(newRegisterImageCommand()) cmd.AddCommand(newConvertToDabsCommand()) + wrapRunErrorsWithDebugTip(cmd) return cmd } + +func wrapRunErrorsWithDebugTip(cmd *cobra.Command) { + if cmd.RunE != nil { + runE := cmd.RunE + cmd.RunE = func(cmd *cobra.Command, args []string) error { + return withDebugErrorTip(cmd, runE(cmd, args)) + } + } + for _, child := range cmd.Commands() { + wrapRunErrorsWithDebugTip(child) + } +} + +func withDebugErrorTip(cmd *cobra.Command, err error) error { + if err == nil { + return nil + } + debugFlag := cmd.Root().PersistentFlags().Lookup("debug") + if debugFlag != nil && debugFlag.Value.String() == "true" { + return err + } + + commandPrefix := cmd.Root().CommandPath() + command := commandPrefix + " --debug" + strings.TrimPrefix(cmd.CommandPath(), commandPrefix) + return fmt.Errorf( + "%w\n\nTip: use the --debug flag to see more details and a trace of this error:\n %s …", + err, + command, + ) +} diff --git a/experimental/air/cmd/air_test.go b/experimental/air/cmd/air_test.go index 1843acfe900..7f3197e0aa3 100644 --- a/experimental/air/cmd/air_test.go +++ b/experimental/air/cmd/air_test.go @@ -1,8 +1,10 @@ package aircmd import ( + "errors" "testing" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -20,3 +22,49 @@ func TestNewRegistersAllSubcommands(t *testing.T) { } assert.Len(t, registered, len(want), "unexpected number of subcommands") } + +func TestRunErrorIncludesDebugTip(t *testing.T) { + originalErr := errors.New("failed") + runCommand := &cobra.Command{ + Use: "run [arg]", + RunE: func(cmd *cobra.Command, args []string) error { + return originalErr + }, + } + airCommand := &cobra.Command{Use: "air"} + airCommand.AddCommand(runCommand) + wrapRunErrorsWithDebugTip(airCommand) + experimentalCommand := &cobra.Command{Use: "experimental"} + experimentalCommand.AddCommand(airCommand) + rootCommand := &cobra.Command{Use: "databricks"} + rootCommand.PersistentFlags().Bool("debug", false, "") + rootCommand.AddCommand(experimentalCommand) + + err := runCommand.RunE(runCommand, []string{"secret-value"}) + + assert.ErrorIs(t, err, originalErr) + assert.Contains(t, err.Error(), "use the --debug flag") + assert.Contains(t, err.Error(), "databricks --debug experimental air run …") + assert.NotContains(t, err.Error(), "secret-value") +} + +func TestRunErrorOmitsDebugTipWhenDebugEnabled(t *testing.T) { + originalErr := errors.New("failed") + runCommand := &cobra.Command{ + Use: "run", + RunE: func(cmd *cobra.Command, args []string) error { + return originalErr + }, + } + airCommand := &cobra.Command{Use: "air"} + airCommand.AddCommand(runCommand) + wrapRunErrorsWithDebugTip(airCommand) + rootCommand := &cobra.Command{Use: "databricks"} + rootCommand.PersistentFlags().Bool("debug", true, "") + rootCommand.AddCommand(airCommand) + + err := runCommand.RunE(runCommand, nil) + + assert.Same(t, originalErr, err) + assert.NotContains(t, err.Error(), "use the --debug flag") +} From b4e81edc7701a722694309a49c1b157e7b992cf9 Mon Sep 17 00:00:00 2001 From: "ben.hansen" Date: Fri, 4 Sep 2026 16:24:53 +0000 Subject: [PATCH 4/5] Update AIR acceptance outputs --- .../experimental/air/convert-to-dabs/output.txt | 6 ++++++ acceptance/experimental/air/get/output.txt | 3 +++ acceptance/experimental/air/logs-download/output.txt | 6 ++++++ acceptance/experimental/air/logs/output.txt | 9 +++++++++ .../register-image-no-secret-permission/output.txt | 3 +++ .../experimental/air/register-image/output.txt | 9 +++++++++ .../experimental/air/run-submit-deps/output.txt | 3 +++ acceptance/experimental/air/run/output.txt | 12 ++++++++++++ 8 files changed, 51 insertions(+) diff --git a/acceptance/experimental/air/convert-to-dabs/output.txt b/acceptance/experimental/air/convert-to-dabs/output.txt index f7b48740045..34fbfd37c68 100644 --- a/acceptance/experimental/air/convert-to-dabs/output.txt +++ b/acceptance/experimental/air/convert-to-dabs/output.txt @@ -72,6 +72,9 @@ Validation OK! >>> [CLI] experimental air convert-to-dabs train.yaml Error: databricks.yml already exists in .; pass --force to overwrite or remove it +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air convert-to-dabs … + Exit code: 1 === --force overwrites it @@ -98,4 +101,7 @@ job and its uploaded files with: >>> [CLI] experimental air convert-to-dabs docker.yaml --output-dir generated-docker Error: environment.docker_image is not yet supported by convert-to-dabs +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air convert-to-dabs … + Exit code: 1 diff --git a/acceptance/experimental/air/get/output.txt b/acceptance/experimental/air/get/output.txt index a448d7a057c..aa46e227c16 100644 --- a/acceptance/experimental/air/get/output.txt +++ b/acceptance/experimental/air/get/output.txt @@ -57,6 +57,9 @@ MLflow URL: [DATABRICKS_URL]/ml/experiments/exp1/runs/run1 >>> [CLI] experimental air get notanumber Error: invalid JOB_RUN_ID "notanumber": must be a positive integer +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air get … + Exit code: 1 === invalid run id (json) diff --git a/acceptance/experimental/air/logs-download/output.txt b/acceptance/experimental/air/logs-download/output.txt index d3458758c42..eace1373864 100644 --- a/acceptance/experimental/air/logs-download/output.txt +++ b/acceptance/experimental/air/logs-download/output.txt @@ -7,10 +7,16 @@ No logs available for run 123. Run terminated in state SUCCESS >>> [CLI] experimental air logs 123 --download-to dl-logs --node 5 Error: invalid --node 5: run has 2 node(s), indexed 0 to 1 +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air logs … + Exit code: 1 === download-to cannot be combined with --lines >>> [CLI] experimental air logs 123 --download-to dl-logs --lines 50 Error: --download-to writes complete logs, so it cannot be combined with --lines or --minutes +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air logs … + Exit code: 1 diff --git a/acceptance/experimental/air/logs/output.txt b/acceptance/experimental/air/logs/output.txt index 9ea8deedc5e..624286ca75b 100644 --- a/acceptance/experimental/air/logs/output.txt +++ b/acceptance/experimental/air/logs/output.txt @@ -36,6 +36,9 @@ CUDA out of memory >>> [CLI] experimental air logs 123 --lines 100 --minutes 30 Error: cannot combine --lines with --minutes: --lines tails by line count, --minutes by time window +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air logs … + Exit code: 1 === logs --lines and --minutes are mutually exclusive (json) @@ -57,10 +60,16 @@ Exit code: 1 >>> [CLI] experimental air logs notanumber Error: invalid JOB_RUN_ID "notanumber": must be a positive integer +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air logs … + Exit code: 1 === negative node >>> [CLI] experimental air logs 123 --node -1 Error: invalid --node -1: must not be negative +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air logs … + Exit code: 1 diff --git a/acceptance/experimental/air/register-image-no-secret-permission/output.txt b/acceptance/experimental/air/register-image-no-secret-permission/output.txt index 36a98a1fe76..98278254992 100644 --- a/acceptance/experimental/air/register-image-no-secret-permission/output.txt +++ b/acceptance/experimental/air/register-image-no-secret-permission/output.txt @@ -3,6 +3,9 @@ >>> [CLI] experimental air register-image nvcr.io/org/img:1.0 Error: image "nvcr.io/org/img:1.0" requires credentials, and the credentials found in your local Docker config could not be stored: creating secret scope "docker-credentials-[USERNAME]" was denied (user does not have permission to create secret scopes). Ask a workspace admin for permission to create secret scopes +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Host: [DATABRICKS_URL] Auth type: Personal Access Token (pat) diff --git a/acceptance/experimental/air/register-image/output.txt b/acceptance/experimental/air/register-image/output.txt index ebb73f8cba8..95f6dcf3de7 100644 --- a/acceptance/experimental/air/register-image/output.txt +++ b/acceptance/experimental/air/register-image/output.txt @@ -40,12 +40,18 @@ To use this image in your training config: >>> [CLI] experimental air register-image Error: IMAGE_URL cannot be empty +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Exit code: 1 === tag-policy auto is rejected >>> [CLI] experimental air register-image my-image:latest --tag-policy auto Error: --tag-policy auto is no longer supported: auto mode was removed and registration now always checks the source registry for the latest digest; omit the flag or use --tag-policy latest +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Exit code: 1 === invalid tag policy (json) @@ -67,6 +73,9 @@ Exit code: 1 >>> [CLI] experimental air register-image my-image:latest --timeout-minutes 0 Error: --timeout-minutes must be positive, got 0 +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Exit code: 1 === removed credential flags are unknown diff --git a/acceptance/experimental/air/run-submit-deps/output.txt b/acceptance/experimental/air/run-submit-deps/output.txt index 5425469f42f..761216b21be 100644 --- a/acceptance/experimental/air/run-submit-deps/output.txt +++ b/acceptance/experimental/air/run-submit-deps/output.txt @@ -63,3 +63,6 @@ Stream logs after submission using: === a requirements.yaml file path is rejected; deps must be inline >>> [CLI] experimental air run -f run-file.yaml Error: invalid config run-file.yaml: environment.dependencies must be a list of packages or reference a requirements.txt (see https://docs.databricks.com/aws/en/machine-learning/ai-runtime/cli/yaml-config#reference). A direct file reference is not supported + +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … diff --git a/acceptance/experimental/air/run/output.txt b/acceptance/experimental/air/run/output.txt index 18bf9526f31..de25a42433d 100644 --- a/acceptance/experimental/air/run/output.txt +++ b/acceptance/experimental/air/run/output.txt @@ -24,6 +24,9 @@ Dry run: configuration for "smoke-test" is valid; not submitting. >>> [CLI] experimental air run -f valid.yaml --dry-run --override bogus=1 Error: invalid --override "bogus": "bogus" is not a known field; available fields are: code_source, command, compute, env_variables, environment, experiment_name, idempotency_token, max_retries, mlflow_artifact_location, mlflow_experiment_directory, mlflow_run_name, parameters, permissions, secrets, timeout_minutes, usage_policy_id, usage_policy_name +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === override still runs schema validation @@ -31,6 +34,9 @@ Exit code: 1 Override: changing compute.num_accelerators from 1 to 0 Error: compute.num_accelerators must be positive, got 0 +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === watch is ignored with dry-run (nothing is submitted) @@ -45,12 +51,18 @@ Dry run: configuration for "smoke-test" is valid; not submitting. >>> [CLI] experimental air run -f git-remote.yaml --dry-run Error: git.remote is no longer supported: the snapshot archives your local copy, so a branch resolves to its local HEAD. To deploy a specific committed revision, use git.commit +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === invalid config is rejected >>> [CLI] experimental air run -f invalid.yaml --dry-run Error: invalid experiment_name "bad.name": only alphanumeric characters, hyphens (-), and underscores (_) are allowed +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === missing --file From 755025d213728df0bd4f901cd7df9a0f571c48c0 Mon Sep 17 00:00:00 2001 From: "ben.hansen" Date: Fri, 4 Sep 2026 17:55:51 +0000 Subject: [PATCH 5/5] Limit AIR debug tip to submission commands --- .../air/convert-to-dabs/output.txt | 6 ----- acceptance/experimental/air/get/output.txt | 3 --- .../experimental/air/logs-download/output.txt | 6 ----- acceptance/experimental/air/logs/output.txt | 9 -------- experimental/air/cmd/air.go | 22 +++++++++---------- experimental/air/cmd/air_test.go | 4 ++-- 6 files changed, 12 insertions(+), 38 deletions(-) diff --git a/acceptance/experimental/air/convert-to-dabs/output.txt b/acceptance/experimental/air/convert-to-dabs/output.txt index 34fbfd37c68..f7b48740045 100644 --- a/acceptance/experimental/air/convert-to-dabs/output.txt +++ b/acceptance/experimental/air/convert-to-dabs/output.txt @@ -72,9 +72,6 @@ Validation OK! >>> [CLI] experimental air convert-to-dabs train.yaml Error: databricks.yml already exists in .; pass --force to overwrite or remove it -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air convert-to-dabs … - Exit code: 1 === --force overwrites it @@ -101,7 +98,4 @@ job and its uploaded files with: >>> [CLI] experimental air convert-to-dabs docker.yaml --output-dir generated-docker Error: environment.docker_image is not yet supported by convert-to-dabs -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air convert-to-dabs … - Exit code: 1 diff --git a/acceptance/experimental/air/get/output.txt b/acceptance/experimental/air/get/output.txt index aa46e227c16..a448d7a057c 100644 --- a/acceptance/experimental/air/get/output.txt +++ b/acceptance/experimental/air/get/output.txt @@ -57,9 +57,6 @@ MLflow URL: [DATABRICKS_URL]/ml/experiments/exp1/runs/run1 >>> [CLI] experimental air get notanumber Error: invalid JOB_RUN_ID "notanumber": must be a positive integer -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air get … - Exit code: 1 === invalid run id (json) diff --git a/acceptance/experimental/air/logs-download/output.txt b/acceptance/experimental/air/logs-download/output.txt index eace1373864..d3458758c42 100644 --- a/acceptance/experimental/air/logs-download/output.txt +++ b/acceptance/experimental/air/logs-download/output.txt @@ -7,16 +7,10 @@ No logs available for run 123. Run terminated in state SUCCESS >>> [CLI] experimental air logs 123 --download-to dl-logs --node 5 Error: invalid --node 5: run has 2 node(s), indexed 0 to 1 -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air logs … - Exit code: 1 === download-to cannot be combined with --lines >>> [CLI] experimental air logs 123 --download-to dl-logs --lines 50 Error: --download-to writes complete logs, so it cannot be combined with --lines or --minutes -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air logs … - Exit code: 1 diff --git a/acceptance/experimental/air/logs/output.txt b/acceptance/experimental/air/logs/output.txt index 624286ca75b..9ea8deedc5e 100644 --- a/acceptance/experimental/air/logs/output.txt +++ b/acceptance/experimental/air/logs/output.txt @@ -36,9 +36,6 @@ CUDA out of memory >>> [CLI] experimental air logs 123 --lines 100 --minutes 30 Error: cannot combine --lines with --minutes: --lines tails by line count, --minutes by time window -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air logs … - Exit code: 1 === logs --lines and --minutes are mutually exclusive (json) @@ -60,16 +57,10 @@ Exit code: 1 >>> [CLI] experimental air logs notanumber Error: invalid JOB_RUN_ID "notanumber": must be a positive integer -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air logs … - Exit code: 1 === negative node >>> [CLI] experimental air logs 123 --node -1 Error: invalid --node -1: must not be negative -Tip: use the --debug flag to see more details and a trace of this error: - databricks --debug experimental air logs … - Exit code: 1 diff --git a/experimental/air/cmd/air.go b/experimental/air/cmd/air.go index be2274a1bcf..a90773b284f 100644 --- a/experimental/air/cmd/air.go +++ b/experimental/air/cmd/air.go @@ -18,27 +18,25 @@ This command set is the Go port of the standalone Python "air" CLI. It is experimental and may change in future versions.`, } - cmd.AddCommand(newRunCommand()) + runCommand := newRunCommand() + wrapRunErrorWithDebugTip(runCommand) + cmd.AddCommand(runCommand) cmd.AddCommand(newGetCommand()) cmd.AddCommand(newListCommand()) cmd.AddCommand(newLogsCommand()) cmd.AddCommand(newCancelCommand()) - cmd.AddCommand(newRegisterImageCommand()) + registerImageCommand := newRegisterImageCommand() + wrapRunErrorWithDebugTip(registerImageCommand) + cmd.AddCommand(registerImageCommand) cmd.AddCommand(newConvertToDabsCommand()) - wrapRunErrorsWithDebugTip(cmd) return cmd } -func wrapRunErrorsWithDebugTip(cmd *cobra.Command) { - if cmd.RunE != nil { - runE := cmd.RunE - cmd.RunE = func(cmd *cobra.Command, args []string) error { - return withDebugErrorTip(cmd, runE(cmd, args)) - } - } - for _, child := range cmd.Commands() { - wrapRunErrorsWithDebugTip(child) +func wrapRunErrorWithDebugTip(cmd *cobra.Command) { + runE := cmd.RunE + cmd.RunE = func(cmd *cobra.Command, args []string) error { + return withDebugErrorTip(cmd, runE(cmd, args)) } } diff --git a/experimental/air/cmd/air_test.go b/experimental/air/cmd/air_test.go index 7f3197e0aa3..8717c293886 100644 --- a/experimental/air/cmd/air_test.go +++ b/experimental/air/cmd/air_test.go @@ -33,7 +33,7 @@ func TestRunErrorIncludesDebugTip(t *testing.T) { } airCommand := &cobra.Command{Use: "air"} airCommand.AddCommand(runCommand) - wrapRunErrorsWithDebugTip(airCommand) + wrapRunErrorWithDebugTip(runCommand) experimentalCommand := &cobra.Command{Use: "experimental"} experimentalCommand.AddCommand(airCommand) rootCommand := &cobra.Command{Use: "databricks"} @@ -58,7 +58,7 @@ func TestRunErrorOmitsDebugTipWhenDebugEnabled(t *testing.T) { } airCommand := &cobra.Command{Use: "air"} airCommand.AddCommand(runCommand) - wrapRunErrorsWithDebugTip(airCommand) + wrapRunErrorWithDebugTip(runCommand) rootCommand := &cobra.Command{Use: "databricks"} rootCommand.PersistentFlags().Bool("debug", true, "") rootCommand.AddCommand(airCommand)