Skip to content

Generalize CLI token source into progressive command list#1605

Open
mihaimitrea-db wants to merge 2 commits intomainfrom
mihaimitrea-db/stack/generalize-cli-commands
Open

Generalize CLI token source into progressive command list#1605
mihaimitrea-db wants to merge 2 commits intomainfrom
mihaimitrea-db/stack/generalize-cli-commands

Conversation

@mihaimitrea-db
Copy link
Copy Markdown
Contributor

@mihaimitrea-db mihaimitrea-db commented Mar 30, 2026

🥞 Stacked PR

Use this link to review incremental changes.


Summary

Generalize CliTokenSource from three explicit command fields into a []cliCommand list with an activeCommandIndex, so that adding future CLI flags (e.g. --scopes) requires adding one cliCommand literal instead of a new field, a new if block, and a new error check.

Why

The parent PR (#1604) introduced --force-refresh support by adding a third command field and hand-writing each fallback block in Token(). This works, but every new flag would require adding another field, another if block, another error check, and another test — the pattern doesn't scale.

Rather than growing the struct and Token() linearly with each flag, this PR extracts the repeating pattern into a loop over a command list.

Why try-and-retry over version detection or --help parsing

  • Version detection (databricks version + static version table) was rejected because it creates a maintenance burden across every SDK (Go, Python, Java) and a second source of truth that can fall out of sync.
  • --help flag parsing was rejected because the output format is not a stable API.
  • Feature probing with try-and-retry (the approach taken here) uses the CLI itself as the authority on what it supports. Commands are built at init time from most-featured to simplest. On the first Token() call, each command is tried in order; when the CLI responds with "unknown flag:", the next simpler command is tried. The working command index is cached atomically so subsequent calls skip probing.

What changed

Interface changes

None. CliTokenSource is not part of the public API surface.

Behavioral changes

None. The set of commands tried is identical to the parent PR. The only observable difference is that activeCommandIndex caches the working command, so subsequent Token() calls skip fallback attempts — a pure performance improvement.

Internal changes

  • cliCommand struct: replaces the three separate []string fields. Each entry holds args (the full CLI command), usedFlags (the flags in this command, used for error matching), and warningMessage (logged when falling back).
  • CliTokenSource: holds commands []cliCommand and activeCommandIndex atomic.Int32 (initialized to -1). The atomic type makes concurrent access safe when blocking and async token refreshes overlap. Once resolved, the index never changes — older CLIs don't gain new flags.
  • buildCliCommands: constructs all command variants inline. When cfg.Profile is set, --force-refresh is based on the --profile command; when only cfg.Host is set, --force-refresh is based on the --host command. Adding a future flag means adding one more cliCommand literal here.
  • NewCliTokenSource: guards against empty command lists (neither profile nor host configured) with an early error.
  • Token(): fast path when activeCommandIndex >= 0 (resolved); otherwise delegates to probeAndExec.
  • probeAndExec: walks the command list. Unknown-flag errors for the command's usedFlags trigger fallback with a warning; any other error stops probing immediately and is returned to the caller. On success, stores the index atomically.
  • isUnknownFlagError: simplified to plain strings.Contains matching on err.Error(). The previous cliError wrapper type was removed — stderr is formatted with %q to keep multi-line CLI output on one line in logs. exec.ExitError is intentionally not preserved in the error chain since only the stderr text matters for error classification; exit codes and process state are not useful to callers.

How is this tested?

Unit tests in config/cli_token_source_test.go:

  • TestBuildCliCommands — verifies the full []cliCommand list (args) for each config combination: host-only (2 commands), account host (2 commands), profile+host (3 commands), profile-only (2 commands).
  • TestNewCliTokenSource/no_profile_or_host — verifies the empty-commands guard returns an error.
  • TestCliTokenSource_Token_Fallback — table-driven test covering: force-refresh success, force→profile fallback, force→host fallback (no profile), profile→host fallback, full chain, real error stops fallback, all commands fail with unknown flag.
  • TestCliTokenSource_Token_ActiveCommandIndexPersists — first call falls back and caches activeCommandIndex = 1; second call starts at index 1 and succeeds without retrying the failed command.

@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (4cb4c26 -> 82d9599)
NEXT_CHANGELOG.md
@@ -6,5 +6,5 @@
  
 + * Generalize CLI token source into a progressive command list for forward-compatible flag support.
   * Normalize internal token sources on `auth.TokenSource` for proper context propagation ([#1577](https://github.com/databricks/databricks-sdk-go/pull/1577)).
-  * Bump golang.org/x/crypto from 0.21.0 to 0.45.0 in /examples/slog ([#1566](https://github.com/databricks/databricks-sdk-go/pull/1566)).
-  * Bump golang.org/x/net from 0.23.0 to 0.33.0 in /examples/slog ([#1127](https://github.com/databricks/databricks-sdk-go/pull/1127)).
\ No newline at end of file
+  * Fix `TestAzureGithubOIDCCredentials` hang caused by missing `HTTPTransport` stub: `EnsureResolved` now calls `resolveHostMetadata`, which makes a real network request when no transport is set ([#1550](https://github.com/databricks/databricks-sdk-go/pull/1550)).
+  * Bump golang.org/x/crypto from 0.21.0 to 0.45.0 in /examples/slog ([#1566](https://github.com/databricks/databricks-sdk-go/pull/1566)).
\ No newline at end of file
config/cli_token_source.go
@@ -18,13 +18,13 @@
 +	flag           string
 +	warningMessage string
 +}
-+
+ 
+-	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
+-	profileCmd []string
 +var cliFeatureFlags = []cliFeatureFlag{
 +	{"--force-refresh", "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version."},
 +}
- 
--	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
--	profileCmd []string
++
 +const profileFlagWarning = "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version."
  
 -	// hostCmd uses --host as a fallback for CLIs that predate --profile support.
@@ -83,7 +83,7 @@
 +				warning = cliFeatureFlags[i].warningMessage
 +			}
 +			commands = append(commands, cliCommand{args: args, warningMessage: warning})
-+			}
++		}
  	}
 -	if cfg.Host != "" {
 -		hostCmd = buildHostCommand(cliPath, cfg)

Reproduce locally: git range-diff f626fed..4cb4c26 67f79d8..82d9599 | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 82d9599 to 68d45f4 Compare March 30, 2026 13:56
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (82d9599 -> 68d45f4)
config/cli_token_source.go
@@ -18,13 +18,13 @@
 +	flag           string
 +	warningMessage string
 +}
- 
--	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
--	profileCmd []string
++
 +var cliFeatureFlags = []cliFeatureFlag{
 +	{"--force-refresh", "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version."},
 +}
-+
+ 
+-	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
+-	profileCmd []string
 +const profileFlagWarning = "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version."
  
 -	// hostCmd uses --host as a fallback for CLIs that predate --profile support.
@@ -143,10 +143,8 @@
 -		logger.Warnf(ctx, "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.")
 -	}
 -
--	if c.hostCmd != nil {
--		return c.execCliCommand(ctx, c.hostCmd)
--	}
--
- 	return nil, fmt.Errorf("no CLI command configured")
+-	return c.execCliCommand(ctx, c.hostCmd)
++	return nil, fmt.Errorf("cannot get access token: no CLI commands configured")
  }
- 
\ No newline at end of file
+ 
+ func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oauth2.Token, error) {
\ No newline at end of file

Reproduce locally: git range-diff 67f79d8..82d9599 db4df21..68d45f4 | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 68d45f4 to 4a5079f Compare March 30, 2026 15:14
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (68d45f4 -> 4a5079f)
config/cli_token_source.go
@@ -116,6 +116,17 @@
  func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
 -	if c.forceCmd != nil {
 -		tok, err := c.execCliCommand(ctx, c.forceCmd)
+-		if err == nil {
+-			return tok, nil
+-		}
+-		if !isUnknownFlagError(err, "--force-refresh") && !isUnknownFlagError(err, "--profile") {
+-			return nil, err
+-		}
+-		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
+-	}
+-
+-	if c.profileCmd != nil {
+-		tok, err := c.execCliCommand(ctx, c.profileCmd)
 +	for i := c.activeCommandIndex; i < len(c.commands); i++ {
 +		cmd := c.commands[i]
 +		tok, err := c.execCliCommand(ctx, cmd.args)
@@ -123,26 +134,18 @@
 +			c.activeCommandIndex = i
  			return tok, nil
  		}
--		if !isUnknownFlagError(err, "") {
+-		if !isUnknownFlagError(err, "--profile") {
 +		lastCommand := i == len(c.commands)-1
 +		if lastCommand || !isUnknownFlagError(err, "") {
  			return nil, err
  		}
--		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
-+		logger.Warnf(ctx, cmd.warningMessage)
- 	}
--
--	if c.profileCmd != nil {
--		tok, err := c.execCliCommand(ctx, c.profileCmd)
--		if err == nil {
--			return tok, nil
--		}
--		if !isUnknownFlagError(err, "--profile") {
--			return nil, err
--		}
 -		logger.Warnf(ctx, "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.")
 -	}
 -
+-	if c.hostCmd == nil {
+-		return nil, fmt.Errorf("cannot get access token: no CLI commands available")
++		logger.Warnf(ctx, cmd.warningMessage)
+ 	}
 -	return c.execCliCommand(ctx, c.hostCmd)
 +	return nil, fmt.Errorf("cannot get access token: no CLI commands configured")
  }
config/cli_token_source_test.go
@@ -76,13 +76,13 @@
 -			gotForceCmd, gotProfileCmd, gotHostCmd := buildCliCommands(cliPath, tc.cfg)
 -			if !slices.Equal(gotForceCmd, tc.wantForceCmd) {
 -				t.Errorf("force cmd = %v, want %v", gotForceCmd, tc.wantForceCmd)
--			}
--			if !slices.Equal(gotProfileCmd, tc.wantProfileCmd) {
--				t.Errorf("profile cmd = %v, want %v", gotProfileCmd, tc.wantProfileCmd)
 +			got := buildCliCommands(cliPath, tc.cfg)
 +			if len(got) != len(tc.wantCommands) {
 +				t.Fatalf("got %d commands, want %d", len(got), len(tc.wantCommands))
  			}
+-			if !slices.Equal(gotProfileCmd, tc.wantProfileCmd) {
+-				t.Errorf("profile cmd = %v, want %v", gotProfileCmd, tc.wantProfileCmd)
+-			}
 -			if !slices.Equal(gotHostCmd, tc.wantHostCmd) {
 -				t.Errorf("host cmd = %v, want %v", gotHostCmd, tc.wantHostCmd)
 +			for i, want := range tc.wantCommands {
@@ -208,15 +208,15 @@
  	_, err := ts.Token(context.Background())
  	if err == nil {
  		t.Fatal("Token() error = nil, want error")
- 		t.Errorf("Token() error = %v, want error containing original auth failure", err)
  	}
  }
-+
+ 
+-func TestCliTokenSource_Token_NilHostCmdReturnsError(t *testing.T) {
 +func TestCliTokenSource_Token_ActiveCommandIndexPersists(t *testing.T) {
-+	if runtime.GOOS == "windows" {
-+		t.Skip("Skipping shell script test on Windows")
-+	}
-+
+ 	if runtime.GOOS == "windows" {
+ 		t.Skip("Skipping shell script test on Windows")
+ 	}
+ 
 +	expiry := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
 +	validResponse, _ := json.Marshal(cliTokenResponse{
 +		AccessToken: "host-token",
@@ -224,18 +224,25 @@
 +		Expiry:      expiry,
 +	})
 +
-+	tempDir := t.TempDir()
-+
-+	forceScript := filepath.Join(tempDir, "force_cli.sh")
+ 	tempDir := t.TempDir()
+ 
+ 	forceScript := filepath.Join(tempDir, "force_cli.sh")
+-	if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
 +	if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --force-refresh' >&2\nexit 1"), 0755); err != nil {
-+		t.Fatalf("failed to create force script: %v", err)
-+	}
-+
+ 		t.Fatalf("failed to create force script: %v", err)
+ 	}
+ 
+-	profileScript := filepath.Join(tempDir, "profile_cli.sh")
+-	if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
+-		t.Fatalf("failed to create profile script: %v", err)
 +	hostScript := filepath.Join(tempDir, "host_cli.sh")
 +	if err := os.WriteFile(hostScript, []byte("#!/bin/sh\necho '"+string(validResponse)+"'"), 0755); err != nil {
 +		t.Fatalf("failed to create host script: %v", err)
-+	}
-+
+ 	}
+ 
+-	ts := &CliTokenSource{
+-		forceCmd:   []string{forceScript},
+-		profileCmd: []string{profileScript},
 +	ts := &CliTokenSource{commands: []cliCommand{
 +		{args: []string{forceScript}, warningMessage: "force-refresh not supported"},
 +		{args: []string{hostScript}},
@@ -245,13 +252,18 @@
 +	token, err := ts.Token(context.Background())
 +	if err != nil {
 +		t.Fatalf("first Token() error = %v", err)
-+	}
+ 	}
+-	_, err := ts.Token(context.Background())
+-	if err == nil {
+-		t.Fatal("Token() error = nil, want error")
 +	if token.AccessToken != "host-token" {
 +		t.Errorf("first AccessToken = %q, want %q", token.AccessToken, "host-token")
 +	}
 +	if ts.activeCommandIndex != 1 {
 +		t.Errorf("activeCommandIndex = %d, want 1", ts.activeCommandIndex)
-+	}
+ 	}
+-	if !strings.Contains(err.Error(), "no CLI commands available") {
+-		t.Errorf("Token() error = %v, want error containing %q", err, "no CLI commands available")
 +
 +	// Second call: starts at activeCommandIndex, skipping the force command.
 +	token, err = ts.Token(context.Background())
@@ -260,5 +272,5 @@
 +	}
 +	if token.AccessToken != "host-token" {
 +		t.Errorf("second AccessToken = %q, want %q", token.AccessToken, "host-token")
-+	}
-+}
\ No newline at end of file
+ 	}
+ }
\ No newline at end of file

Reproduce locally: git range-diff db4df21..68d45f4 97a1007..4a5079f | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 4a5079f to 6f4fead Compare March 30, 2026 16:27
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (4a5079f -> 6f4fead)
config/cli_token_source.go
@@ -1,6 +1,13 @@
 diff --git a/config/cli_token_source.go b/config/cli_token_source.go
 --- a/config/cli_token_source.go
 +++ b/config/cli_token_source.go
+ 	"path/filepath"
+ 	"runtime"
+ 	"strings"
++	"sync/atomic"
+ 	"time"
+ 
+ 	"github.com/databricks/databricks-sdk-go/logger"
  	Expiry      string `json:"expiry"`
  }
  
@@ -42,7 +49,7 @@
 +// falling back progressively for older CLI versions that lack newer flags.
 +type CliTokenSource struct {
 +	commands           []cliCommand
-+	activeCommandIndex int
++	activeCommandIndex atomic.Int32
  }
  
  func NewCliTokenSource(cfg *Config) (*CliTokenSource, error) {
@@ -127,11 +134,11 @@
 -
 -	if c.profileCmd != nil {
 -		tok, err := c.execCliCommand(ctx, c.profileCmd)
-+	for i := c.activeCommandIndex; i < len(c.commands); i++ {
++	for i := int(c.activeCommandIndex.Load()); i < len(c.commands); i++ {
 +		cmd := c.commands[i]
 +		tok, err := c.execCliCommand(ctx, cmd.args)
  		if err == nil {
-+			c.activeCommandIndex = i
++			c.activeCommandIndex.Store(int32(i))
  			return tok, nil
  		}
 -		if !isUnknownFlagError(err, "--profile") {
config/cli_token_source_test.go
@@ -259,8 +259,8 @@
 +	if token.AccessToken != "host-token" {
 +		t.Errorf("first AccessToken = %q, want %q", token.AccessToken, "host-token")
 +	}
-+	if ts.activeCommandIndex != 1 {
-+		t.Errorf("activeCommandIndex = %d, want 1", ts.activeCommandIndex)
++	if ts.activeCommandIndex.Load() != 1 {
++		t.Errorf("activeCommandIndex = %d, want 1", ts.activeCommandIndex.Load())
  	}
 -	if !strings.Contains(err.Error(), "no CLI commands available") {
 -		t.Errorf("Token() error = %v, want error containing %q", err, "no CLI commands available")

Reproduce locally: git range-diff 97a1007..4a5079f 69a7c95..6f4fead | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 6f4fead to 2218270 Compare March 31, 2026 08:07
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (6f4fead -> 2218270)
config/cli_token_source.go
@@ -11,43 +11,29 @@
  	Expiry      string `json:"expiry"`
  }
  
--// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
++// cliCommand is a single CLI invocation with an associated warning message
++// that is logged when this command fails and we fall back to the next one.
++type cliCommand struct {
++	args           []string
++	warningMessage string
++}
++
+ // CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
 -// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, progressively
 -// falling back to simpler invocations for older CLI versions.
--type CliTokenSource struct {
++// It holds a list of commands ordered from most feature-rich to simplest,
++// falling back progressively for older CLI versions that lack newer flags.
+ type CliTokenSource struct {
 -	// forceCmd uses --profile with --force-refresh to bypass the CLI's token cache.
 -	// Nil when cfg.Profile is empty (--force-refresh requires --profile support).
 -	forceCmd []string
-+// cliFeatureFlag defines a CLI feature flag and the warning to log when
-+// falling back because the CLI does not support it. Ordered newest-first:
-+// commands are built by progressively stripping these flags for older CLIs.
-+type cliFeatureFlag struct {
-+	flag           string
-+	warningMessage string
-+}
-+
-+var cliFeatureFlags = []cliFeatureFlag{
-+	{"--force-refresh", "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version."},
-+}
- 
+-
 -	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
 -	profileCmd []string
-+const profileFlagWarning = "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version."
- 
+-
 -	// hostCmd uses --host as a fallback for CLIs that predate --profile support.
 -	// Nil when cfg.Host is empty.
 -	hostCmd []string
-+// cliCommand is a single CLI invocation with an associated warning message
-+// that is logged when this command fails and we fall back to the next one.
-+type cliCommand struct {
-+	args           []string
-+	warningMessage string
-+}
-+
-+// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
-+// It holds a list of commands ordered from most feature-rich to simplest,
-+// falling back progressively for older CLI versions that lack newer flags.
-+type CliTokenSource struct {
 +	commands           []cliCommand
 +	activeCommandIndex atomic.Int32
  }
@@ -69,48 +55,47 @@
 -func buildCliCommands(cliPath string, cfg *Config) ([]string, []string, []string) {
 -	var forceCmd, profileCmd, hostCmd []string
 +// buildCliCommands constructs the list of CLI commands for fetching an auth
-+// token, ordered from most feature-rich to simplest. When cfg.Profile is set,
-+// commands include feature flags from [cliFeatureFlags] (stripped progressively)
-+// plus a --host fallback when host is available. When cfg.Profile is empty,
-+// only --host is returned — the CLI must support --profile before any feature
-+// flags can be used (monotonic feature assumption).
++// token, ordered from most feature-rich to simplest. The order defines the
++// fallback chain: when a command fails with an unknown flag error, the next
++// one is tried.
 +func buildCliCommands(cliPath string, cfg *Config) []cliCommand {
 +	var commands []cliCommand
  	if cfg.Profile != "" {
 -		profileCmd = []string{cliPath, "auth", "token", "--profile", cfg.Profile}
 -		forceCmd = append(profileCmd, "--force-refresh")
-+		baseArgs := []string{cliPath, "auth", "token", "--profile", cfg.Profile}
-+		for i := 0; i <= len(cliFeatureFlags); i++ {
-+			args := append([]string{}, baseArgs...)
-+			for _, f := range cliFeatureFlags[i:] {
-+				args = append(args, f.flag)
-+			}
-+			warning := profileFlagWarning
-+			if i < len(cliFeatureFlags) {
-+				warning = cliFeatureFlags[i].warningMessage
-+			}
-+			commands = append(commands, cliCommand{args: args, warningMessage: warning})
-+		}
- 	}
+-	}
 -	if cfg.Host != "" {
 -		hostCmd = buildHostCommand(cliPath, cfg)
-+	hostArgs := buildHostCommand(cliPath, cfg)
-+	if hostArgs != nil {
-+		commands = append(commands, cliCommand{args: hostArgs})
- 	}
+-	}
 -	return forceCmd, profileCmd, hostCmd
++		commands = append(commands, cliCommand{
++			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile, "--force-refresh"},
++			warningMessage: "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.",
++		})
++		commands = append(commands, cliCommand{
++			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile},
++			warningMessage: "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.",
++		})
++	}
++	commands = appendHostCommand(commands, cliPath, cfg)
 +	return commands
  }
  
- // buildHostCommand constructs the legacy --host based CLI command.
-+// Returns nil when cfg.Host is empty.
- func buildHostCommand(cliPath string, cfg *Config) []string {
+-// buildHostCommand constructs the legacy --host based CLI command.
+-func buildHostCommand(cliPath string, cfg *Config) []string {
+-	cmd := []string{cliPath, "auth", "token", "--host", cfg.Host}
++func appendHostCommand(commands []cliCommand, cliPath string, cfg *Config) []cliCommand {
 +	if cfg.Host == "" {
-+		return nil
++		return commands
 +	}
- 	cmd := []string{cliPath, "auth", "token", "--host", cfg.Host}
++	args := []string{cliPath, "auth", "token", "--host", cfg.Host}
  	switch cfg.HostType() {
  	case AccountHost:
+-		cmd = append(cmd, "--account-id", cfg.AccountID)
++		args = append(args, "--account-id", cfg.AccountID)
+ 	}
+-	return cmd
++	return append(commands, cliCommand{args: args})
  }
  
  // Token fetches an OAuth token by shelling out to the Databricks CLI.

Reproduce locally: git range-diff 69a7c95..6f4fead 69a7c95..2218270 | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 2218270 to 76e74ca Compare March 31, 2026 09:06
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (2218270 -> 76e74ca)
config/cli_token_source.go
@@ -15,6 +15,7 @@
 +// that is logged when this command fails and we fall back to the next one.
 +type cliCommand struct {
 +	args           []string
++	flags          []string
 +	warningMessage string
 +}
 +
@@ -70,10 +71,12 @@
 -	return forceCmd, profileCmd, hostCmd
 +		commands = append(commands, cliCommand{
 +			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile, "--force-refresh"},
++			flags:          []string{"--force-refresh", "--profile"},
 +			warningMessage: "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.",
 +		})
 +		commands = append(commands, cliCommand{
 +			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile},
++			flags:          []string{"--profile"},
 +			warningMessage: "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.",
 +		})
 +	}
@@ -108,17 +111,6 @@
  func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
 -	if c.forceCmd != nil {
 -		tok, err := c.execCliCommand(ctx, c.forceCmd)
--		if err == nil {
--			return tok, nil
--		}
--		if !isUnknownFlagError(err, "--force-refresh") && !isUnknownFlagError(err, "--profile") {
--			return nil, err
--		}
--		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
--	}
--
--	if c.profileCmd != nil {
--		tok, err := c.execCliCommand(ctx, c.profileCmd)
 +	for i := int(c.activeCommandIndex.Load()); i < len(c.commands); i++ {
 +		cmd := c.commands[i]
 +		tok, err := c.execCliCommand(ctx, cmd.args)
@@ -126,20 +118,52 @@
 +			c.activeCommandIndex.Store(int32(i))
  			return tok, nil
  		}
--		if !isUnknownFlagError(err, "--profile") {
+-		if !isUnknownFlagError(err, "--force-refresh") && !isUnknownFlagError(err, "--profile") {
 +		lastCommand := i == len(c.commands)-1
-+		if lastCommand || !isUnknownFlagError(err, "") {
++		if lastCommand || !isUnknownFlagError(err, cmd.flags) {
  			return nil, err
  		}
+-		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
++		logger.Warnf(ctx, cmd.warningMessage)
+ 	}
+-
+-	if c.profileCmd != nil {
+-		tok, err := c.execCliCommand(ctx, c.profileCmd)
+-		if err == nil {
+-			return tok, nil
+-		}
+-		if !isUnknownFlagError(err, "--profile") {
+-			return nil, err
+-		}
 -		logger.Warnf(ctx, "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.")
 -	}
 -
 -	if c.hostCmd == nil {
 -		return nil, fmt.Errorf("cannot get access token: no CLI commands available")
-+		logger.Warnf(ctx, cmd.warningMessage)
- 	}
+-	}
 -	return c.execCliCommand(ctx, c.hostCmd)
 +	return nil, fmt.Errorf("cannot get access token: no CLI commands configured")
  }
  
- func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oauth2.Token, error) {
\ No newline at end of file
+ func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oauth2.Token, error) {
+ }
+ 
+ // isUnknownFlagError returns true if the error indicates the CLI does not
+-// recognize a flag. Pass a specific flag (e.g. "--profile") to check for that
+-// flag, or pass "" to match any "unknown flag:" error.
+-func isUnknownFlagError(err error, flag string) bool {
+-	if flag == "" {
+-		return strings.Contains(err.Error(), "unknown flag:")
++// recognize one of the given flags.
++func isUnknownFlagError(err error, flags []string) bool {
++	msg := err.Error()
++	for _, flag := range flags {
++		if strings.Contains(msg, "unknown flag: "+flag) {
++			return true
++		}
+ 	}
+-	return strings.Contains(err.Error(), "unknown flag: "+flag)
++	return false
+ }
+ 
+ // parseExpiry parses an expiry time string in multiple formats for cross-SDK compatibility.
\ No newline at end of file
config/cli_token_source_test.go
@@ -121,7 +121,7 @@
 -		profileCmd: []string{profileScript},
 -	}
 +	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, warningMessage: "force-refresh not supported"},
++		{args: []string{forceScript}, flags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
 +		{args: []string{profileScript}},
 +	}}
  	token, err := ts.Token(context.Background())
@@ -135,7 +135,7 @@
 -		hostCmd:    []string{hostScript},
 -	}
 +	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{profileScript}, warningMessage: "profile not supported"},
++		{args: []string{profileScript}, flags: []string{"--profile"}, warningMessage: "profile not supported"},
 +		{args: []string{hostScript}},
 +	}}
  	token, err := ts.Token(context.Background())
@@ -171,8 +171,8 @@
 -		hostCmd:    []string{hostScript},
 -	}
 +	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}, warningMessage: "profile not supported"},
++		{args: []string{forceScript}, flags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
++		{args: []string{profileScript}, flags: []string{"--profile"}, warningMessage: "profile not supported"},
 +		{args: []string{hostScript}},
 +	}}
  	token, err := ts.Token(context.Background())
@@ -201,8 +201,8 @@
 -		hostCmd:    []string{hostScript},
 -	}
 +	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}, warningMessage: "profile not supported"},
++		{args: []string{forceScript}, flags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
++		{args: []string{profileScript}, flags: []string{"--profile"}, warningMessage: "profile not supported"},
 +		{args: []string{hostScript}},
 +	}}
  	_, err := ts.Token(context.Background())
@@ -244,7 +244,7 @@
 -		forceCmd:   []string{forceScript},
 -		profileCmd: []string{profileScript},
 +	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, warningMessage: "force-refresh not supported"},
++		{args: []string{forceScript}, flags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
 +		{args: []string{hostScript}},
 +	}}
 +

Reproduce locally: git range-diff 69a7c95..2218270 69a7c95..76e74ca | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db marked this pull request as ready for review March 31, 2026 09:53
Comment on lines +67 to +68
args: []string{cliPath, "auth", "token", "--profile", cfg.Profile, "--force-refresh"},
flags: []string{"--force-refresh", "--profile"},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have duplicate args and flags, is that intended?

Copy link
Copy Markdown
Contributor

@renaudhartert-db renaudhartert-db Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: it required me to reach line 141 to realize that this was not a bug. We should make this clearer with a better name (e.g. usedFlags) and/or a comment in cliCommand. Something like:

type cliCommand struct {
	args []string

    // List of flags used by the command. This is used for 
    // distinguishing errors caused by unknown flags from
    // other errors.
	usedFlags      []string
	warningMessage string
}

// buildHostCommand constructs the legacy --host based CLI command.
func buildHostCommand(cliPath string, cfg *Config) []string {
cmd := []string{cliPath, "auth", "token", "--host", cfg.Host}
func appendHostCommand(commands []cliCommand, cliPath string, cfg *Config) []cliCommand {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be inlined in buildCliCommands? It's unclear to me why some commands have their own function while others don't.

if err != nil && c.hostCmd != nil && isUnknownFlagError(err) {
logger.Warnf(ctx, "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.")
return c.execCliCommand(ctx, c.hostCmd)
for i := int(c.activeCommandIndex.Load()); i < len(c.commands); i++ {
Copy link
Copy Markdown
Contributor

@renaudhartert-db renaudhartert-db Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we're really trying to model a concurrent "binary" behavior: either we know the index and use it, or we don't and need to figure out which one to use.

In particular, I'm not sure I understand why we would want the index to change after having identified the right command. I understand that it will not in practice but the code tells a different story.

I believe this could be better modeled using sync.Once or sync.OnceValue instead of atomic.Int though we would have to be cautious about errors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I''m not sure how to handle transient errors with sync.Once...

Right now activeCommandIndex is set on command success. But if right now the CLI is encountering network issues for example everything might fail so activeCommandIndex is never set (nothing succeeds).

For sync.Once you could get permanently stuck with the idea that everything is failing right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the idea that the index will not change once it's resolved we could do something like this:

type CliTokenSource struct {
	commands           []cliCommand
	activeCommandIndex atomic.Int32 // -1 = unresolved
}

// Other code

func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
	if idx := int(c.activeCommandIndex.Load()); idx >= 0 {
		return c.execCliCommand(ctx, c.commands[idx].args)
	}
	return c.probeAndExec(ctx)
}

func (c *CliTokenSource) probeAndExec(ctx context.Context) (*oauth2.Token, error) {
	for i := 0; i < len(c.commands); i++ {
		tok, err := c.execCliCommand(ctx, c.commands[i].args)
		if err == nil {
			c.activeCommandIndex.Store(int32(i))
			return tok, nil
		}
		lastCommand := i == len(c.commands)-1
		if lastCommand || !isUnknownFlagError(err, c.commands[i].usedFlags) {
			return nil, err
		}
		logger.Warnf(ctx, c.commands[i].warningMessage)
	}
	return nil, fmt.Errorf("cannot get access token: no CLI commands configured")
}

}
profileCmd, hostCmd := buildCliCommands(cliPath, cfg)
return &CliTokenSource{cmd: profileCmd, hostCmd: hostCmd}, nil
return &CliTokenSource{commands: buildCliCommands(cliPath, cfg)}, nil
Copy link
Copy Markdown
Contributor

@renaudhartert-db renaudhartert-db Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both cfg.Profile and cfg.Host are empty, buildCliCommands returns an empty slice and Token() falls through to the sentinel error. There is no test covering this path. I think either (i) NewCliTokenSource should guard against this and return an error early, or (ii) there should be a test asserting the current behavior.

Note that it is fine to return "cannot get access token: no CLI commands configured" but the condition should be treated upstream. Having it at the end of the for loop makes the code much harder to follow and reason about.

var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return nil, fmt.Errorf("cannot get access token: %s", strings.TrimSpace(string(exitErr.Stderr)))
return nil, fmt.Errorf("cannot get access token: %q", strings.TrimSpace(string(exitErr.Stderr)))
Copy link
Copy Markdown
Contributor

@renaudhartert-db renaudhartert-db Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[no action required] Out of curiosity, what's the reason to change %s to %q? I'm also wondering if we should try to keep wrapping ExitError type. It's a little weird that we wrap everything else. If this is intentional, let's make it clear with a comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind the error was coming from an external source (the CLI) so it should be quoted. I also find that easier to understand in case of empty errors. But not that relevant in this case.

Wrapping the error wouldn't hurt. The isUnknownFlagError function could also ensure that the error which is checking is actually an exit error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what I meant is that we are losing the exec.ExitError type. The new type that we have introduced does not solve that.

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 76e74ca to 79ac794 Compare March 31, 2026 14:15
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (76e74ca -> 79ac794)
config/cli_token_source.go
@@ -11,31 +11,47 @@
  	Expiry      string `json:"expiry"`
  }
  
-+// cliCommand is a single CLI invocation with an associated warning message
-+// that is logged when this command fails and we fall back to the next one.
-+type cliCommand struct {
-+	args           []string
-+	flags          []string
-+	warningMessage string
-+}
-+
- // CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
+-// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
 -// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, progressively
 -// falling back to simpler invocations for older CLI versions.
-+// It holds a list of commands ordered from most feature-rich to simplest,
-+// falling back progressively for older CLI versions that lack newer flags.
- type CliTokenSource struct {
+-type CliTokenSource struct {
 -	// forceCmd uses --profile with --force-refresh to bypass the CLI's token cache.
 -	// Nil when cfg.Profile is empty (--force-refresh requires --profile support).
 -	forceCmd []string
--
++// cliError wraps stderr output from a failed CLI invocation.
++type cliError struct {
++	stderr string
++}
+ 
 -	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
 -	profileCmd []string
--
++func (e *cliError) Error() string {
++	return e.stderr
++}
++
++// cliCommand is a single CLI invocation with an associated warning message
++// that is logged when this command fails and we fall back to the next one.
++type cliCommand struct {
++	args []string
++	// usedFlags lists all flags passed in this command. When the CLI reports
++	// "unknown flag: <flag>" for one of these, we fall back to the next command.
++	usedFlags      []string
++	warningMessage string
++}
+ 
 -	// hostCmd uses --host as a fallback for CLIs that predate --profile support.
 -	// Nil when cfg.Host is empty.
 -	hostCmd []string
-+	commands           []cliCommand
++// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
++// It holds a list of commands ordered from most feature-rich to simplest,
++// falling back progressively for older CLI versions that lack newer flags.
++type CliTokenSource struct {
++	commands []cliCommand
++	// activeCommandIndex is the index of the CLI command known to work, or -1
++	// if not yet resolved. Once resolved it never changes — older CLIs don't
++	// gain new flags. We use atomic.Int32 instead of sync.Once because
++	// probing must be retryable on transient errors: concurrent callers may
++	// redundantly probe, but all converge to the same index.
 +	activeCommandIndex atomic.Int32
  }
  
@@ -45,7 +61,13 @@
  	}
 -	forceCmd, profileCmd, hostCmd := buildCliCommands(cliPath, cfg)
 -	return &CliTokenSource{forceCmd: forceCmd, profileCmd: profileCmd, hostCmd: hostCmd}, nil
-+	return &CliTokenSource{commands: buildCliCommands(cliPath, cfg)}, nil
++	commands := buildCliCommands(cliPath, cfg)
++	if len(commands) == 0 {
++		return nil, fmt.Errorf("cannot configure CLI token source: neither profile nor host is set")
++	}
++	ts := &CliTokenSource{commands: commands}
++	ts.activeCommandIndex.Store(-1)
++	return ts, nil
  }
  
 -// buildCliCommands constructs the CLI commands for fetching an auth token.
@@ -64,101 +86,120 @@
  	if cfg.Profile != "" {
 -		profileCmd = []string{cliPath, "auth", "token", "--profile", cfg.Profile}
 -		forceCmd = append(profileCmd, "--force-refresh")
--	}
--	if cfg.Host != "" {
--		hostCmd = buildHostCommand(cliPath, cfg)
--	}
--	return forceCmd, profileCmd, hostCmd
 +		commands = append(commands, cliCommand{
 +			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile, "--force-refresh"},
-+			flags:          []string{"--force-refresh", "--profile"},
++			usedFlags:      []string{"--force-refresh", "--profile"},
 +			warningMessage: "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.",
 +		})
 +		commands = append(commands, cliCommand{
 +			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile},
-+			flags:          []string{"--profile"},
++			usedFlags:      []string{"--profile"},
 +			warningMessage: "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.",
 +		})
-+	}
-+	commands = appendHostCommand(commands, cliPath, cfg)
-+	return commands
- }
- 
+ 	}
+ 	if cfg.Host != "" {
+-		hostCmd = buildHostCommand(cliPath, cfg)
+-	}
+-	return forceCmd, profileCmd, hostCmd
+-}
+-
 -// buildHostCommand constructs the legacy --host based CLI command.
 -func buildHostCommand(cliPath string, cfg *Config) []string {
 -	cmd := []string{cliPath, "auth", "token", "--host", cfg.Host}
-+func appendHostCommand(commands []cliCommand, cliPath string, cfg *Config) []cliCommand {
-+	if cfg.Host == "" {
-+		return commands
-+	}
-+	args := []string{cliPath, "auth", "token", "--host", cfg.Host}
- 	switch cfg.HostType() {
- 	case AccountHost:
+-	switch cfg.HostType() {
+-	case AccountHost:
 -		cmd = append(cmd, "--account-id", cfg.AccountID)
-+		args = append(args, "--account-id", cfg.AccountID)
++		args := []string{cliPath, "auth", "token", "--host", cfg.Host}
++		switch cfg.HostType() {
++		case AccountHost:
++			args = append(args, "--account-id", cfg.AccountID)
++		}
++		commands = append(commands, cliCommand{args: args})
  	}
 -	return cmd
-+	return append(commands, cliCommand{args: args})
++	return commands
  }
  
  // Token fetches an OAuth token by shelling out to the Databricks CLI.
 -// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, skipping nil
 -// entries. Each command falls through to the next on "unknown flag" errors,
 -// logging a warning about the unsupported feature.
-+// Commands are tried from [activeCommandIndex] forward, falling back to simpler
-+// commands when the CLI reports an unknown flag. Once a command succeeds,
-+// [activeCommandIndex] is updated so subsequent calls skip commands known to fail.
++// If the working command has already been identified, it is called directly.
++// Otherwise, [probeAndExec] tries each command in order to find one that works.
  func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
 -	if c.forceCmd != nil {
 -		tok, err := c.execCliCommand(ctx, c.forceCmd)
-+	for i := int(c.activeCommandIndex.Load()); i < len(c.commands); i++ {
+-		if err == nil {
+-			return tok, nil
+-		}
+-		if !isUnknownFlagError(err, "--force-refresh") && !isUnknownFlagError(err, "--profile") {
+-			return nil, err
+-		}
+-		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
++	if idx := int(c.activeCommandIndex.Load()); idx >= 0 {
++		return c.execCliCommand(ctx, c.commands[idx].args)
+ 	}
++	return c.probeAndExec(ctx)
++}
+ 
+-	if c.profileCmd != nil {
+-		tok, err := c.execCliCommand(ctx, c.profileCmd)
++// probeAndExec walks the command list from most-featured to simplest, looking
++// for a CLI command that succeeds. When a command fails with "unknown flag" for
++// one of its [cliCommand.usedFlags], it logs a warning and tries the next.
++// On success, [activeCommandIndex] is stored so future calls skip probing.
++func (c *CliTokenSource) probeAndExec(ctx context.Context) (*oauth2.Token, error) {
++	for i := range c.commands {
 +		cmd := c.commands[i]
 +		tok, err := c.execCliCommand(ctx, cmd.args)
  		if err == nil {
 +			c.activeCommandIndex.Store(int32(i))
  			return tok, nil
  		}
--		if !isUnknownFlagError(err, "--force-refresh") && !isUnknownFlagError(err, "--profile") {
+-		if !isUnknownFlagError(err, "--profile") {
 +		lastCommand := i == len(c.commands)-1
-+		if lastCommand || !isUnknownFlagError(err, cmd.flags) {
++		if lastCommand || !isUnknownFlagError(err, cmd.usedFlags) {
  			return nil, err
  		}
--		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
-+		logger.Warnf(ctx, cmd.warningMessage)
- 	}
--
--	if c.profileCmd != nil {
--		tok, err := c.execCliCommand(ctx, c.profileCmd)
--		if err == nil {
--			return tok, nil
--		}
--		if !isUnknownFlagError(err, "--profile") {
--			return nil, err
--		}
 -		logger.Warnf(ctx, "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.")
 -	}
 -
 -	if c.hostCmd == nil {
 -		return nil, fmt.Errorf("cannot get access token: no CLI commands available")
--	}
++		logger.Warnf(ctx, cmd.warningMessage)
+ 	}
 -	return c.execCliCommand(ctx, c.hostCmd)
 +	return nil, fmt.Errorf("cannot get access token: no CLI commands configured")
  }
  
  func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oauth2.Token, error) {
+ 	if err != nil {
+ 		var exitErr *exec.ExitError
+ 		if errors.As(err, &exitErr) {
+-			return nil, fmt.Errorf("cannot get access token: %q", strings.TrimSpace(string(exitErr.Stderr)))
++			return nil, fmt.Errorf("cannot get access token: %w",
++				&cliError{stderr: strings.TrimSpace(string(exitErr.Stderr))})
+ 		}
+ 		return nil, fmt.Errorf("cannot get access token: %w", err)
+ 	}
+ 	}, nil
  }
  
- // isUnknownFlagError returns true if the error indicates the CLI does not
+-// isUnknownFlagError returns true if the error indicates the CLI does not
 -// recognize a flag. Pass a specific flag (e.g. "--profile") to check for that
 -// flag, or pass "" to match any "unknown flag:" error.
 -func isUnknownFlagError(err error, flag string) bool {
 -	if flag == "" {
 -		return strings.Contains(err.Error(), "unknown flag:")
-+// recognize one of the given flags.
++// isUnknownFlagError returns true if the error wraps a [cliError] whose stderr
++// indicates the CLI does not recognize one of the given flags.
 +func isUnknownFlagError(err error, flags []string) bool {
-+	msg := err.Error()
++	var cliErr *cliError
++	if !errors.As(err, &cliErr) {
++		return false
++	}
 +	for _, flag := range flags {
-+		if strings.Contains(msg, "unknown flag: "+flag) {
++		if strings.Contains(cliErr.stderr, "unknown flag: "+flag) {
 +			return true
 +		}
  	}
config/cli_token_source_test.go
@@ -76,13 +76,13 @@
 -			gotForceCmd, gotProfileCmd, gotHostCmd := buildCliCommands(cliPath, tc.cfg)
 -			if !slices.Equal(gotForceCmd, tc.wantForceCmd) {
 -				t.Errorf("force cmd = %v, want %v", gotForceCmd, tc.wantForceCmd)
+-			}
+-			if !slices.Equal(gotProfileCmd, tc.wantProfileCmd) {
+-				t.Errorf("profile cmd = %v, want %v", gotProfileCmd, tc.wantProfileCmd)
 +			got := buildCliCommands(cliPath, tc.cfg)
 +			if len(got) != len(tc.wantCommands) {
 +				t.Fatalf("got %d commands, want %d", len(got), len(tc.wantCommands))
  			}
--			if !slices.Equal(gotProfileCmd, tc.wantProfileCmd) {
--				t.Errorf("profile cmd = %v, want %v", gotProfileCmd, tc.wantProfileCmd)
--			}
 -			if !slices.Equal(gotHostCmd, tc.wantHostCmd) {
 -				t.Errorf("host cmd = %v, want %v", gotHostCmd, tc.wantHostCmd)
 +			for i, want := range tc.wantCommands {
@@ -105,11 +105,34 @@
  		}
  	})
  
+ 			t.Errorf("NewCliTokenSource() error = %v, want %v", err, ErrCliNotFound)
+ 		}
+ 	})
++
++	t.Run("no profile or host", func(t *testing.T) {
++		cfg := &Config{DatabricksCliPath: validCliPath}
++		_, err := NewCliTokenSource(cfg)
++		if err == nil {
++			t.Fatal("NewCliTokenSource() error = nil, want error")
++		}
++		if !strings.Contains(err.Error(), "neither profile nor host is set") {
++			t.Errorf("NewCliTokenSource() error = %v, want error containing %q", err, "neither profile nor host is set")
++		}
++	})
++}
++
++func newTestCliTokenSource(commands []cliCommand) *CliTokenSource {
++	ts := &CliTokenSource{commands: commands}
++	ts.activeCommandIndex.Store(-1)
++	return ts
+ }
+ 
+ func TestCliTokenSource_Token(t *testing.T) {
  				t.Fatalf("failed to create mock script: %v", err)
  			}
  
 -			ts := &CliTokenSource{hostCmd: []string{mockScript}}
-+			ts := &CliTokenSource{commands: []cliCommand{{args: []string{mockScript}}}}
++			ts := newTestCliTokenSource([]cliCommand{{args: []string{mockScript}}})
  			token, err := ts.Token(context.Background())
  
  			if tc.wantErrMsg != "" {
@@ -120,10 +143,10 @@
 -		forceCmd:   []string{forceScript},
 -		profileCmd: []string{profileScript},
 -	}
-+	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, flags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
++	ts := newTestCliTokenSource([]cliCommand{
++		{args: []string{forceScript}, usedFlags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
 +		{args: []string{profileScript}},
-+	}}
++	})
  	token, err := ts.Token(context.Background())
  	if err != nil {
  		t.Fatalf("Token() error = %v, want fallback to profileCmd to succeed", err)
@@ -134,10 +157,10 @@
 -		profileCmd: []string{profileScript},
 -		hostCmd:    []string{hostScript},
 -	}
-+	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{profileScript}, flags: []string{"--profile"}, warningMessage: "profile not supported"},
++	ts := newTestCliTokenSource([]cliCommand{
++		{args: []string{profileScript}, usedFlags: []string{"--profile"}, warningMessage: "profile not supported"},
 +		{args: []string{hostScript}},
-+	}}
++	})
  	token, err := ts.Token(context.Background())
  	if err != nil {
  		t.Fatalf("Token() error = %v, want fallback to hostCmd to succeed", err)
@@ -170,11 +193,11 @@
 -		profileCmd: []string{profileScript},
 -		hostCmd:    []string{hostScript},
 -	}
-+	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, flags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}, flags: []string{"--profile"}, warningMessage: "profile not supported"},
++	ts := newTestCliTokenSource([]cliCommand{
++		{args: []string{forceScript}, usedFlags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
++		{args: []string{profileScript}, usedFlags: []string{"--profile"}, warningMessage: "profile not supported"},
 +		{args: []string{hostScript}},
-+	}}
++	})
  	token, err := ts.Token(context.Background())
  	if err != nil {
  		t.Fatalf("Token() error = %v, want fallback through to hostCmd to succeed", err)
@@ -200,11 +223,11 @@
 -		profileCmd: []string{profileScript},
 -		hostCmd:    []string{hostScript},
 -	}
-+	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, flags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}, flags: []string{"--profile"}, warningMessage: "profile not supported"},
++	ts := newTestCliTokenSource([]cliCommand{
++		{args: []string{forceScript}, usedFlags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
++		{args: []string{profileScript}, usedFlags: []string{"--profile"}, warningMessage: "profile not supported"},
 +		{args: []string{hostScript}},
-+	}}
++	})
  	_, err := ts.Token(context.Background())
  	if err == nil {
  		t.Fatal("Token() error = nil, want error")
@@ -243,10 +266,10 @@
 -	ts := &CliTokenSource{
 -		forceCmd:   []string{forceScript},
 -		profileCmd: []string{profileScript},
-+	ts := &CliTokenSource{commands: []cliCommand{
-+		{args: []string{forceScript}, flags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
++	ts := newTestCliTokenSource([]cliCommand{
++		{args: []string{forceScript}, usedFlags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
 +		{args: []string{hostScript}},
-+	}}
++	})
 +
 +	// First call: falls back from forceScript to hostScript.
 +	token, err := ts.Token(context.Background())
@@ -261,15 +284,15 @@
 +	}
 +	if ts.activeCommandIndex.Load() != 1 {
 +		t.Errorf("activeCommandIndex = %d, want 1", ts.activeCommandIndex.Load())
- 	}
--	if !strings.Contains(err.Error(), "no CLI commands available") {
--		t.Errorf("Token() error = %v, want error containing %q", err, "no CLI commands available")
++	}
 +
 +	// Second call: starts at activeCommandIndex, skipping the force command.
 +	token, err = ts.Token(context.Background())
 +	if err != nil {
 +		t.Fatalf("second Token() error = %v", err)
-+	}
+ 	}
+-	if !strings.Contains(err.Error(), "no CLI commands available") {
+-		t.Errorf("Token() error = %v, want error containing %q", err, "no CLI commands available")
 +	if token.AccessToken != "host-token" {
 +		t.Errorf("second AccessToken = %q, want %q", token.AccessToken, "host-token")
  	}

Reproduce locally: git range-diff 69a7c95..76e74ca 69a7c95..79ac794 | Disable: git config gitstack.push-range-diff false

var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return nil, fmt.Errorf("cannot get access token: %s", strings.TrimSpace(string(exitErr.Stderr)))
return nil, fmt.Errorf("cannot get access token: %q", strings.TrimSpace(string(exitErr.Stderr)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what I meant is that we are losing the exec.ExitError type. The new type that we have introduced does not solve that.

// Token fetches an OAuth token by shelling out to the Databricks CLI.
// If the working command has already been identified, it is called directly.
// Otherwise, [probeAndExec] tries each command in order to find one that works.
func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate the part that is about the public API (what users will see) and the part that is about the implementation details (what the developer will see). The latter is not useful to the users, they cannot see the methods that we are referencing.

Suggested change
func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
// Token fetches an OAuth token by shelling out to the Databricks CLI.
func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
// If the working command has already been identified, it is called directly.
// Otherwise, [probeAndExec] tries each command in order to find one that works.

return c.execCliCommand(ctx, c.hostCmd)
// probeAndExec walks the command list from most-featured to simplest, looking
// for a CLI command that succeeds. When a command fails with "unknown flag" for
// one of its [cliCommand.usedFlags], it logs a warning and tries the next.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// one of its [cliCommand.usedFlags], it logs a warning and tries the next.
// one of its [cliCommand.usedFlags], it logs a warning and tries the next. The "counter" is not incremented if the command fails for any other reason.

var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return nil, fmt.Errorf("cannot get access token: %s", strings.TrimSpace(string(exitErr.Stderr)))
return nil, fmt.Errorf("cannot get access token: %w",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From offline discussion: we are loosing the exec.ExitError type. I don't have an opinion on whether we should keep it or not. Though, if we don't let's be explicit about the rationale.

Pass --force-refresh to the Databricks CLI auth token command to bypass
the CLI's internal token cache. The SDK manages its own token caching,
so the CLI serving stale tokens from its cache causes unnecessary
refresh failures.

Commands are now tried in order: forceCmd (--profile + --force-refresh),
profileCmd (--profile), hostCmd (--host), falling back progressively
for older CLI versions that don't support newer flags.

Signed-off-by: Mihai Mitrea <mihai.mitrea@databricks.com>
Replace the three explicit command fields (forceCmd, profileCmd, hostCmd)
with a []cliCommand list and an activeCommandIndex. Feature flags are
defined statically in cliFeatureFlags and stripped progressively to
build commands for older CLI versions.

Token() now iterates from activeCommandIndex, falling back on unknown
flag errors and caching the working command index for subsequent calls.
Adding future flags (e.g. --scopes) requires only a one-line addition
to the cliFeatureFlags slice.

Signed-off-by: Mihai Mitrea <mihai.mitrea@databricks.com>
@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/generalize-cli-commands branch from 79ac794 to a19dab0 Compare April 7, 2026 09:18
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: stack/force-refresh-flag (79ac794 -> a19dab0)
NEXT_CHANGELOG.md
@@ -1,10 +1,10 @@
 diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
 --- a/NEXT_CHANGELOG.md
 +++ b/NEXT_CHANGELOG.md
- 
  ### Internal Changes
  
+  * Pass `--force-refresh` to Databricks CLI `auth token` command to bypass the CLI's internal token cache.
 + * Generalize CLI token source into a progressive command list for forward-compatible flag support.
+  * Use resolved host type from host metadata in `HostType()` method, falling back to URL pattern matching when metadata is unavailable.
   * Normalize internal token sources on `auth.TokenSource` for proper context propagation ([#1577](https://github.com/databricks/databricks-sdk-go/pull/1577)).
-  * Fix `TestAzureGithubOIDCCredentials` hang caused by missing `HTTPTransport` stub: `EnsureResolved` now calls `resolveHostMetadata`, which makes a real network request when no transport is set ([#1550](https://github.com/databricks/databricks-sdk-go/pull/1550)).
-  * Bump golang.org/x/crypto from 0.21.0 to 0.45.0 in /examples/slog ([#1566](https://github.com/databricks/databricks-sdk-go/pull/1566)).
\ No newline at end of file
+  * Fix `TestAzureGithubOIDCCredentials` hang caused by missing `HTTPTransport` stub: `EnsureResolved` now calls `resolveHostMetadata`, which makes a real network request when no transport is set ([#1550](https://github.com/databricks/databricks-sdk-go/pull/1550)).
\ No newline at end of file
config/cli_token_source.go
@@ -11,24 +11,6 @@
  	Expiry      string `json:"expiry"`
  }
  
--// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
--// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, progressively
--// falling back to simpler invocations for older CLI versions.
--type CliTokenSource struct {
--	// forceCmd uses --profile with --force-refresh to bypass the CLI's token cache.
--	// Nil when cfg.Profile is empty (--force-refresh requires --profile support).
--	forceCmd []string
-+// cliError wraps stderr output from a failed CLI invocation.
-+type cliError struct {
-+	stderr string
-+}
- 
--	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
--	profileCmd []string
-+func (e *cliError) Error() string {
-+	return e.stderr
-+}
-+
 +// cliCommand is a single CLI invocation with an associated warning message
 +// that is logged when this command fails and we fall back to the next one.
 +type cliCommand struct {
@@ -38,14 +20,26 @@
 +	usedFlags      []string
 +	warningMessage string
 +}
- 
++
+ // CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
+-// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, progressively
+-// falling back to simpler invocations for older CLI versions.
++// It holds a list of commands ordered from most feature-rich to simplest,
++// falling back progressively for older CLI versions that lack newer flags.
+ type CliTokenSource struct {
+-	// forceCmd appends --force-refresh to the base command (profileCmd when a
+-	// profile is configured, hostCmd otherwise) to bypass the CLI's token cache.
+-	// Nil when neither profile nor host is set.
+-	// CLI support: >= v0.296.0 (databricks/cli#4767).
+-	forceCmd []string
+-
+-	// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
+-	// CLI support: >= v0.207.1 (databricks/cli#855).
+-	profileCmd []string
+-
 -	// hostCmd uses --host as a fallback for CLIs that predate --profile support.
 -	// Nil when cfg.Host is empty.
 -	hostCmd []string
-+// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
-+// It holds a list of commands ordered from most feature-rich to simplest,
-+// falling back progressively for older CLI versions that lack newer flags.
-+type CliTokenSource struct {
 +	commands []cliCommand
 +	// activeCommandIndex is the index of the CLI command known to work, or -1
 +	// if not yet resolved. Once resolved it never changes — older CLIs don't
@@ -71,21 +65,24 @@
  }
  
 -// buildCliCommands constructs the CLI commands for fetching an auth token.
--// When cfg.Profile is set, three commands are built: a --force-refresh variant,
--// a plain --profile variant, and (when host is available) a --host fallback.
--// When cfg.Profile is empty, only --host is returned — the CLI must support
--// --profile before --force-refresh can be used (monotonic feature assumption).
+-// When cfg.Profile is set, three commands are built: a --force-refresh variant
+-// (based on profileCmd), a plain --profile variant, and (when host is available)
+-// a --host fallback. When cfg.Profile is empty, --force-refresh is based on the
+-// --host command instead.
 -func buildCliCommands(cliPath string, cfg *Config) ([]string, []string, []string) {
 -	var forceCmd, profileCmd, hostCmd []string
 +// buildCliCommands constructs the list of CLI commands for fetching an auth
 +// token, ordered from most feature-rich to simplest. The order defines the
 +// fallback chain: when a command fails with an unknown flag error, the next
 +// one is tried.
++//
++// When cfg.Profile is set, --force-refresh is based on the --profile command.
++// When cfg.Profile is empty, --force-refresh is based on the --host command
++// instead, so host-only configurations still benefit from cache bypass.
 +func buildCliCommands(cliPath string, cfg *Config) []cliCommand {
 +	var commands []cliCommand
  	if cfg.Profile != "" {
 -		profileCmd = []string{cliPath, "auth", "token", "--profile", cfg.Profile}
--		forceCmd = append(profileCmd, "--force-refresh")
 +		commands = append(commands, cliCommand{
 +			args:           []string{cliPath, "auth", "token", "--profile", cfg.Profile, "--force-refresh"},
 +			usedFlags:      []string{"--force-refresh", "--profile"},
@@ -100,6 +97,11 @@
  	if cfg.Host != "" {
 -		hostCmd = buildHostCommand(cliPath, cfg)
 -	}
+-	if profileCmd != nil {
+-		forceCmd = append(profileCmd, "--force-refresh")
+-	} else if hostCmd != nil {
+-		forceCmd = append(hostCmd, "--force-refresh")
+-	}
 -	return forceCmd, profileCmd, hostCmd
 -}
 -
@@ -109,12 +111,20 @@
 -	switch cfg.HostType() {
 -	case AccountHost:
 -		cmd = append(cmd, "--account-id", cfg.AccountID)
-+		args := []string{cliPath, "auth", "token", "--host", cfg.Host}
++		hostArgs := []string{cliPath, "auth", "token", "--host", cfg.Host}
 +		switch cfg.HostType() {
 +		case AccountHost:
-+			args = append(args, "--account-id", cfg.AccountID)
++			hostArgs = append(hostArgs, "--account-id", cfg.AccountID)
 +		}
-+		commands = append(commands, cliCommand{args: args})
++		if cfg.Profile == "" {
++			forceArgs := append(hostArgs[:len(hostArgs):len(hostArgs)], "--force-refresh")
++			commands = append(commands, cliCommand{
++				args:           forceArgs,
++				usedFlags:      []string{"--force-refresh"},
++				warningMessage: "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.",
++			})
++		}
++		commands = append(commands, cliCommand{args: hostArgs})
  	}
 -	return cmd
 +	return commands
@@ -124,8 +134,6 @@
 -// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, skipping nil
 -// entries. Each command falls through to the next on "unknown flag" errors,
 -// logging a warning about the unsupported feature.
-+// If the working command has already been identified, it is called directly.
-+// Otherwise, [probeAndExec] tries each command in order to find one that works.
  func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
 -	if c.forceCmd != nil {
 -		tok, err := c.execCliCommand(ctx, c.forceCmd)
@@ -136,6 +144,8 @@
 -			return nil, err
 -		}
 -		logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
++	// If the working command has already been identified, call it directly.
++	// Otherwise, probe each command in order to find one that works.
 +	if idx := int(c.activeCommandIndex.Load()); idx >= 0 {
 +		return c.execCliCommand(ctx, c.commands[idx].args)
  	}
@@ -146,7 +156,8 @@
 -		tok, err := c.execCliCommand(ctx, c.profileCmd)
 +// probeAndExec walks the command list from most-featured to simplest, looking
 +// for a CLI command that succeeds. When a command fails with "unknown flag" for
-+// one of its [cliCommand.usedFlags], it logs a warning and tries the next.
++// one of its [cliCommand.usedFlags], it logs a warning and tries the next. Any
++// other error stops probing immediately and is returned to the caller.
 +// On success, [activeCommandIndex] is stored so future calls skip probing.
 +func (c *CliTokenSource) probeAndExec(ctx context.Context) (*oauth2.Token, error) {
 +	for i := range c.commands {
@@ -177,8 +188,13 @@
  		var exitErr *exec.ExitError
  		if errors.As(err, &exitErr) {
 -			return nil, fmt.Errorf("cannot get access token: %q", strings.TrimSpace(string(exitErr.Stderr)))
-+			return nil, fmt.Errorf("cannot get access token: %w",
-+				&cliError{stderr: strings.TrimSpace(string(exitErr.Stderr))})
++			// Format with %q: the CLI's stderr can be multi-line (includes usage
++			// text on unknown-flag errors) and quoting keeps it on one line so
++			// log aggregators don't split a single error across multiple entries.
++			// We intentionally discard exec.ExitError — the stderr text is the
++			// CLI's error contract; exit codes and process state are not useful.
++			return nil, fmt.Errorf("cannot get access token: %q",
++				strings.TrimSpace(string(exitErr.Stderr)))
  		}
  		return nil, fmt.Errorf("cannot get access token: %w", err)
  	}
@@ -186,24 +202,18 @@
  }
  
 -// isUnknownFlagError returns true if the error indicates the CLI does not
--// recognize a flag. Pass a specific flag (e.g. "--profile") to check for that
--// flag, or pass "" to match any "unknown flag:" error.
+-// recognize the given flag (e.g. "--profile", "--force-refresh").
 -func isUnknownFlagError(err error, flag string) bool {
--	if flag == "" {
--		return strings.Contains(err.Error(), "unknown flag:")
-+// isUnknownFlagError returns true if the error wraps a [cliError] whose stderr
-+// indicates the CLI does not recognize one of the given flags.
+-	return strings.Contains(err.Error(), "unknown flag: "+flag)
++// isUnknownFlagError returns true if the error message indicates the CLI does
++// not recognize one of the given flags.
 +func isUnknownFlagError(err error, flags []string) bool {
-+	var cliErr *cliError
-+	if !errors.As(err, &cliErr) {
-+		return false
-+	}
++	msg := err.Error()
 +	for _, flag := range flags {
-+		if strings.Contains(cliErr.stderr, "unknown flag: "+flag) {
++		if strings.Contains(msg, "unknown flag: "+flag) {
 +			return true
 +		}
- 	}
--	return strings.Contains(err.Error(), "unknown flag: "+flag)
++	}
 +	return false
  }
  
config/cli_token_source_test.go
@@ -14,22 +14,26 @@
 +		wantCommands [][]string
  	}{
  		{
--			name:        "workspace host only",
--			cfg:         &Config{Host: host},
--			wantHostCmd: []string{cliPath, "auth", "token", "--host", host},
-+			name: "workspace host only",
+-			name:         "workspace host only — force-refresh based on host",
+-			cfg:          &Config{Host: host},
+-			wantForceCmd: []string{cliPath, "auth", "token", "--host", host, "--force-refresh"},
+-			wantHostCmd:  []string{cliPath, "auth", "token", "--host", host},
++			name: "workspace host only — force-refresh based on host",
 +			cfg:  &Config{Host: host},
 +			wantCommands: [][]string{
++				{cliPath, "auth", "token", "--host", host, "--force-refresh"},
 +				{cliPath, "auth", "token", "--host", host},
 +			},
  		},
  		{
--			name:        "account host only",
--			cfg:         &Config{Host: accountHost, AccountID: accountID},
--			wantHostCmd: []string{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID},
-+			name: "account host only",
+-			name:         "account host only — force-refresh based on host with account-id",
+-			cfg:          &Config{Host: accountHost, AccountID: accountID},
+-			wantForceCmd: []string{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID, "--force-refresh"},
+-			wantHostCmd:  []string{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID},
++			name: "account host only — force-refresh based on host with account-id",
 +			cfg:  &Config{Host: accountHost, AccountID: accountID},
 +			wantCommands: [][]string{
++				{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID, "--force-refresh"},
 +				{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID},
 +			},
  		},
@@ -38,18 +42,20 @@
  				AccountID:   accountID,
  				WorkspaceID: workspaceID,
  			},
--			wantHostCmd: []string{cliPath, "auth", "token", "--host", unifiedHost},
+-			wantForceCmd: []string{cliPath, "auth", "token", "--host", unifiedHost, "--force-refresh"},
+-			wantHostCmd:  []string{cliPath, "auth", "token", "--host", unifiedHost},
 +			wantCommands: [][]string{
++				{cliPath, "auth", "token", "--host", unifiedHost, "--force-refresh"},
 +				{cliPath, "auth", "token", "--host", unifiedHost},
 +			},
  		},
  		{
--			name:           "profile with host — all three commands",
+-			name:           "profile with host — force-refresh based on profile",
 -			cfg:            &Config{Profile: "my-profile", Host: host},
 -			wantForceCmd:   []string{cliPath, "auth", "token", "--profile", "my-profile", "--force-refresh"},
 -			wantProfileCmd: []string{cliPath, "auth", "token", "--profile", "my-profile"},
 -			wantHostCmd:    []string{cliPath, "auth", "token", "--host", host},
-+			name: "profile with host — all three commands",
++			name: "profile with host — force-refresh based on profile",
 +			cfg:  &Config{Profile: "my-profile", Host: host},
 +			wantCommands: [][]string{
 +				{cliPath, "auth", "token", "--profile", "my-profile", "--force-refresh"},
@@ -136,110 +142,70 @@
  			token, err := ts.Token(context.Background())
  
  			if tc.wantErrMsg != "" {
- 		t.Fatalf("failed to create profile script: %v", err)
+ 			wantErrMsg:    "databricks OAuth is not configured",
+ 		},
+ 		{
+-			name:          "nil hostCmd after profile failure returns error",
++			name:          "all commands fail with unknown flag — last error returned",
+ 			forceScript:   unknownProfile,
+ 			profileScript: unknownProfile,
+-			wantErrMsg:    "no CLI commands available",
++			wantErrMsg:    "unknown flag: --profile",
+ 		},
  	}
  
--	ts := &CliTokenSource{
--		forceCmd:   []string{forceScript},
--		profileCmd: []string{profileScript},
--	}
-+	ts := newTestCliTokenSource([]cliCommand{
-+		{args: []string{forceScript}, usedFlags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}},
-+	})
- 	token, err := ts.Token(context.Background())
- 	if err != nil {
- 		t.Fatalf("Token() error = %v, want fallback to profileCmd to succeed", err)
- 		t.Fatalf("failed to create host script: %v", err)
- 	}
+ 	for _, tc := range testCases {
+ 		t.Run(tc.name, func(t *testing.T) {
+ 			tempDir := t.TempDir()
+-			var ts CliTokenSource
++			var commands []cliCommand
  
--	ts := &CliTokenSource{
--		profileCmd: []string{profileScript},
--		hostCmd:    []string{hostScript},
--	}
-+	ts := newTestCliTokenSource([]cliCommand{
-+		{args: []string{profileScript}, usedFlags: []string{"--profile"}, warningMessage: "profile not supported"},
-+		{args: []string{hostScript}},
-+	})
- 	token, err := ts.Token(context.Background())
- 	if err != nil {
- 		t.Fatalf("Token() error = %v, want fallback to hostCmd to succeed", err)
- 	}
- }
+ 			if tc.forceScript != "" {
+ 				path := filepath.Join(tempDir, "force_cli.sh")
+ 				if err := os.WriteFile(path, []byte(tc.forceScript), 0755); err != nil {
+ 					t.Fatalf("failed to create force script: %v", err)
+ 				}
+-				ts.forceCmd = []string{path}
++				commands = append(commands, cliCommand{
++					args:           []string{path},
++					usedFlags:      []string{"--force-refresh", "--profile"},
++					warningMessage: "force-refresh not supported",
++				})
+ 			}
+ 			if tc.profileScript != "" {
+ 				path := filepath.Join(tempDir, "profile_cli.sh")
+ 				if err := os.WriteFile(path, []byte(tc.profileScript), 0755); err != nil {
+ 					t.Fatalf("failed to create profile script: %v", err)
+ 				}
+-				ts.profileCmd = []string{path}
++				commands = append(commands, cliCommand{
++					args:           []string{path},
++					usedFlags:      []string{"--profile"},
++					warningMessage: "profile not supported",
++				})
+ 			}
+ 			if tc.hostScript != "" {
+ 				path := filepath.Join(tempDir, "host_cli.sh")
+ 				if err := os.WriteFile(path, []byte(tc.hostScript), 0755); err != nil {
+ 					t.Fatalf("failed to create host script: %v", err)
+ 				}
+-				ts.hostCmd = []string{path}
++				commands = append(commands, cliCommand{args: []string{path}})
+ 			}
  
--func TestCliTokenSource_Token_ForceRefreshFallbackToHostOnProfileError(t *testing.T) {
-+func TestCliTokenSource_Token_FullFallbackChain(t *testing.T) {
- 	if runtime.GOOS == "windows" {
- 		t.Skip("Skipping shell script test on Windows")
- 	}
- 
- 	tempDir := t.TempDir()
- 
--	// forceCmd fails with --profile unknown (very old CLI).
- 	forceScript := filepath.Join(tempDir, "force_cli.sh")
- 	if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
- 		t.Fatalf("failed to create force script: %v", err)
- 	}
- 
--	// profileCmd also fails with --profile unknown.
- 	profileScript := filepath.Join(tempDir, "profile_cli.sh")
- 	if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
- 		t.Fatalf("failed to create profile script: %v", err)
- 		t.Fatalf("failed to create host script: %v", err)
- 	}
- 
--	ts := &CliTokenSource{
--		forceCmd:   []string{forceScript},
--		profileCmd: []string{profileScript},
--		hostCmd:    []string{hostScript},
--	}
-+	ts := newTestCliTokenSource([]cliCommand{
-+		{args: []string{forceScript}, usedFlags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}, usedFlags: []string{"--profile"}, warningMessage: "profile not supported"},
-+		{args: []string{hostScript}},
-+	})
- 	token, err := ts.Token(context.Background())
- 	if err != nil {
- 		t.Fatalf("Token() error = %v, want fallback through to hostCmd to succeed", err)
- 
- 	tempDir := t.TempDir()
- 
--	// forceCmd fails with a real auth error (not unknown flag).
- 	forceScript := filepath.Join(tempDir, "force_cli.sh")
- 	if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'cache: databricks OAuth is not configured for this host' >&2\nexit 1"), 0755); err != nil {
- 		t.Fatalf("failed to create force script: %v", err)
- 	}
- 
--	// profileCmd and hostCmd should not be called.
-+	// Subsequent commands should not be called.
- 	profileScript := filepath.Join(tempDir, "profile_cli.sh")
- 	if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'should not reach here' >&2\nexit 1"), 0755); err != nil {
- 		t.Fatalf("failed to create profile script: %v", err)
- 		t.Fatalf("failed to create host script: %v", err)
- 	}
- 
--	ts := &CliTokenSource{
--		forceCmd:   []string{forceScript},
--		profileCmd: []string{profileScript},
--		hostCmd:    []string{hostScript},
--	}
-+	ts := newTestCliTokenSource([]cliCommand{
-+		{args: []string{forceScript}, usedFlags: []string{"--force-refresh", "--profile"}, warningMessage: "force-refresh not supported"},
-+		{args: []string{profileScript}, usedFlags: []string{"--profile"}, warningMessage: "profile not supported"},
-+		{args: []string{hostScript}},
-+	})
- 	_, err := ts.Token(context.Background())
- 	if err == nil {
- 		t.Fatal("Token() error = nil, want error")
++			ts := newTestCliTokenSource(commands)
+ 			token, err := ts.Token(context.Background())
+ 			if tc.wantErrMsg != "" {
+ 				if err == nil || !strings.Contains(err.Error(), tc.wantErrMsg) {
+ 		})
  	}
  }
- 
--func TestCliTokenSource_Token_NilHostCmdReturnsError(t *testing.T) {
++
 +func TestCliTokenSource_Token_ActiveCommandIndexPersists(t *testing.T) {
- 	if runtime.GOOS == "windows" {
- 		t.Skip("Skipping shell script test on Windows")
- 	}
- 
++	if runtime.GOOS == "windows" {
++		t.Skip("Skipping shell script test on Windows")
++	}
++
 +	expiry := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
 +	validResponse, _ := json.Marshal(cliTokenResponse{
 +		AccessToken: "host-token",
@@ -247,38 +213,27 @@
 +		Expiry:      expiry,
 +	})
 +
- 	tempDir := t.TempDir()
- 
- 	forceScript := filepath.Join(tempDir, "force_cli.sh")
--	if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
++	tempDir := t.TempDir()
++
++	forceScript := filepath.Join(tempDir, "force_cli.sh")
 +	if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --force-refresh' >&2\nexit 1"), 0755); err != nil {
- 		t.Fatalf("failed to create force script: %v", err)
- 	}
- 
--	profileScript := filepath.Join(tempDir, "profile_cli.sh")
--	if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
--		t.Fatalf("failed to create profile script: %v", err)
++		t.Fatalf("failed to create force script: %v", err)
++	}
++
 +	hostScript := filepath.Join(tempDir, "host_cli.sh")
 +	if err := os.WriteFile(hostScript, []byte("#!/bin/sh\necho '"+string(validResponse)+"'"), 0755); err != nil {
 +		t.Fatalf("failed to create host script: %v", err)
- 	}
- 
--	ts := &CliTokenSource{
--		forceCmd:   []string{forceScript},
--		profileCmd: []string{profileScript},
++	}
++
 +	ts := newTestCliTokenSource([]cliCommand{
 +		{args: []string{forceScript}, usedFlags: []string{"--force-refresh"}, warningMessage: "force-refresh not supported"},
 +		{args: []string{hostScript}},
 +	})
 +
-+	// First call: falls back from forceScript to hostScript.
 +	token, err := ts.Token(context.Background())
 +	if err != nil {
 +		t.Fatalf("first Token() error = %v", err)
- 	}
--	_, err := ts.Token(context.Background())
--	if err == nil {
--		t.Fatal("Token() error = nil, want error")
++	}
 +	if token.AccessToken != "host-token" {
 +		t.Errorf("first AccessToken = %q, want %q", token.AccessToken, "host-token")
 +	}
@@ -286,14 +241,11 @@
 +		t.Errorf("activeCommandIndex = %d, want 1", ts.activeCommandIndex.Load())
 +	}
 +
-+	// Second call: starts at activeCommandIndex, skipping the force command.
 +	token, err = ts.Token(context.Background())
 +	if err != nil {
 +		t.Fatalf("second Token() error = %v", err)
- 	}
--	if !strings.Contains(err.Error(), "no CLI commands available") {
--		t.Errorf("Token() error = %v, want error containing %q", err, "no CLI commands available")
++	}
 +	if token.AccessToken != "host-token" {
 +		t.Errorf("second AccessToken = %q, want %q", token.AccessToken, "host-token")
- 	}
- }
\ No newline at end of file
++	}
++}
\ No newline at end of file

Reproduce locally: git range-diff 69a7c95..79ac794 a314000..a19dab0 | Disable: git config gitstack.push-range-diff false

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

If integration tests don't run automatically, an authorized user can run them manually by following the instructions below:

Trigger:
go/deco-tests-run/sdk-go

Inputs:

  • PR number: 1605
  • Commit SHA: a19dab0baa3eb94bc3e13119f811d7f3fa1fe401

Checks will be approved automatically on success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants