Skip to content
Merged
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

* Added `databricks aitools` command group for installing Databricks skills into your coding agents (Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity). Skills are fetched from [github.com/databricks/databricks-agent-skills](https://github.com/databricks/databricks-agent-skills) and either symlinked into each agent's skills directory or copied into the current project. Use `databricks aitools install` to set up, `update` to pull newer versions, `list` to see what's available, and `uninstall` to remove them. Pick where they go with `--scope=project|global` (`--scope=both` is accepted on `update` and `list`).
* `[__settings__].default_profile` is now consulted as a fallback by `databricks api`, `databricks auth token`, and bundle commands when neither `--profile` nor `DATABRICKS_CONFIG_PROFILE` is set. `databricks auth token` continues to give precedence to `DATABRICKS_HOST` over `default_profile`. For bundle commands, `default_profile` only applies when the bundle does not pin its own `workspace.host`.
* Fixed bug where auth commands did not load the DEFAULT profile properly during auth where type is `databricks-cli`.
* `databricks workspace import-dir` now skips `.git`, `.databricks`, and `node_modules` directories during recursive imports. To import one of these directories deliberately, pass it as `SOURCE_PATH` ([#5118](https://github.com/databricks/cli/pull/5118)).
* `databricks postgres create-role --help` now documents the `--json` body shape and rejects the common mistake of wrapping the body in `{"role": ...}` client-side with a hint pointing at the correct shape ([#5111](https://github.com/databricks/cli/pull/5111)).
* `databricks aitools list` honors `--output json`, emitting a structured `{release, skills[...], summary{}}` document so coding agents and CI can consume the skill/version/installation matrix without scraping the tabular text output ([#5233](https://github.com/databricks/cli/pull/5233)).
Expand Down
3 changes: 2 additions & 1 deletion cmd/root/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ token = named-token

w := cmdctx.WorkspaceClient(cmd.Context())
require.NotNil(t, w)
assert.Equal(t, "", w.Config.Profile)
// Pinned so the OAuth cache key matches what `databricks auth login` writes.
assert.Equal(t, "DEFAULT", w.Config.Profile)
assert.Equal(t, "https://default.cloud.databricks.com", w.Config.Host)
}
12 changes: 7 additions & 5 deletions cmd/root/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ func configureProfile(cmd *cobra.Command, b *bundle.Bundle) {
profile := getProfile(cmd)

// Fall back to [__settings__].default_profile only when the bundle does
// not pin its own host. If the bundle declares workspace.host, applying
// default_profile here could route the user to a profile that points at
// a different host than the bundle expects — let the SDK resolve auth
// from the host instead.
// not pin its own host. The legacy [DEFAULT] section is intentionally
// NOT considered here: a hostless bundle silently routing to whatever
// [DEFAULT] points at could deploy to the wrong workspace and mask a
// missing workspace.host. Auth-only paths use the broader
// databrickscfg.ResolveDefaultProfile, which also accepts [DEFAULT].
if profile == "" && b.Config.Workspace.Host == "" && b.Config.Workspace.Profile == "" {
profile = databrickscfg.ResolveDefaultProfile(cmd.Context())
configFilePath := envlib.Get(cmd.Context(), "DATABRICKS_CONFIG_FILE")
profile, _ = databrickscfg.GetConfiguredDefaultProfile(cmd.Context(), configFilePath)
}

if profile == "" {
Expand Down
36 changes: 26 additions & 10 deletions libs/databrickscfg/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,42 @@ func GetConfiguredDefaultProfile(ctx context.Context, configFilePath string) (st
return GetConfiguredDefaultProfileFrom(configFile), nil
}

// ResolveDefaultProfile returns the [__settings__].default_profile value from
// the config file pointed to by DATABRICKS_CONFIG_FILE (or ~/.databrickscfg
// when unset). Returns "" with no error when the file is missing, the setting
// is absent, or parsing fails (a warning is logged on parse error).
// ResolveDefaultProfile returns the default profile from the config file
// pointed to by DATABRICKS_CONFIG_FILE (or ~/.databrickscfg when unset):
// [__settings__].default_profile, else [DEFAULT] if it has a host key,
// else "". Returns "" with no error when the file is missing or parsing
// fails (a warning is logged on parse error).
//
// Callers must respect their own higher-priority sources (an explicit
// --profile flag or DATABRICKS_CONFIG_PROFILE env var) before consulting
// this helper. default_profile is a CLI-level fallback; the SDK does not
// know about it.
// this helper. default_profile and [DEFAULT] are CLI-level fallbacks; the
// SDK loader silently falls back to [DEFAULT] but leaves cfg.Profile empty,
// which breaks the per-profile OAuth cache key. Pinning the name here keeps
// cfg.Profile in sync with what the SDK would read.
//
// Single-profile fallback (using "the only profile in the file" as the
// default) is intentionally NOT applied: that is a prompt-seeding convenience
// (see GetDefaultProfile), not an auth rule, and it would silently route a
// single account-only profile through the workspace-client path.
func ResolveDefaultProfile(ctx context.Context) string {
configFilePath := env.Get(ctx, "DATABRICKS_CONFIG_FILE")
resolved, err := GetConfiguredDefaultProfile(ctx, configFilePath)
configFile, err := loadConfigFile(ctx, configFilePath)
if err != nil {
log.Warnf(ctx, "Failed to load default profile: %v", err)
return ""
}
if resolved != "" {
log.Debugf(ctx, "profile %q resolved from [__settings__].default_profile", resolved)
if configFile == nil {
return ""
}
if profile := GetConfiguredDefaultProfileFrom(configFile); profile != "" {
log.Debugf(ctx, "profile %q resolved from [__settings__].default_profile", profile)
return profile
}
return resolved
if section := configFile.Section(ini.DefaultSection); section.HasKey("host") {
log.Debugf(ctx, "profile %q resolved from the [DEFAULT] section", ini.DefaultSection)
return ini.DefaultSection
}
return ""
}

// GetConfiguredDefaultProfileFrom returns the explicit default profile from
Expand Down
17 changes: 16 additions & 1 deletion libs/databrickscfg/ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ func TestResolveDefaultProfile(t *testing.T) {
want: "my-workspace",
},
{
name: "settings without default_profile",
name: "settings without default_profile, no DEFAULT section",
contents: "[__settings__]\nauth_storage = secure\n\n[my-workspace]\nhost = https://abc\n",
want: "",
},
Expand All @@ -341,6 +341,21 @@ func TestResolveDefaultProfile(t *testing.T) {
contents: "[__settings__]\ndefault_profile = __settings__\n\n[profile1]\nhost = https://abc\n",
want: "",
},
{
name: "DEFAULT section with host is used when settings is empty",
contents: "[DEFAULT]\nhost = https://default.abc\n\n[profile1]\nhost = https://abc\n",
want: "DEFAULT",
},
{
name: "DEFAULT section without host is ignored",
contents: "[DEFAULT]\naccount_id = 1234\n\n[profile1]\nhost = https://abc\n",
want: "",
},
{
name: "settings takes precedence over DEFAULT section",
contents: "[__settings__]\ndefault_profile = override\n\n[DEFAULT]\nhost = https://default.abc\n\n[override]\nhost = https://override.abc\n",
want: "override",
},
}

for _, tc := range cases {
Expand Down
Loading