From 3b44449ef90fc0da63a959ced707fbbe085ca5be Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 11:16:33 +0000 Subject: [PATCH 1/4] Skip stale --select-ids selectors in config-remote-sync instead of failing --- NEXT_CHANGELOG.md | 1 + .../select_basic/output.txt | 37 +++++++++++++------ .../config-remote-sync/select_basic/script | 21 ++++++++--- bundle/configsync/select.go | 16 ++++++-- cmd/bundle/config_remote_sync.go | 2 +- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index da7072e3864..f1c731b78b1 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -10,6 +10,7 @@ ### Bundles +* Fix `bundle config-remote-sync --select-ids` failing the entire sync with "no deployed resource with id" when one selected resource was deleted remotely or its deploy state drifted; such selectors are now skipped so the remaining resources still sync ([#XXXX](https://github.com/databricks/cli/pull/XXXX)). * direct: add basic version of job_runs resource (experimental) ([#5603](https://github.com/databricks/cli/pull/5603)). * Fix permissions added to a job or pipeline by a Python (PyDABs) mutator failing to deploy with "must have exactly one owner"; the deploying identity is now set as owner, matching resources whose permissions are declared in YAML ([#5821](https://github.com/databricks/cli/pull/5821)). * Remove duplicate enum values for jsonschema.json ([#5839](https://github.com/databricks/cli/pull/5839)). diff --git a/acceptance/bundle/config-remote-sync/select_basic/output.txt b/acceptance/bundle/config-remote-sync/select_basic/output.txt index 14987921858..198ea857f5f 100644 --- a/acceptance/bundle/config-remote-sync/select_basic/output.txt +++ b/acceptance/bundle/config-remote-sync/select_basic/output.txt @@ -37,21 +37,36 @@ Resource: resources.jobs.job_two -=== An unknown resource id is rejected ->>> [CLI] bundle config-remote-sync --select-ids jobs:no-such-id-123 -Error: no deployed jobs resource with id no-such-id-123 +=== A stale selector is skipped, not fatal: job_two still syncs alongside it +Detected changes in 1 resource(s): -Exit code: 1 +Resource: resources.jobs.job_two + max_concurrent_runs: replace -=== A selector without a type is rejected ->>> [CLI] bundle config-remote-sync --select-ids no-such-id-123 -Error: invalid --select-ids value "no-such-id-123", expected : (e.g. jobs:[NUMID]) -Exit code: 1 -=== An id that exists under a different type is rejected (no cross-type collision) ->>> [CLI] bundle config-remote-sync --select-ids pipelines:[JOB_ONE_ID] -Error: no deployed pipelines resource with id [JOB_ONE_ID] +>>> diff.py databricks.yml.backup databricks.yml +--- databricks.yml.backup ++++ databricks.yml +@@ -16,5 +16,5 @@ + + job_two: +- max_concurrent_runs: 2 ++ max_concurrent_runs: 10 + tasks: + - task_key: main + +=== An unknown resource id alone is a no-op (skipped), not an error +No changes detected. + + +=== An id that exists under a different type matches nothing and is skipped too +No changes detected. + + +=== A selector without a type is still rejected +>>> [CLI] bundle config-remote-sync --select-ids no-such-id-123 +Error: invalid --select-ids value "no-such-id-123", expected : (e.g. jobs:[NUMID]) Exit code: 1 diff --git a/acceptance/bundle/config-remote-sync/select_basic/script b/acceptance/bundle/config-remote-sync/select_basic/script index a33b9f9e57c..5cc915f2db2 100644 --- a/acceptance/bundle/config-remote-sync/select_basic/script +++ b/acceptance/bundle/config-remote-sync/select_basic/script @@ -38,11 +38,20 @@ title "Unfiltered sync still detects the job_two drift (no lost updates)" echo $CLI bundle config-remote-sync -title "An unknown resource id is rejected" -errcode trace $CLI bundle config-remote-sync --select-ids jobs:no-such-id-123 +title "A stale selector is skipped, not fatal: job_two still syncs alongside it" +echo +cp databricks.yml databricks.yml.backup +$CLI bundle config-remote-sync --select-ids "jobs:no-such-id-123,jobs:$job_two_id" --save +trace diff.py databricks.yml.backup databricks.yml +rm databricks.yml.backup -title "A selector without a type is rejected" -errcode trace $CLI bundle config-remote-sync --select-ids no-such-id-123 +title "An unknown resource id alone is a no-op (skipped), not an error" +echo +$CLI bundle config-remote-sync --select-ids jobs:no-such-id-123 + +title "An id that exists under a different type matches nothing and is skipped too" +echo +$CLI bundle config-remote-sync --select-ids "pipelines:$job_one_id" -title "An id that exists under a different type is rejected (no cross-type collision)" -errcode trace $CLI bundle config-remote-sync --select-ids "pipelines:$job_one_id" +title "A selector without a type is still rejected" +errcode trace $CLI bundle config-remote-sync --select-ids no-such-id-123 diff --git a/bundle/configsync/select.go b/bundle/configsync/select.go index fcd8549d0be..5b00c1a70a1 100644 --- a/bundle/configsync/select.go +++ b/bundle/configsync/select.go @@ -1,11 +1,13 @@ package configsync import ( + "context" "fmt" "slices" "strings" "github.com/databricks/cli/bundle/direct/dstate" + "github.com/databricks/cli/libs/log" ) // ResolveResourceSelectors maps ":" selectors to their plan keys @@ -21,11 +23,16 @@ import ( // is also why selection is independent from `bundle deploy --select`, which // matches "type.name" keys. // -// A selector that matches no deployed resource is an error: only deployed -// resources have an id, so a selector matching nothing is a caller mistake. +// A selector that matches no deployed resource is skipped, not an error: the +// workspace UI batches every edited resource into one sync, and a resource that +// was deleted remotely (or whose deploy state has drifted) has no remote change +// to pull back into config. Failing the whole run on one stale selector would +// drop the valid resources' edits too, which is why such workspaces saw every +// sync fail. A malformed selector (missing ":" shape) is still an +// error, because that is a caller mistake rather than drift. // Duplicate selectors are deduplicated; the returned keys preserve the order in // which their selectors first appear. -func ResolveResourceSelectors(state *dstate.DeploymentState, selectors []string) ([]string, error) { +func ResolveResourceSelectors(ctx context.Context, state *dstate.DeploymentState, selectors []string) ([]string, error) { // Index deployed resources by ":". State keys have the form // "resources.."; indexing by the component means a // selector can only ever match a resource of that exact type, never an id @@ -55,7 +62,8 @@ func ResolveResourceSelectors(state *dstate.DeploymentState, selectors []string) } key, ok := byTypeID[selector] if !ok { - return nil, fmt.Errorf("no deployed %s resource with id %s", resourceType, id) + log.Debugf(ctx, "config-remote-sync: skipping selector %s:%s, no deployed resource with that id", resourceType, id) + continue } if !slices.Contains(keys, key) { keys = append(keys, key) diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 93d9bbb14c3..e5231fef58a 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -104,7 +104,7 @@ Examples: // Filter after planning, never before: the plan must cover every // resource so ${resources.*} references resolve; only the emitted // changes are restricted to the selected resources. - selected, err := configsync.ResolveResourceSelectors(&deployBundle.StateDB, selectIDs) + selected, err := configsync.ResolveResourceSelectors(ctx, &deployBundle.StateDB, selectIDs) if err != nil { return err } From 1a6784b8ae1600a188a7ec887296d4b6e4b7c0e5 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 11:47:43 +0000 Subject: [PATCH 2/4] config-remote-sync: log skipped selector directly instead of rebuilding it --- bundle/configsync/select.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/configsync/select.go b/bundle/configsync/select.go index 5b00c1a70a1..0960a004b9d 100644 --- a/bundle/configsync/select.go +++ b/bundle/configsync/select.go @@ -62,7 +62,7 @@ func ResolveResourceSelectors(ctx context.Context, state *dstate.DeploymentState } key, ok := byTypeID[selector] if !ok { - log.Debugf(ctx, "config-remote-sync: skipping selector %s:%s, no deployed resource with that id", resourceType, id) + log.Debugf(ctx, "config-remote-sync: skipping selector %q, no deployed resource with that id", selector) continue } if !slices.Contains(keys, key) { From f6d2d1dc2ee3a4009c84ee312b8ad57b59b5e1e8 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 12:47:57 +0000 Subject: [PATCH 3/4] Drop changelog entry (config-remote-sync is a hidden command) --- NEXT_CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index f1c731b78b1..da7072e3864 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -10,7 +10,6 @@ ### Bundles -* Fix `bundle config-remote-sync --select-ids` failing the entire sync with "no deployed resource with id" when one selected resource was deleted remotely or its deploy state drifted; such selectors are now skipped so the remaining resources still sync ([#XXXX](https://github.com/databricks/cli/pull/XXXX)). * direct: add basic version of job_runs resource (experimental) ([#5603](https://github.com/databricks/cli/pull/5603)). * Fix permissions added to a job or pipeline by a Python (PyDABs) mutator failing to deploy with "must have exactly one owner"; the deploying identity is now set as owner, matching resources whose permissions are declared in YAML ([#5821](https://github.com/databricks/cli/pull/5821)). * Remove duplicate enum values for jsonschema.json ([#5839](https://github.com/databricks/cli/pull/5839)). From 968cd77f5af0ce23019f9a1dc03bcc0af8ca5ab0 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Tue, 21 Jul 2026 14:02:45 +0000 Subject: [PATCH 4/4] config-remote-sync: only skip stale --select-ids when another selector matched (avoid false success) --- .../select_basic/output.txt | 14 +++++--- .../config-remote-sync/select_basic/script | 8 ++--- bundle/configsync/select.go | 32 ++++++++++++++----- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/acceptance/bundle/config-remote-sync/select_basic/output.txt b/acceptance/bundle/config-remote-sync/select_basic/output.txt index 198ea857f5f..2909fc4e403 100644 --- a/acceptance/bundle/config-remote-sync/select_basic/output.txt +++ b/acceptance/bundle/config-remote-sync/select_basic/output.txt @@ -56,13 +56,19 @@ Resource: resources.jobs.job_two tasks: - task_key: main -=== An unknown resource id alone is a no-op (skipped), not an error -No changes detected. +=== An unknown resource id alone is rejected (no match at all must not silently succeed) +>>> [CLI] bundle config-remote-sync --select-ids jobs:no-such-id-123 +Error: no deployed jobs resource with id no-such-id-123 -=== An id that exists under a different type matches nothing and is skipped too -No changes detected. +Exit code: 1 +=== An id that exists under a different type matches nothing and is rejected too + +>>> [CLI] bundle config-remote-sync --select-ids pipelines:[JOB_ONE_ID] +Error: no deployed pipelines resource with id [JOB_ONE_ID] + +Exit code: 1 === A selector without a type is still rejected >>> [CLI] bundle config-remote-sync --select-ids no-such-id-123 diff --git a/acceptance/bundle/config-remote-sync/select_basic/script b/acceptance/bundle/config-remote-sync/select_basic/script index 5cc915f2db2..a85bb0b46a6 100644 --- a/acceptance/bundle/config-remote-sync/select_basic/script +++ b/acceptance/bundle/config-remote-sync/select_basic/script @@ -45,13 +45,13 @@ $CLI bundle config-remote-sync --select-ids "jobs:no-such-id-123,jobs:$job_two_i trace diff.py databricks.yml.backup databricks.yml rm databricks.yml.backup -title "An unknown resource id alone is a no-op (skipped), not an error" +title "An unknown resource id alone is rejected (no match at all must not silently succeed)" echo -$CLI bundle config-remote-sync --select-ids jobs:no-such-id-123 +errcode trace $CLI bundle config-remote-sync --select-ids jobs:no-such-id-123 -title "An id that exists under a different type matches nothing and is skipped too" +title "An id that exists under a different type matches nothing and is rejected too" echo -$CLI bundle config-remote-sync --select-ids "pipelines:$job_one_id" +errcode trace $CLI bundle config-remote-sync --select-ids "pipelines:$job_one_id" title "A selector without a type is still rejected" errcode trace $CLI bundle config-remote-sync --select-ids no-such-id-123 diff --git a/bundle/configsync/select.go b/bundle/configsync/select.go index 0960a004b9d..443c94423ae 100644 --- a/bundle/configsync/select.go +++ b/bundle/configsync/select.go @@ -23,13 +23,15 @@ import ( // is also why selection is independent from `bundle deploy --select`, which // matches "type.name" keys. // -// A selector that matches no deployed resource is skipped, not an error: the -// workspace UI batches every edited resource into one sync, and a resource that -// was deleted remotely (or whose deploy state has drifted) has no remote change -// to pull back into config. Failing the whole run on one stale selector would -// drop the valid resources' edits too, which is why such workspaces saw every -// sync fail. A malformed selector (missing ":" shape) is still an -// error, because that is a caller mistake rather than drift. +// A selector that matches no deployed resource is skipped rather than failing +// the run — but only when at least one other selector did match. The workspace +// UI batches every edited resource into one sync, so a single stale selector +// (a resource deleted remotely, or whose deploy state has drifted) must not +// drop the valid resources' edits. When NO selector matches, the error is +// returned instead: silently reporting "no changes" would let the UI show a +// success while nothing synced, hiding a real state problem. A malformed +// selector (missing ":" shape) is always an error, because that is a +// caller mistake rather than drift. // Duplicate selectors are deduplicated; the returned keys preserve the order in // which their selectors first appear. func ResolveResourceSelectors(ctx context.Context, state *dstate.DeploymentState, selectors []string) ([]string, error) { @@ -55,6 +57,7 @@ func ResolveResourceSelectors(ctx context.Context, state *dstate.DeploymentState } keys := make([]string, 0, len(selectors)) + var missing []string for _, selector := range selectors { resourceType, id, ok := strings.Cut(selector, ":") if !ok || resourceType == "" || id == "" { @@ -62,13 +65,26 @@ func ResolveResourceSelectors(ctx context.Context, state *dstate.DeploymentState } key, ok := byTypeID[selector] if !ok { - log.Debugf(ctx, "config-remote-sync: skipping selector %q, no deployed resource with that id", selector) + missing = append(missing, selector) continue } if !slices.Contains(keys, key) { keys = append(keys, key) } } + + // No selector matched a deployed resource: fail loudly instead of syncing + // nothing, so the caller does not report a spurious success. + if len(keys) == 0 { + resourceType, id, _ := strings.Cut(missing[0], ":") + return nil, fmt.Errorf("no deployed %s resource with id %s", resourceType, id) + } + + // Some selectors matched: skip the stale ones so the matched resources still + // sync (the UI batches several resources into one run). + for _, selector := range missing { + log.Debugf(ctx, "config-remote-sync: skipping selector %q, no deployed resource with that id", selector) + } return keys, nil }