Skip to content

Commit c7eefd2

Browse files
committed
Merge branch 'main' of github.com:databricks/cli into go-sdk-bump
2 parents a3afe13 + be5dada commit c7eefd2

File tree

270 files changed

+4473
-1422
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

270 files changed

+4473
-1422
lines changed

.release_metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"timestamp": "2026-02-05 13:18:41+0000"
2+
"timestamp": "2026-02-12 09:31:35+0000"
33
}

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Version changelog
22

3+
## Release v0.288.0 (2026-02-12)
4+
5+
### Bundles
6+
7+
* Add support for task-level `compute` configuration with hardware accelerators (GPU_1xA10, GPU_8xH100) ([#4457](https://github.com/databricks/cli/pull/4457))
8+
9+
### Dependency updates
10+
11+
* Upgrade Go SDK to 0.104.0 and Terraform provider to 1.105.0 ([#4457](https://github.com/databricks/cli/pull/4457))
12+
13+
314
## Release v0.287.0 (2026-02-05)
415

516
### CLI

Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ integration:
147147
integration-short:
148148
VERBOSE_TEST=1 $(INTEGRATION) -short
149149

150+
dbr-integration:
151+
DBR_ENABLED=true go test -v -timeout 4h -run TestDbrAcceptance$$ ./acceptance
152+
153+
# DBR acceptance tests - run on Databricks Runtime using serverless compute
154+
# These require deco env run for authentication
155+
# Set DBR_TEST_VERBOSE=1 for detailed output (e.g., DBR_TEST_VERBOSE=1 make dbr-test)
156+
dbr-test:
157+
deco env run -i -n aws-prod-ucws -- make dbr-integration
158+
150159
generate-validation:
151160
go run ./bundle/internal/validation/.
152161
gofmt -w -s ./bundle/internal/validation/generated

NEXT_CHANGELOG.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# NEXT CHANGELOG
22

3-
## Release v0.288.0
4-
5-
### Notable Changes
6-
7-
### CLI
3+
## Release v0.289.0
84

95
### Bundles
6+
* Log artifact build output in debug mode ([#4208](https://github.com/databricks/cli/pull/4208))
107

118
### Dependency updates
129

13-
### API Changes
10+
### Bundles
11+
* Fix bundle init not working in Azure Government ([#4286](https://github.com/databricks/cli/pull/4286))

acceptance/acceptance_test.go

Lines changed: 76 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ var (
4646
SkipLocal bool
4747
UseVersion string
4848
WorkspaceTmpDir bool
49-
TerraformDir string
5049
OnlyOutTestToml bool
5150
)
5251

@@ -79,11 +78,6 @@ func init() {
7978
// DABs in the workspace runs on the workspace file system. This flags does the same for acceptance tests
8079
// to simulate an identical environment.
8180
flag.BoolVar(&WorkspaceTmpDir, "workspace-tmp-dir", false, "Run tests on the workspace file system (For DBR testing).")
82-
83-
// Symlinks from workspace file system to local file mount are not supported on DBR. Terraform implicitly
84-
// creates these symlinks when a file_mirror is used for a provider (in .terraformrc). This flag
85-
// allows us to download the provider to the workspace file system on DBR enabling DBR integration testing.
86-
flag.StringVar(&TerraformDir, "terraform-dir", "", "Directory to download the terraform provider to")
8781
flag.BoolVar(&OnlyOutTestToml, "only-out-test-toml", false, "Only regenerate out.test.toml files without running tests")
8882
}
8983

@@ -173,14 +167,11 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
173167

174168
buildDir := getBuildDir(t, cwd, runtime.GOOS, runtime.GOARCH)
175169

176-
terraformDir := TerraformDir
177-
if terraformDir == "" {
178-
terraformDir = buildDir
170+
// Set up terraform for tests. Skip on DBR - tests with RunsOnDbr only use direct deployment.
171+
if !WorkspaceTmpDir {
172+
setupTerraform(t, cwd, buildDir, &repls)
179173
}
180174

181-
// Download terraform and provider and create config.
182-
RunCommand(t, []string{"python3", filepath.Join(cwd, "install_terraform.py"), "--targetdir", terraformDir}, ".", []string{})
183-
184175
wheelPath := buildDatabricksBundlesWheel(t, buildDir)
185176
if wheelPath != "" {
186177
t.Setenv("DATABRICKS_BUNDLES_WHEEL", wheelPath)
@@ -210,7 +201,12 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
210201
}
211202
}
212203

213-
BuildYamlfmt(t)
204+
// Skip building yamlfmt when running on workspace filesystem (DBR).
205+
// This fails today on DBR. Can be looked into and fixed as a follow-up
206+
// as and when needed.
207+
if !WorkspaceTmpDir {
208+
BuildYamlfmt(t)
209+
}
214210

215211
t.Setenv("CLI", execPath)
216212
repls.SetPath(execPath, "[CLI]")
@@ -255,16 +251,6 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
255251
t.Setenv("CLI_RELEASES_DIR", releasesDir)
256252
}
257253

258-
terraformrcPath := filepath.Join(terraformDir, ".terraformrc")
259-
t.Setenv("TF_CLI_CONFIG_FILE", terraformrcPath)
260-
t.Setenv("DATABRICKS_TF_CLI_CONFIG_FILE", terraformrcPath)
261-
repls.SetPath(terraformrcPath, "[DATABRICKS_TF_CLI_CONFIG_FILE]")
262-
263-
terraformExecPath := filepath.Join(terraformDir, "terraform") + exeSuffix
264-
t.Setenv("DATABRICKS_TF_EXEC_PATH", terraformExecPath)
265-
t.Setenv("TERRAFORM", terraformExecPath)
266-
repls.SetPath(terraformExecPath, "[TERRAFORM]")
267-
268254
// do it last so that full paths match first:
269255
repls.SetPath(buildDir, "[BUILD_DIR]")
270256

@@ -429,8 +415,8 @@ func getSkipReason(config *internal.TestConfig, configPath string) string {
429415
return ""
430416
}
431417

432-
if isTruePtr(config.SkipOnDbr) && WorkspaceTmpDir {
433-
return "Disabled via SkipOnDbr setting in " + configPath
418+
if WorkspaceTmpDir && !isTruePtr(config.RunsOnDbr) {
419+
return "Disabled because RunsOnDbr is not set in " + configPath
434420
}
435421

436422
if isTruePtr(config.Slow) && testing.Short() {
@@ -499,6 +485,13 @@ func runTest(t *testing.T,
499485
customEnv []string,
500486
envFilters []string,
501487
) {
488+
// Check env filters early, before creating any resources like directories on the file system.
489+
// Creating / deleting too many directories causes this error on the workspace FUSE mount:
490+
// unlinkat <workspace_path>: directory not empty. Thus avoiding unnecessary directory creation
491+
// is important.
492+
testEnv := buildTestEnv(config.Env, customEnv)
493+
checkEnvFilters(t, testEnv, envFilters)
494+
502495
if LogConfig {
503496
configBytes, err := json.MarshalIndent(config, "", " ")
504497
require.NoError(t, err)
@@ -530,7 +523,7 @@ func runTest(t *testing.T,
530523
// If the test is being run on DBR, auth is already configured
531524
// by the dbr_runner notebook by reading a token from the notebook context and
532525
// setting DATABRICKS_TOKEN and DATABRICKS_HOST environment variables.
533-
_, _, tmpDir = workspaceTmpDir(t.Context(), t)
526+
tmpDir = workspaceTmpDir(t.Context(), t)
534527

535528
// Run DBR tests on the workspace file system to mimic usage from
536529
// DABs in the workspace.
@@ -630,38 +623,12 @@ func runTest(t *testing.T,
630623
uniqueCacheDir := filepath.Join(t.TempDir(), ".cache")
631624
cmd.Env = append(cmd.Env, "DATABRICKS_CACHE_DIR="+uniqueCacheDir)
632625

633-
for _, key := range utils.SortedKeys(config.Env) {
634-
if hasKey(customEnv, key) {
635-
// We want EnvMatrix to take precedence.
636-
// Skip rather than relying on cmd.Env order, because this might interfere with replacements and substitutions.
637-
continue
638-
}
639-
cmd.Env = addEnvVar(t, cmd.Env, &repls, key, config.Env[key], config.EnvRepl, false)
640-
}
641-
642-
for _, keyvalue := range customEnv {
643-
items := strings.SplitN(keyvalue, "=", 2)
644-
require.Len(t, items, 2)
645-
key := items[0]
646-
value := items[1]
626+
for _, kv := range testEnv {
627+
key, value, _ := strings.Cut(kv, "=")
647628
// Only add replacement by default if value is part of EnvMatrix with more than 1 option and length is 4 or more chars
648629
// (to avoid matching "yes" and "no" values from template input parameters)
649-
cmd.Env = addEnvVar(t, cmd.Env, &repls, key, value, config.EnvRepl, len(config.EnvMatrix[key]) > 1 && len(value) >= 4)
650-
}
651-
652-
for filterInd, filterEnv := range envFilters {
653-
filterEnvKey := strings.Split(filterEnv, "=")[0]
654-
for ind := range cmd.Env {
655-
// Search backwards, because the latest settings is what is actually applicable.
656-
envPair := cmd.Env[len(cmd.Env)-1-ind]
657-
if strings.Split(envPair, "=")[0] == filterEnvKey {
658-
if envPair == filterEnv {
659-
break
660-
} else {
661-
t.Skipf("Skipping because test environment %s does not match ENVFILTER#%d: %s", envPair, filterInd, filterEnv)
662-
}
663-
}
664-
}
630+
defaultRepl := hasKey(customEnv, key) && len(config.EnvMatrix[key]) > 1 && len(value) >= 4
631+
cmd.Env = addEnvVar(t, cmd.Env, &repls, key, value, config.EnvRepl, defaultRepl)
665632
}
666633

667634
absDir, err := filepath.Abs(dir)
@@ -745,10 +712,44 @@ func runTest(t *testing.T,
745712
}
746713
}
747714

715+
// checkEnvFilters skips the test if any env filter doesn't match testEnv.
716+
func checkEnvFilters(t *testing.T, testEnv, envFilters []string) {
717+
envMap := make(map[string]string, len(testEnv))
718+
for _, kv := range testEnv {
719+
key, value, _ := strings.Cut(kv, "=")
720+
envMap[key] = value
721+
}
722+
for i, filter := range envFilters {
723+
key, expected, _ := strings.Cut(filter, "=")
724+
if actual, ok := envMap[key]; ok && actual != expected {
725+
t.Skipf("Skipping because test environment %s=%s does not match ENVFILTER#%d: %s", key, actual, i, filter)
726+
}
727+
}
728+
}
729+
730+
// buildTestEnv builds the test environment from config.Env and customEnv.
731+
// customEnv (from EnvMatrix) takes precedence over config.Env.
732+
func buildTestEnv(configEnv map[string]string, customEnv []string) []string {
733+
env := make([]string, 0, len(configEnv)+len(customEnv))
734+
735+
// Add config.Env first (but skip keys that exist in customEnv)
736+
for _, key := range utils.SortedKeys(configEnv) {
737+
if hasKey(customEnv, key) {
738+
continue
739+
}
740+
env = append(env, key+"="+configEnv[key])
741+
}
742+
743+
// Add customEnv second (takes precedence)
744+
env = append(env, customEnv...)
745+
746+
return env
747+
}
748+
748749
func hasKey(env []string, key string) bool {
749-
for _, keyvalue := range env {
750-
items := strings.SplitN(keyvalue, "=", 2)
751-
if len(items) == 2 && items[0] == key {
750+
for _, kv := range env {
751+
k, _, ok := strings.Cut(kv, "=")
752+
if ok && k == key {
752753
return true
753754
}
754755
}
@@ -1383,6 +1384,22 @@ func BuildYamlfmt(t *testing.T) {
13831384
RunCommand(t, args, "..", []string{})
13841385
}
13851386

1387+
// setupTerraform installs terraform and configures environment variables for tests.
1388+
func setupTerraform(t *testing.T, cwd, buildDir string, repls *testdiff.ReplacementsContext) {
1389+
RunCommand(t, []string{"python3", filepath.Join(cwd, "install_terraform.py"), "--targetdir", buildDir}, ".", []string{})
1390+
1391+
terraformrcPath := filepath.Join(buildDir, ".terraformrc")
1392+
terraformExecPath := filepath.Join(buildDir, "terraform") + exeSuffix
1393+
1394+
t.Setenv("TF_CLI_CONFIG_FILE", terraformrcPath)
1395+
t.Setenv("DATABRICKS_TF_CLI_CONFIG_FILE", terraformrcPath)
1396+
t.Setenv("DATABRICKS_TF_EXEC_PATH", terraformExecPath)
1397+
t.Setenv("TERRAFORM", terraformExecPath)
1398+
1399+
repls.SetPath(terraformrcPath, "[DATABRICKS_TF_CLI_CONFIG_FILE]")
1400+
repls.SetPath(terraformExecPath, "[TERRAFORM]")
1401+
}
1402+
13861403
func loadUserReplacements(t *testing.T, repls *testdiff.ReplacementsContext, tmpDir string) {
13871404
b, err := os.ReadFile(filepath.Join(tmpDir, userReplacementsFilename))
13881405
if os.IsNotExist(err) {

acceptance/bin/gron.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import sys
4+
from pathlib import Path
5+
6+
sys.path.insert(0, str(Path(__file__).parent))
7+
from print_requests import read_json_many
8+
9+
10+
def gron(obj, path="json"):
11+
"""Flatten JSON into greppable assignments.
12+
13+
The path parameter defaults to "json" to match the original gron tool,
14+
which treats the input as a JavaScript variable named "json".
15+
16+
Container declarations are only printed for empty dicts/lists.
17+
This differs from https://github.com/tomnomnom/gron which always prints
18+
container declarations to allow reconstruction via ungron. We don't need
19+
reversibility - we only care about making JSON greppable.
20+
21+
>>> gron({"name": "Tom", "age": 30})
22+
json.name = "Tom";
23+
json.age = 30;
24+
25+
>>> gron({"items": ["apple", "banana"]})
26+
json.items[0] = "apple";
27+
json.items[1] = "banana";
28+
29+
>>> gron({"tasks": [{"libraries": [{"whl": "file.whl"}]}]})
30+
json.tasks[0].libraries[0].whl = "file.whl";
31+
32+
>>> gron({"empty": {}, "items": []})
33+
json.empty = {};
34+
json.items = [];
35+
"""
36+
if isinstance(obj, dict):
37+
if not obj:
38+
print(f"{path} = {{}};")
39+
else:
40+
for key in obj:
41+
gron(obj[key], f"{path}.{key}")
42+
elif isinstance(obj, list):
43+
if not obj:
44+
print(f"{path} = [];")
45+
else:
46+
for i, item in enumerate(obj):
47+
gron(item, f"{path}[{i}]")
48+
else:
49+
print(f"{path} = {json.dumps(obj)};")
50+
51+
52+
def main():
53+
if len(sys.argv) > 1:
54+
with open(sys.argv[1]) as f:
55+
content = f.read()
56+
data = read_json_many(content)
57+
if len(data) == 1:
58+
data = data[0]
59+
else:
60+
content = sys.stdin.read()
61+
data = read_json_many(content)
62+
if len(data) == 1:
63+
data = data[0]
64+
65+
gron(data)
66+
67+
68+
if __name__ == "__main__":
69+
main()

acceptance/bundle/artifacts/test.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ RecordRequests = true
1111
# Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
1212
# Caused by: Failed to query Python interpreter
1313
# Caused by: failed to canonicalize path `/Workspace/abcd/.venv/bin/python3`: Invalid cross-device link (os error 18)
14-
SkipOnDbr = true
14+
# Thus we cannot run this test on DBR.
1515

1616
Ignore = [
1717
'.venv',

acceptance/bundle/artifacts/whl_change_version/output.txt

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,7 @@ dist/my_test_code-0.2.0-py3-none-any.whl
6464

6565
=== Expecting 1 wheel in libraries section in /jobs/reset
6666
>>> jq -s .[] | select(.path=="/api/2.2/jobs/reset") | .body.new_settings.tasks out.requests.txt
67-
[
68-
{
69-
"existing_cluster_id": "0717-aaaaa-bbbbbb",
70-
"libraries": [
71-
{
72-
"whl": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal/my_test_code-0.2.0-py3-none-any.whl"
73-
}
74-
],
75-
"python_wheel_task": {
76-
"entry_point": "run",
77-
"package_name": "my_test_code"
78-
},
79-
"task_key": "TestTask"
80-
}
81-
]
67+
json[0].libraries[0].whl = "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal/my_test_code-0.2.0-py3-none-any.whl";
8268

8369
=== Expecting 1 wheel to be uploaded
8470
>>> jq .path out.requests.txt
@@ -110,21 +96,7 @@ dist/my_test_code-0.2.0-py3-none-any.whl
11096

11197
=== Expecting 1 wheel in libraries section in /jobs/reset
11298
>>> jq -s .[] | select(.path=="/api/2.2/jobs/reset") | .body.new_settings.tasks out.requests.txt
113-
[
114-
{
115-
"existing_cluster_id": "0717-aaaaa-bbbbbb",
116-
"libraries": [
117-
{
118-
"whl": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal/my_test_code-0.1.0-py3-none-any.whl"
119-
}
120-
],
121-
"python_wheel_task": {
122-
"entry_point": "run",
123-
"package_name": "my_test_code"
124-
},
125-
"task_key": "TestTask"
126-
}
127-
]
99+
json[0].libraries[0].whl = "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal/my_test_code-0.1.0-py3-none-any.whl";
128100

129101
=== Expecting 1 wheel to be uploaded
130102
>>> jq .path out.requests.txt

0 commit comments

Comments
 (0)