Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6674d4b
structpath: add NewPattern with AnyKey/AnyIndex wildcards
denik Oct 15, 2026
6079e94
bundle: delete libs/dyn; configuration is typed structs with structva…
denik Oct 7, 2026
21fa868
structvar: map YAML locations in the YAML source; drop mappedSource
denik Oct 7, 2026
dc8c9b1
Use NewStringKeysSlice for single key paths
denik Oct 7, 2026
a703bfc
Build literal diagnostic paths with NewStringKeysSlice
denik Oct 7, 2026
6b66d26
Restack fixes: structvar grammar for structpath's PureReferenceToPath…
denik Oct 7, 2026
9f08470
Restore import block after restack
denik Oct 7, 2026
18b64b8
Use structpath.NewPath for key/index paths
denik Oct 7, 2026
0671d48
Build the target preset path in one NewPath call
denik Oct 8, 2026
abb002f
GetLocationsOf on the location tree
denik Oct 8, 2026
fbe25ac
Minimal import edits; drop unused diag import
denik Oct 8, 2026
6476693
structvar: test the YAML decoder with the yamlloader fixtures
denik Oct 9, 2026
b189c77
structvar: add the YAML test helpers
denik Oct 9, 2026
1ed2dd9
Port the job environment_variables files pattern (#6985) to structpath
denik Oct 9, 2026
96e6767
Fix codex review findings
denik Oct 9, 2026
3ca9616
Port the job environments project_environment pattern (#6983) to stru…
denik Oct 12, 2026
651eecc
Fix regressions recorded by the config edge-case tests
denik Oct 14, 2026
3ecf7d6
Drop the embedded JobSettings literal flagged by embedlit
denik Oct 15, 2026
86b1c45
Keep main's path-node location lookups; build app permission paths fr…
denik Oct 15, 2026
1bcb66d
config: Nearest/Exact location lookups by path node; build paths from…
denik Oct 15, 2026
e4cdd6c
structvar: don't allocate per struct field when traversing views
denik Oct 16, 2026
586b4f7
structvar: traverse views without closures, map lookups or rebuilding…
denik Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .agents/rules/style-guide-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ return fieldPaths

### Encoding

**RULE: When mutating an API response by round-tripping JSON, decode with `json.Decoder` + `UseNumber()`.** The naive `json.Marshal` → `map[string]any` → `Marshal` path corrupts any int64 larger than 2^53 (it degrades to a float64 mantissa — e.g. a real `spark_context_id`) and alphabetizes object keys. `libs/dyn/jsonloader` preserves key order but also lacks `UseNumber`, so it shares the int64 hazard.
**RULE: When mutating an API response by round-tripping JSON, decode with `json.Decoder` + `UseNumber()`.** The naive `json.Marshal` → `map[string]any` → `Marshal` path corrupts any int64 larger than 2^53 (it degrades to a float64 mantissa — e.g. a real `spark_context_id`) and alphabetizes object keys.

**RULE: Be careful with `encoding/csv` `Writer.UseCRLF = true`.** It rewrites both record terminators AND embedded newlines inside quoted fields to `\r\n`, so tests for quoted multiline fields must expect `\r\n`, not just the line endings between rows.

Expand Down Expand Up @@ -191,7 +191,7 @@ func NewService(deps ServiceDeps) *Service { ... }

### Configuration patterns

- Bundle config uses `dyn.Value` for dynamic typing
- Bundle config is typed structs; `libs/structs/structvar` carries `${...}` references and source locations
- Config loading supports includes, variable interpolation, and target overrides
- Schema generation is automated from Go struct tags

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
acceptance/bundle/python/ \
go.mod \
go.sum \
libs/dyn/dynvar/testdata/reference_vectors.json \
libs/structs/structvar/testdata/reference_vectors.json \
python/ \
tools/task/ \
)
Expand Down
1 change: 1 addition & 0 deletions .nextchanges/bundles/6954.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Speed up bundle commands on large bundles: configuration is kept as typed structs instead of being converted to and from a dynamic tree (`bundle validate` and `bundle plan` with 1000 jobs are about 10x faster and use about 3.5x less memory). ([#6954](https://github.com/databricks/cli/pull/6954))
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This is the Databricks CLI, a command-line interface for interacting with Databr

**RULE: Keep each PR focused on one change.** If you notice an unrelated cleanup, bug fix, or refactor while making your primary change, leave it alone or put it in a separate PR. Reviewers consistently ask to split mixed PRs, especially when a dependency bump or schema diff rides along with a feature change.

**RULE: Before adding a new helper, search the codebase for an existing one.** Common homes: `libs/` (shared utilities), `libs/databrickscfg/` (config), `libs/git/`, `libs/filer/`, `libs/cmdio/` (CLI I/O, spinners, prompts), `libs/env/` (env vars), `libs/testserver/`, `libs/structpath/` and `libs/dyn/` (path / dynamic values), `acceptance/bin/` (acceptance test helpers), `internal/mocks/` (generated mocks). A function that duplicates an existing name and signature in the same package is a compile error waiting to happen; grep before you name.
**RULE: Before adding a new helper, search the codebase for an existing one.** Common homes: `libs/` (shared utilities), `libs/databrickscfg/` (config), `libs/git/`, `libs/filer/`, `libs/cmdio/` (CLI I/O, spinners, prompts), `libs/env/` (env vars), `libs/testserver/`, `libs/structs/structpath/` and `libs/structs/structvar/` (paths / typed config values), `acceptance/bin/` (acceptance test helpers), `internal/mocks/` (generated mocks). A function that duplicates an existing name and signature in the same package is a compile error waiting to happen; grep before you name.

# Development Commands

Expand Down Expand Up @@ -73,7 +73,7 @@ GIT_EDITOR=true GIT_SEQUENCE_EDITOR=true VISUAL=true GIT_PAGER=cat git rebase or
- `bundle/phases/` - High-level deployment phases

**libs/** - Shared libraries and utilities
- `libs/dyn/` - Dynamic configuration value manipulation
- `libs/structs/structvar/` - Typed configuration values with references and locations
- `libs/filer/` - File system abstraction (local, DBFS, workspace)
- `libs/auth/` - Databricks authentication handling
- `libs/sync/` - File synchronization between local and remote
Expand Down
32 changes: 11 additions & 21 deletions acceptance/invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import (
"testing"

"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/dyn/yamlloader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
)

const invariantConfigsDir = "bundle/invariant/configs"
Expand Down Expand Up @@ -88,31 +87,22 @@ func scanInvariantConfigs(t *testing.T) (present, withPermissions, withGrants ma
contents, err := os.ReadFile(path)
require.NoError(t, err)

v, err := yamlloader.LoadYAML(path, strings.NewReader(string(contents)))
require.NoError(t, err, "failed to parse %s", path)

resources := v.Get("resources")
if resources.Kind() != dyn.KindMap {
// Some configs (e.g. PyDABs) declare resources outside of YAML.
continue
var doc struct {
Resources map[string]any `yaml:"resources"`
}
require.NoError(t, yaml.Unmarshal(contents, &doc), "failed to parse %s", path)

for _, group := range resources.MustMap().Pairs() {
groupName := group.Key.MustString()
// Some configs (e.g. PyDABs) declare resources outside of YAML.
for groupName, group := range doc.Resources {
present[groupName] = true

if group.Value.Kind() != dyn.KindMap {
continue
}
for _, resource := range group.Value.MustMap().Pairs() {
cfg := resource.Value
if cfg.Kind() != dyn.KindMap {
continue
}
if cfg.Get("permissions").Kind() != dyn.KindInvalid {
resources, _ := group.(map[string]any)
for _, resource := range resources {
cfg, _ := resource.(map[string]any)
if _, ok := cfg["permissions"]; ok {
withPermissions[groupName] = true
}
if cfg.Get("grants").Kind() != dyn.KindInvalid {
if _, ok := cfg["grants"]; ok {
withGrants[groupName] = true
}
}
Expand Down
46 changes: 16 additions & 30 deletions bundle/apps/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structvar"
"github.com/databricks/databricks-sdk-go/service/apps"
)

Expand All @@ -28,7 +28,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
Severity: diag.Error,
Summary: "Missing app source code path or git source",
Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field", key),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key)),
Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "apps", key)),
})
continue
}
Expand All @@ -38,7 +38,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
Severity: diag.Error,
Summary: "Both source_code_path and git_source fields are set",
Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field, not both", key),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key)),
Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "apps", key)),
})
continue
}
Expand All @@ -48,7 +48,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
Severity: diag.Error,
Summary: "Duplicate app source code path",
Detail: fmt.Sprintf("app resource '%s' has the same source code path as app resource '%s', this will lead to the app configuration being overridden by each other", key, usedSourceCodePaths[app.SourceCodePath]),
Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key, "source_code_path")),
Locations: b.Config.NearestLocations(structpath.NewPath(nil, "resources", "apps", key, "source_code_path")),
})
}
usedSourceCodePaths[app.SourceCodePath] = key
Expand Down Expand Up @@ -92,34 +92,20 @@ type appResourceReference struct {
permission string
}

// hasPermissions checks if a bundle resource at the given dyn path has a non-empty permissions list.
func hasPermissions(b *bundle.Bundle, resourcePath string) bool {
pv, err := dyn.Get(b.Config.Value(), resourcePath+".permissions")
if err != nil {
return false
// hasPermissions checks if a bundle resource has a non-empty permissions list.
func hasPermissions(resource structvar.View) bool {
for range resource.Get("permissions").Sequence() {
return true
}
s, ok := pv.AsSequence()
return ok && len(s) > 0
return false
}

// hasAppSPInPermissions checks if any permission entry for the given resource
// references the app's service principal via variable interpolation.
func hasAppSPInPermissions(b *bundle.Bundle, resourcePath, appKey string) bool {
func hasAppSPInPermissions(resource structvar.View, appKey string) bool {
appSPRef := fmt.Sprintf("${resources.apps.%s.service_principal_client_id}", appKey)
pv, err := dyn.Get(b.Config.Value(), resourcePath+".permissions")
if err != nil {
return false
}
s, ok := pv.AsSequence()
if !ok {
return false
}
for _, entry := range s {
spn, err := dyn.Get(entry, "service_principal_name")
if err != nil {
continue
}
if str, ok := spn.AsString(); ok && str == appSPRef {
for _, entry := range resource.Get("permissions").Sequence() {
if str, ok := entry.Get("service_principal_name").AsString(); ok && str == appSPRef {
return true
}
}
Expand All @@ -146,12 +132,12 @@ func warnForAppResourcePermissions(b *bundle.Bundle, appKey string, app *resourc
}
refType, resourceKey := matches[1], matches[2]

resourcePath := fmt.Sprintf("resources.%s.%s", refType, resourceKey)
if !hasPermissions(b, resourcePath) {
resource := b.Config.View().Get("resources").Get(refType).Get(resourceKey)
if !hasPermissions(resource) {
continue
}

if hasAppSPInPermissions(b, resourcePath, appKey) {
if hasAppSPInPermissions(resource, appKey) {
continue
}

Expand All @@ -174,7 +160,7 @@ func warnForAppResourcePermissions(b *bundle.Bundle, appKey string, app *resourc
appKey,
),
Paths: []*structpath.PathNode{appPath},
Locations: b.Config.GetLocationsOf(appPath),
Locations: b.Config.NearestLocations(appPath),
})
}

Expand Down
145 changes: 71 additions & 74 deletions bundle/artifacts/expand_globs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import (
"path/filepath"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/patchwheel"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structvar"
)

func createGlobError(v dyn.Value, p dyn.Path, message string) diag.Diagnostic {
func createGlobError(v structvar.View, p *structpath.PathNode, message string) diag.Diagnostic {
// The pattern contained in v is an absolute path.
// Make it relative to the value's location to make it more readable.
source := v.MustString()
source, _ := v.AsString()
if l := v.Location(); l.File != "" {
rel, err := filepath.Rel(filepath.Dir(l.File), source)
if err == nil {
Expand All @@ -27,7 +28,7 @@ func createGlobError(v dyn.Value, p dyn.Path, message string) diag.Diagnostic {
Severity: diag.Error,
Summary: fmt.Sprintf("%s: %s", source, message),
Locations: []diag.Location{v.Location()},
Paths: dyn.ToStructPaths(p),
Paths: []*structpath.PathNode{p},
}
}

Expand All @@ -42,82 +43,78 @@ func (e expandGlobs) Name() string {
func (e expandGlobs) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// Base path for this mutator.
// This path is set with the list of expanded globs when done.
base := dyn.NewPath(
dyn.Key("artifacts"),
dyn.Key(e.name),
dyn.Key("files"),
)
base := structpath.NewPath(nil, "artifacts", e.name, "files")

// Pattern to match the source key in the files sequence.
pattern := dyn.NewPatternFromPath(base).Append(
dyn.AnyIndex(),
dyn.Key("source"),
)
pattern := structpath.NewPattern(nil, "artifacts", e.name, "files", structpath.AnyIndex, "source")

artifact := b.Config.Artifacts[e.name]
if artifact == nil {
return nil
}

var diags diag.Diagnostics
err := b.Config.Mutate(func(rootv dyn.Value) (dyn.Value, error) {
var output []dyn.Value
_, err := dyn.MapByPattern(rootv, pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
if v.Kind() != dyn.KindString {
return v, nil
}

source := v.MustString()

// Expand any glob reference in files source path
matches, err := filepath.Glob(source)
if err != nil {
diags = diags.Append(createGlobError(v, p, err.Error()))

// Drop this value from the list; this does not matter since we've raised an error anyway
return v, nil
}

// Note, we're applying this for all artifact types, not just "whl".
// Rationale:
// 1. type is optional
// 2. if you have wheels in other artifact type, maybe you still want the filter logic? impossible to say.
matches = patchwheel.FilterLatestWheels(ctx, matches)

if len(matches) == 1 && matches[0] == source {
// No glob expansion was performed.
// Keep node unchanged. We need to ensure that "patched" field remains and not wiped out by code below.
parent, err := dyn.GetByPath(rootv, p[0:len(p)-1])
if err != nil {
log.Debugf(ctx, "Failed to get parent of %s", p.String())
} else {
output = append(output, parent)
}
return v, nil
}

if len(matches) == 0 {
diags = diags.Append(createGlobError(v, p, "no matching files"))

// Drop this value from the list; this does not matter since we've raised an error anyway
return v, nil
}

for _, match := range matches {
output = append(output, dyn.V(
map[string]dyn.Value{
"source": dyn.NewValue(match, v.Locations()),
},
))
}

return v, nil
})

if err != nil || diags.HasError() {
return rootv, err
var output []config.ArtifactFile
var sources [][]int
var locations [][]diag.Location
err := structvar.ForEach(b.Config.View(), pattern, func(np *structpath.PathNode, v structvar.View) error {
if v.Kind() != structvar.KindString {
return nil
}

index, _ := np.Parent().Index()
source, _ := v.AsString()

// Expand any glob reference in files source path
matches, err := filepath.Glob(source)
if err != nil {
diags = diags.Append(createGlobError(v, np, err.Error()))

// Drop this value from the list; this does not matter since we've raised an error anyway
return nil
}

// Note, we're applying this for all artifact types, not just "whl".
// Rationale:
// 1. type is optional
// 2. if you have wheels in other artifact type, maybe you still want the filter logic? impossible to say.
matches = patchwheel.FilterLatestWheels(ctx, matches)

if len(matches) == 1 && matches[0] == source {
// No glob expansion was performed.
// Keep node unchanged. We need to ensure that "patched" field remains and not wiped out by code below.
output = append(output, artifact.Files[index])
sources = append(sources, []int{index})
locations = append(locations, nil)
return nil
}

// Set the expanded globs back into the configuration.
return dyn.SetByPath(rootv, base, dyn.V(output))
if len(matches) == 0 {
diags = diags.Append(createGlobError(v, np, "no matching files"))

// Drop this value from the list; this does not matter since we've raised an error anyway
return nil
}

for _, match := range matches {
output = append(output, config.ArtifactFile{Source: match})
sources = append(sources, []int{index})
locations = append(locations, v.Locations())
}

return nil
})
if err != nil {
diags = diags.Extend(diag.FromErr(err))
if err != nil || diags.HasError() {
return diags.Extend(diag.FromErr(err))
}

// Set the expanded globs back into the configuration.
artifact.Files = output
b.Config.UpdateSequence(base, sources)
for i, locs := range locations {
if locs != nil {
b.Config.SetLocations(structpath.NewPath(base, i, "source"), locs)
}
}

return diags
Expand Down
Loading
Loading