From 2cb06d8c9e2915fbe441fb01e04b3f6670abcce4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 12:46:07 +0000 Subject: [PATCH 01/12] direct: key job task depends_on slices by task_key Register tasks[*].depends_on and tasks[*].for_each_task.task.depends_on as keyed slices so task dependencies are diffed by task_key rather than by position. Also fix pathToPattern in structdiff to treat key-value path nodes as [*] wildcards, enabling nested keyed-slice patterns to match correctly. Co-authored-by: Isaac --- bundle/direct/dresources/job.go | 14 ++++++++++---- libs/structs/structdiff/diff.go | 15 ++++----------- libs/structs/structdiff/diff_test.go | 10 ++++++++++ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/bundle/direct/dresources/job.go b/bundle/direct/dresources/job.go index f10813b2fc..9477bf5251 100644 --- a/bundle/direct/dresources/job.go +++ b/bundle/direct/dresources/job.go @@ -71,12 +71,18 @@ func getEnvironmentKey(x jobs.JobEnvironment) (string, string) { return "environment_key", x.EnvironmentKey } +func getDependsOnTaskKey(x jobs.TaskDependency) (string, string) { + return "task_key", x.TaskKey +} + func (*ResourceJob) KeyedSlices() map[string]any { return map[string]any{ - "tasks": getTaskKey, - "parameters": getParameterName, - "job_clusters": getJobClusterKey, - "environments": getEnvironmentKey, + "tasks": getTaskKey, + "parameters": getParameterName, + "job_clusters": getJobClusterKey, + "environments": getEnvironmentKey, + "tasks[*].depends_on": getDependsOnTaskKey, + "tasks[*].for_each_task.task.depends_on": getDependsOnTaskKey, } } diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index 61c909dfd1..a852d347e4 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -308,7 +308,7 @@ func (ctx *diffContext) findKeyFunc(path *structpath.PathNode) KeyFunc { } // pathToPattern converts a PathNode to a pattern string for matching. -// Slice indices are converted to [*] wildcard. +// Slice indices and key-value pairs are converted to [*] wildcard. func pathToPattern(path *structpath.PathNode) string { if path == nil { return "" @@ -318,17 +318,10 @@ func pathToPattern(path *structpath.PathNode) string { var result strings.Builder for i, node := range components { - if idx, ok := node.Index(); ok { - // Convert numeric index to wildcard - _ = idx + if _, ok := node.Index(); ok { + result.WriteString("[*]") + } else if _, _, ok := node.KeyValue(); ok { result.WriteString("[*]") - } else if key, value, ok := node.KeyValue(); ok { - // Key-value syntax - result.WriteString("[") - result.WriteString(key) - result.WriteString("=") - result.WriteString(structpath.EncodeMapKey(value)) - result.WriteString("]") } else if key, ok := node.StringKey(); ok { if i != 0 { result.WriteString(".") diff --git a/libs/structs/structdiff/diff_test.go b/libs/structs/structdiff/diff_test.go index 4b57f87c88..72012b11a6 100644 --- a/libs/structs/structdiff/diff_test.go +++ b/libs/structs/structdiff/diff_test.go @@ -558,10 +558,16 @@ func TestGetStructDiffEmbedTagWithKeyFunc(t *testing.T) { } } +type Dep struct { + TaskKey string `json:"task_key,omitempty"` + Outcome string `json:"outcome,omitempty"` +} + type Task struct { TaskKey string `json:"task_key,omitempty"` Description string `json:"description,omitempty"` Timeout int `json:"timeout,omitempty"` + DependsOn []Dep `json:"depends_on,omitempty"` } type Job struct { @@ -573,6 +579,10 @@ func taskKeyFunc(task Task) (string, string) { return "task_key", task.TaskKey } +func depKeyFunc(dep Dep) (string, string) { + return "task_key", dep.TaskKey +} + func TestGetStructDiffSliceKeys(t *testing.T) { sliceKeys := map[string]KeyFunc{ "tasks": taskKeyFunc, From 168770714142f225108ae23bcf6535ce17617db4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 12:50:04 +0000 Subject: [PATCH 02/12] direct: add unit tests for nested depends_on keyed slices Test that reordered depends_on arrays within job tasks produce no phantom diffs when using the tasks[*].depends_on key function. Task: 001.md Co-authored-by: Isaac --- libs/structs/structdiff/diff_test.go | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/libs/structs/structdiff/diff_test.go b/libs/structs/structdiff/diff_test.go index 72012b11a6..567287fd8c 100644 --- a/libs/structs/structdiff/diff_test.go +++ b/libs/structs/structdiff/diff_test.go @@ -652,6 +652,64 @@ func TestGetStructDiffSliceKeys(t *testing.T) { } } +func TestGetStructDiffNestedDependsOn(t *testing.T) { + sliceKeys := map[string]KeyFunc{ + "tasks": taskKeyFunc, + "tasks[*].depends_on": depKeyFunc, + } + + tests := []struct { + name string + a, b Job + want []ResolvedChange + }{ + { + name: "depends_on reordered no diff", + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "b"}, {TaskKey: "a"}}}}}, + want: nil, + }, + { + name: "depends_on field change", + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a", Outcome: "success"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a", Outcome: "failed"}}}}}, + want: []ResolvedChange{{Field: "tasks[task_key='c'].depends_on[task_key='a'].outcome", Old: "success", New: "failed"}}, + }, + { + name: "depends_on element added", + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, + want: []ResolvedChange{{Field: "tasks[task_key='c'].depends_on[task_key='b']", Old: nil, New: Dep{TaskKey: "b"}}}, + }, + { + name: "depends_on element removed", + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}}}}}, + want: []ResolvedChange{{Field: "tasks[task_key='c'].depends_on[task_key='b']", Old: Dep{TaskKey: "b"}, New: nil}}, + }, + { + name: "tasks and depends_on both reordered no diff", + a: Job{Tasks: []Task{ + {TaskKey: "x", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}, + {TaskKey: "y", DependsOn: []Dep{{TaskKey: "c"}}}, + }}, + b: Job{Tasks: []Task{ + {TaskKey: "y", DependsOn: []Dep{{TaskKey: "c"}}}, + {TaskKey: "x", DependsOn: []Dep{{TaskKey: "b"}, {TaskKey: "a"}}}, + }}, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetStructDiff(tt.a, tt.b, sliceKeys) + assert.NoError(t, err) + assert.Equal(t, tt.want, resolveChanges(got)) + }) + } +} + type Nested struct { Items []Item `json:"items,omitempty"` } From 6626f4b3d14471bdb4df23c375651bf8c95cb732 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:00:59 +0000 Subject: [PATCH 03/12] testserver: sort depends_on by task_key; add reorder acceptance test Simulates real API behavior where depends_on entries are returned in a different order than submitted. Adds an acceptance test that verifies bundle plan shows 0 changes after deploy despite the reordering. --- .../jobs/depends-on-reorder/databricks.yml | 21 +++++++++++++++++++ .../resources/jobs/depends-on-reorder/main.py | 1 + .../jobs/depends-on-reorder/out.test.toml | 5 +++++ .../resources/jobs/depends-on-reorder/script | 2 ++ .../jobs/depends-on-reorder/test.toml | 2 ++ libs/testserver/jobs.go | 6 ++++++ 6 files changed, 37 insertions(+) create mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml create mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/main.py create mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml create mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/script create mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/test.toml diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml b/acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml new file mode 100644 index 0000000000..3fdfae66b9 --- /dev/null +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml @@ -0,0 +1,21 @@ +bundle: + name: test-bundle + +resources: + jobs: + foo: + tasks: + - task_key: main + notebook_task: + notebook_path: main.py + - task_key: process + depends_on: + - task_key: main + notebook_task: + notebook_path: main.py + - task_key: finalize + depends_on: + - task_key: process + - task_key: main + notebook_task: + notebook_path: main.py diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/main.py b/acceptance/bundle/resources/jobs/depends-on-reorder/main.py new file mode 100644 index 0000000000..1645e04b1d --- /dev/null +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/main.py @@ -0,0 +1 @@ +# Databricks notebook source diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml b/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/script b/acceptance/bundle/resources/jobs/depends-on-reorder/script new file mode 100644 index 0000000000..3e0c67c396 --- /dev/null +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/script @@ -0,0 +1,2 @@ +trace $CLI bundle deploy +trace $CLI bundle plan | contains.py "0 to add, 0 to change, 0 to delete" diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml b/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml new file mode 100644 index 0000000000..e1290bad9f --- /dev/null +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml @@ -0,0 +1,2 @@ +RecordRequests = false +Ignore = [".databricks"] diff --git a/libs/testserver/jobs.go b/libs/testserver/jobs.go index 15800341de..cd9432fac1 100644 --- a/libs/testserver/jobs.go +++ b/libs/testserver/jobs.go @@ -106,6 +106,12 @@ func jobFixUps(jobSettings *jobs.JobSettings) { for i := range jobSettings.Tasks { task := &jobSettings.Tasks[i] + // Sort depends_on by task_key to simulate the real API which returns + // dependencies in a different order than submitted. + slices.SortFunc(task.DependsOn, func(a, b jobs.TaskDependency) int { + return cmp.Compare(a.TaskKey, b.TaskKey) + }) + // Set task email notifications to empty struct if not set if task.EmailNotifications == nil { task.EmailNotifications = &jobs.TaskEmailNotifications{} From 14ba9e3fda06645f53f10ace7feb612168b93216 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:03:48 +0000 Subject: [PATCH 04/12] acceptance: add output.txt and make depends-on-reorder test direct-only The depends_on reordering fix is in the direct engine. The terraform engine handles ordering through its provider, so the acceptance test should only run with the direct engine. Task: 001.md Co-authored-by: Isaac --- .../resources/jobs/depends-on-reorder/out.test.toml | 2 +- .../bundle/resources/jobs/depends-on-reorder/output.txt | 9 +++++++++ .../bundle/resources/jobs/depends-on-reorder/test.toml | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/output.txt diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml b/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml index d560f1de04..54146af564 100644 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml @@ -2,4 +2,4 @@ Local = true Cloud = false [EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] + DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/output.txt b/acceptance/bundle/resources/jobs/depends-on-reorder/output.txt new file mode 100644 index 0000000000..6d239f5055 --- /dev/null +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/output.txt @@ -0,0 +1,9 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml b/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml index e1290bad9f..09aa42374f 100644 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml +++ b/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml @@ -1,2 +1,5 @@ RecordRequests = false Ignore = [".databricks"] + +[EnvMatrix] +DATABRICKS_BUNDLE_ENGINE = ["direct"] From e3b5e65db4d3179a6e62b59e11ab2f205f89c96f Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:18:01 +0000 Subject: [PATCH 05/12] Add changelog entry for depends_on reordering fix Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0e4d6de08f..1801896aa1 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -9,6 +9,7 @@ * Added `--limit` flag to all paginated list commands for client-side result capping ([#4984](https://github.com/databricks/cli/pull/4984)). ### Bundles +* engine/direct: Fix phantom diffs from `depends_on` reordering in job tasks (denik/jobs-depends-on) ### Dependency updates From 5e000934295d21dee7c827d50359f9925d2c049c Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:24:58 +0000 Subject: [PATCH 06/12] Update NEXT_CHANGELOG.md with PR #4990 --- NEXT_CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 1801896aa1..9344db5523 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -9,7 +9,7 @@ * Added `--limit` flag to all paginated list commands for client-side result capping ([#4984](https://github.com/databricks/cli/pull/4984)). ### Bundles -* engine/direct: Fix phantom diffs from `depends_on` reordering in job tasks (denik/jobs-depends-on) +* engine/direct: Fix phantom diffs from `depends_on` reordering in job tasks ([#4990](https://github.com/databricks/cli/pull/4990)) ### Dependency updates From 75a9b0f789ecf12ee0740cea533a524b67ab61f4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:29:05 +0000 Subject: [PATCH 07/12] acceptance: add invariant test config for job with depends_on Adds a template config with tasks that use depends_on to verify depends_on handling is stable across deploy/redeploy cycles. --- .../configs/job_with_depends_on.yml.tmpl | 22 +++++++++++++++++++ acceptance/bundle/invariant/test.toml | 1 + 2 files changed, 23 insertions(+) create mode 100644 acceptance/bundle/invariant/configs/job_with_depends_on.yml.tmpl diff --git a/acceptance/bundle/invariant/configs/job_with_depends_on.yml.tmpl b/acceptance/bundle/invariant/configs/job_with_depends_on.yml.tmpl new file mode 100644 index 0000000000..8a460d1d90 --- /dev/null +++ b/acceptance/bundle/invariant/configs/job_with_depends_on.yml.tmpl @@ -0,0 +1,22 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + jobs: + foo: + name: test-job-$UNIQUE_NAME + tasks: + - task_key: main + notebook_task: + notebook_path: /Shared/notebook + - task_key: process + depends_on: + - task_key: main + notebook_task: + notebook_path: /Shared/notebook + - task_key: finalize + depends_on: + - task_key: process + - task_key: main + notebook_task: + notebook_path: /Shared/notebook diff --git a/acceptance/bundle/invariant/test.toml b/acceptance/bundle/invariant/test.toml index 85b2defc92..2924a6062c 100644 --- a/acceptance/bundle/invariant/test.toml +++ b/acceptance/bundle/invariant/test.toml @@ -35,6 +35,7 @@ EnvMatrix.INPUT_CONFIG = [ "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", + "job_with_depends_on.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", From 82f2c0319ebda63961942bc085c8a90eded1a26d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:36:43 +0000 Subject: [PATCH 08/12] Replace custom depends-on-reorder test with invariant test config Remove the dedicated acceptance/bundle/resources/jobs/depends-on-reorder/ test since the invariant test config job_with_depends_on.yml.tmpl covers the same scenario through the no_drift and continue_293 variants. Exclude job_with_depends_on from the migrate variant because terraform does not apply keyed-slice ordering for depends_on. Task: 002.md Co-authored-by: Isaac --- .../invariant/continue_293/out.test.toml | 2 +- .../bundle/invariant/migrate/out.test.toml | 2 +- acceptance/bundle/invariant/migrate/test.toml | 4 ++++ .../bundle/invariant/no_drift/out.test.toml | 2 +- .../jobs/depends-on-reorder/databricks.yml | 21 ------------------- .../resources/jobs/depends-on-reorder/main.py | 1 - .../jobs/depends-on-reorder/out.test.toml | 5 ----- .../jobs/depends-on-reorder/output.txt | 9 -------- .../resources/jobs/depends-on-reorder/script | 2 -- .../jobs/depends-on-reorder/test.toml | 5 ----- 10 files changed, 7 insertions(+), 46 deletions(-) delete mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml delete mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/main.py delete mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml delete mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/output.txt delete mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/script delete mode 100644 acceptance/bundle/resources/jobs/depends-on-reorder/test.toml diff --git a/acceptance/bundle/invariant/continue_293/out.test.toml b/acceptance/bundle/invariant/continue_293/out.test.toml index 7abd75f42e..18de4aecc1 100644 --- a/acceptance/bundle/invariant/continue_293/out.test.toml +++ b/acceptance/bundle/invariant/continue_293/out.test.toml @@ -4,4 +4,4 @@ RequiresUnityCatalog = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct"] - INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_with_permissions.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "pipeline_config_dots.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "schema_grant_ref.yml.tmpl", "schema_with_grants.yml.tmpl", "secret_scope.yml.tmpl", "secret_scope_default_backend_type.yml.tmpl", "secret_scope_with_permissions.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] + INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_depends_on.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_with_permissions.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "pipeline_config_dots.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "schema_grant_ref.yml.tmpl", "schema_with_grants.yml.tmpl", "secret_scope.yml.tmpl", "secret_scope_default_backend_type.yml.tmpl", "secret_scope_with_permissions.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] diff --git a/acceptance/bundle/invariant/migrate/out.test.toml b/acceptance/bundle/invariant/migrate/out.test.toml index 7abd75f42e..18de4aecc1 100644 --- a/acceptance/bundle/invariant/migrate/out.test.toml +++ b/acceptance/bundle/invariant/migrate/out.test.toml @@ -4,4 +4,4 @@ RequiresUnityCatalog = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct"] - INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_with_permissions.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "pipeline_config_dots.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "schema_grant_ref.yml.tmpl", "schema_with_grants.yml.tmpl", "secret_scope.yml.tmpl", "secret_scope_default_backend_type.yml.tmpl", "secret_scope_with_permissions.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] + INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_depends_on.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_with_permissions.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "pipeline_config_dots.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "schema_grant_ref.yml.tmpl", "schema_with_grants.yml.tmpl", "secret_scope.yml.tmpl", "secret_scope_default_backend_type.yml.tmpl", "secret_scope_with_permissions.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] diff --git a/acceptance/bundle/invariant/migrate/test.toml b/acceptance/bundle/invariant/migrate/test.toml index 5ffa32ae13..5a23b0e4e7 100644 --- a/acceptance/bundle/invariant/migrate/test.toml +++ b/acceptance/bundle/invariant/migrate/test.toml @@ -13,3 +13,7 @@ EnvMatrixExclude.no_cross_resource_ref = ["INPUT_CONFIG=job_cross_resource_ref.y # Grant cross-references require the EmbeddedSlice pattern not present in terraform mode. EnvMatrixExclude.no_grant_ref = ["INPUT_CONFIG=schema_grant_ref.yml.tmpl"] + +# Terraform does not apply keyed-slice ordering for depends_on, so migration +# detects drift when depends_on order in config differs from server-side order. +EnvMatrixExclude.no_depends_on = ["INPUT_CONFIG=job_with_depends_on.yml.tmpl"] diff --git a/acceptance/bundle/invariant/no_drift/out.test.toml b/acceptance/bundle/invariant/no_drift/out.test.toml index 7abd75f42e..18de4aecc1 100644 --- a/acceptance/bundle/invariant/no_drift/out.test.toml +++ b/acceptance/bundle/invariant/no_drift/out.test.toml @@ -4,4 +4,4 @@ RequiresUnityCatalog = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct"] - INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_with_permissions.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "pipeline_config_dots.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "schema_grant_ref.yml.tmpl", "schema_with_grants.yml.tmpl", "secret_scope.yml.tmpl", "secret_scope_default_backend_type.yml.tmpl", "secret_scope_with_permissions.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] + INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_depends_on.yml.tmpl", "job_with_permissions.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_with_permissions.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "pipeline_config_dots.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "schema_grant_ref.yml.tmpl", "schema_with_grants.yml.tmpl", "secret_scope.yml.tmpl", "secret_scope_default_backend_type.yml.tmpl", "secret_scope_with_permissions.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml b/acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml deleted file mode 100644 index 3fdfae66b9..0000000000 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/databricks.yml +++ /dev/null @@ -1,21 +0,0 @@ -bundle: - name: test-bundle - -resources: - jobs: - foo: - tasks: - - task_key: main - notebook_task: - notebook_path: main.py - - task_key: process - depends_on: - - task_key: main - notebook_task: - notebook_path: main.py - - task_key: finalize - depends_on: - - task_key: process - - task_key: main - notebook_task: - notebook_path: main.py diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/main.py b/acceptance/bundle/resources/jobs/depends-on-reorder/main.py deleted file mode 100644 index 1645e04b1d..0000000000 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/main.py +++ /dev/null @@ -1 +0,0 @@ -# Databricks notebook source diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml b/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml deleted file mode 100644 index 54146af564..0000000000 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/out.test.toml +++ /dev/null @@ -1,5 +0,0 @@ -Local = true -Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/output.txt b/acceptance/bundle/resources/jobs/depends-on-reorder/output.txt deleted file mode 100644 index 6d239f5055..0000000000 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/output.txt +++ /dev/null @@ -1,9 +0,0 @@ - ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... -Deploying resources... -Updating deployment state... -Deployment complete! - ->>> [CLI] bundle plan -Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/script b/acceptance/bundle/resources/jobs/depends-on-reorder/script deleted file mode 100644 index 3e0c67c396..0000000000 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/script +++ /dev/null @@ -1,2 +0,0 @@ -trace $CLI bundle deploy -trace $CLI bundle plan | contains.py "0 to add, 0 to change, 0 to delete" diff --git a/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml b/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml deleted file mode 100644 index 09aa42374f..0000000000 --- a/acceptance/bundle/resources/jobs/depends-on-reorder/test.toml +++ /dev/null @@ -1,5 +0,0 @@ -RecordRequests = false -Ignore = [".databricks"] - -[EnvMatrix] -DATABRICKS_BUNDLE_ENGINE = ["direct"] From ad2726504126a7665ba92a94a5ae24f2b4badf31 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 14:00:21 +0000 Subject: [PATCH 09/12] Enable migration test for job_with_depends_on config Remove the EnvMatrixExclude for job_with_depends_on.yml.tmpl from the migration invariant test. Instead, pass --noplancheck to the migrate command for this config, since Terraform's plan detects false drift due to depends_on ordering differences. The post-migration plan check (using direct engine) validates no drift correctly because the direct engine handles keyed-slice ordering. Task: 003.md Co-authored-by: Isaac --- acceptance/bundle/invariant/migrate/script | 9 ++++++++- acceptance/bundle/invariant/migrate/test.toml | 4 ---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/acceptance/bundle/invariant/migrate/script b/acceptance/bundle/invariant/migrate/script index d02200cb53..cd4a077ee8 100644 --- a/acceptance/bundle/invariant/migrate/script +++ b/acceptance/bundle/invariant/migrate/script @@ -34,7 +34,14 @@ cat LOG.deploy | contains.py '!panic:' '!internal error' > /dev/null echo INPUT_CONFIG_OK -trace $CLI bundle deployment migrate &> LOG.migrate +MIGRATE_ARGS="" +# Terraform does not apply keyed-slice ordering for depends_on, so its plan +# detects drift when depends_on order in config differs from server-side order. +if [[ "$INPUT_CONFIG" == "job_with_depends_on.yml.tmpl" ]]; then + MIGRATE_ARGS="--noplancheck" +fi + +trace $CLI bundle deployment migrate $MIGRATE_ARGS &> LOG.migrate cat LOG.migrate | contains.py '!panic:' '!internal error' > /dev/null diff --git a/acceptance/bundle/invariant/migrate/test.toml b/acceptance/bundle/invariant/migrate/test.toml index 5a23b0e4e7..5ffa32ae13 100644 --- a/acceptance/bundle/invariant/migrate/test.toml +++ b/acceptance/bundle/invariant/migrate/test.toml @@ -13,7 +13,3 @@ EnvMatrixExclude.no_cross_resource_ref = ["INPUT_CONFIG=job_cross_resource_ref.y # Grant cross-references require the EmbeddedSlice pattern not present in terraform mode. EnvMatrixExclude.no_grant_ref = ["INPUT_CONFIG=schema_grant_ref.yml.tmpl"] - -# Terraform does not apply keyed-slice ordering for depends_on, so migration -# detects drift when depends_on order in config differs from server-side order. -EnvMatrixExclude.no_depends_on = ["INPUT_CONFIG=job_with_depends_on.yml.tmpl"] From ebceb5dfbfd3f2b2e1161533f76689b35694959a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 14:18:59 +0000 Subject: [PATCH 10/12] Sort depends_on by task_key in terraform job conversion The terraform provider sorts depends_on entries by task_key on Read (see terraform-provider-databricks PR #3000). Since depends_on uses TypeList (order-sensitive), terraform plan detects positional differences when the config order doesn't match the sorted state order. Fix by sorting depends_on in the CLI's terraform conversion, matching the provider's behavior. This removes the --noplancheck workaround from the migration invariant test. Task: 004.md --- acceptance/bundle/invariant/migrate/script | 9 +----- bundle/deploy/terraform/tfdyn/convert_job.go | 29 +++++++++++++++++++ .../terraform/tfdyn/convert_job_test.go | 8 +++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/acceptance/bundle/invariant/migrate/script b/acceptance/bundle/invariant/migrate/script index cd4a077ee8..d02200cb53 100644 --- a/acceptance/bundle/invariant/migrate/script +++ b/acceptance/bundle/invariant/migrate/script @@ -34,14 +34,7 @@ cat LOG.deploy | contains.py '!panic:' '!internal error' > /dev/null echo INPUT_CONFIG_OK -MIGRATE_ARGS="" -# Terraform does not apply keyed-slice ordering for depends_on, so its plan -# detects drift when depends_on order in config differs from server-side order. -if [[ "$INPUT_CONFIG" == "job_with_depends_on.yml.tmpl" ]]; then - MIGRATE_ARGS="--noplancheck" -fi - -trace $CLI bundle deployment migrate $MIGRATE_ARGS &> LOG.migrate +trace $CLI bundle deployment migrate &> LOG.migrate cat LOG.migrate | contains.py '!panic:' '!internal error' > /dev/null diff --git a/bundle/deploy/terraform/tfdyn/convert_job.go b/bundle/deploy/terraform/tfdyn/convert_job.go index c9f7e8219a..3efc49de98 100644 --- a/bundle/deploy/terraform/tfdyn/convert_job.go +++ b/bundle/deploy/terraform/tfdyn/convert_job.go @@ -114,6 +114,20 @@ func patchApplyPolicyDefaultValues(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return v, nil } +func sortDependsOn(v dyn.Value, key string) dyn.Value { + deps, ok := v.Get(key).AsSequence() + if !ok || len(deps) < 2 { + return v + } + slices.SortFunc(deps, func(a, b dyn.Value) int { + ak, _ := a.Get("task_key").AsString() + bk, _ := b.Get("task_key").AsString() + return cmp.Compare(ak, bk) + }) + v, _ = dyn.Set(v, key, dyn.V(deps)) + return v +} + func convertJobResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) { // Normalize the input value to the underlying job schema. // This removes superfluous keys and adapts the input to the expected schema. @@ -155,6 +169,21 @@ func convertJobResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) { } } + // Sort depends_on entries within each task by task_key. The terraform provider + // sorts depends_on on Read (see https://github.com/databricks/terraform-provider-databricks/pull/3000). + // Since depends_on uses TypeList (order-sensitive), the config order must match the + // state order to avoid perpetual drift in terraform plan. + vout, err = dyn.Map(vout, "tasks", dyn.Foreach(func(_ dyn.Path, task dyn.Value) (dyn.Value, error) { + task = sortDependsOn(task, "depends_on") + task, _ = dyn.Map(task, "for_each_task.task", func(_ dyn.Path, inner dyn.Value) (dyn.Value, error) { + return sortDependsOn(inner, "depends_on"), nil + }) + return task, nil + })) + if err != nil { + return dyn.InvalidValue, err + } + // Apply default task source logic vout, err = applyDefaultTaskSource(vout) if err != nil { diff --git a/bundle/deploy/terraform/tfdyn/convert_job_test.go b/bundle/deploy/terraform/tfdyn/convert_job_test.go index 782075fc7f..fb0f14979f 100644 --- a/bundle/deploy/terraform/tfdyn/convert_job_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_job_test.go @@ -65,6 +65,10 @@ func TestConvertJob(t *testing.T) { { TaskKey: "task_key_c", JobClusterKey: "job_cluster_key_c", + DependsOn: []jobs.TaskDependency{ + {TaskKey: "task_key_b"}, + {TaskKey: "task_key_a"}, + }, }, { Description: "missing task key 😱", @@ -137,6 +141,10 @@ func TestConvertJob(t *testing.T) { map[string]any{ "task_key": "task_key_c", "job_cluster_key": "job_cluster_key_c", + "depends_on": []any{ + map[string]any{"task_key": "task_key_a"}, + map[string]any{"task_key": "task_key_b"}, + }, }, }, }, out.Job["my_job"]) From 3f0f2c2cf71416ddf0bc267af40c38802c2af7f5 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 14:42:36 +0000 Subject: [PATCH 11/12] Revert depends_on sorting in terraform job conversion Remove the sortDependsOn function and its usage in convertJobResource. The terraform provider already sorts depends_on by task_key on Read, so fixing the ordering in the CLI is unnecessary complexity. Instead, restore --noplancheck for job_with_depends_on in the migrate invariant test with a detailed comment explaining the false drift: the provider sorts depends_on by task_key using TypeList (order-sensitive), so terraform plan reports positional differences when the config order doesn't match the sorted state. Task: 005.md Co-authored-by: Isaac --- acceptance/bundle/invariant/migrate/script | 12 +++++++- bundle/deploy/terraform/tfdyn/convert_job.go | 29 ------------------- .../terraform/tfdyn/convert_job_test.go | 8 ----- 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/acceptance/bundle/invariant/migrate/script b/acceptance/bundle/invariant/migrate/script index d02200cb53..78f45faa7d 100644 --- a/acceptance/bundle/invariant/migrate/script +++ b/acceptance/bundle/invariant/migrate/script @@ -34,7 +34,17 @@ cat LOG.deploy | contains.py '!panic:' '!internal error' > /dev/null echo INPUT_CONFIG_OK -trace $CLI bundle deployment migrate &> LOG.migrate +MIGRATE_ARGS="" +# The terraform provider sorts depends_on entries alphabetically by task_key on Read +# (see terraform-provider-databricks PR #3000). Since depends_on uses TypeList +# (order-sensitive), terraform plan reports positional drift when the bundle config +# specifies depends_on in a different order than the provider's sorted state. +# This is a false positive -- the logical dependencies are identical. +if [[ "$INPUT_CONFIG" == "job_with_depends_on.yml.tmpl" ]]; then + MIGRATE_ARGS="--noplancheck" +fi + +trace $CLI bundle deployment migrate $MIGRATE_ARGS &> LOG.migrate cat LOG.migrate | contains.py '!panic:' '!internal error' > /dev/null diff --git a/bundle/deploy/terraform/tfdyn/convert_job.go b/bundle/deploy/terraform/tfdyn/convert_job.go index 3efc49de98..c9f7e8219a 100644 --- a/bundle/deploy/terraform/tfdyn/convert_job.go +++ b/bundle/deploy/terraform/tfdyn/convert_job.go @@ -114,20 +114,6 @@ func patchApplyPolicyDefaultValues(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return v, nil } -func sortDependsOn(v dyn.Value, key string) dyn.Value { - deps, ok := v.Get(key).AsSequence() - if !ok || len(deps) < 2 { - return v - } - slices.SortFunc(deps, func(a, b dyn.Value) int { - ak, _ := a.Get("task_key").AsString() - bk, _ := b.Get("task_key").AsString() - return cmp.Compare(ak, bk) - }) - v, _ = dyn.Set(v, key, dyn.V(deps)) - return v -} - func convertJobResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) { // Normalize the input value to the underlying job schema. // This removes superfluous keys and adapts the input to the expected schema. @@ -169,21 +155,6 @@ func convertJobResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) { } } - // Sort depends_on entries within each task by task_key. The terraform provider - // sorts depends_on on Read (see https://github.com/databricks/terraform-provider-databricks/pull/3000). - // Since depends_on uses TypeList (order-sensitive), the config order must match the - // state order to avoid perpetual drift in terraform plan. - vout, err = dyn.Map(vout, "tasks", dyn.Foreach(func(_ dyn.Path, task dyn.Value) (dyn.Value, error) { - task = sortDependsOn(task, "depends_on") - task, _ = dyn.Map(task, "for_each_task.task", func(_ dyn.Path, inner dyn.Value) (dyn.Value, error) { - return sortDependsOn(inner, "depends_on"), nil - }) - return task, nil - })) - if err != nil { - return dyn.InvalidValue, err - } - // Apply default task source logic vout, err = applyDefaultTaskSource(vout) if err != nil { diff --git a/bundle/deploy/terraform/tfdyn/convert_job_test.go b/bundle/deploy/terraform/tfdyn/convert_job_test.go index fb0f14979f..782075fc7f 100644 --- a/bundle/deploy/terraform/tfdyn/convert_job_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_job_test.go @@ -65,10 +65,6 @@ func TestConvertJob(t *testing.T) { { TaskKey: "task_key_c", JobClusterKey: "job_cluster_key_c", - DependsOn: []jobs.TaskDependency{ - {TaskKey: "task_key_b"}, - {TaskKey: "task_key_a"}, - }, }, { Description: "missing task key 😱", @@ -141,10 +137,6 @@ func TestConvertJob(t *testing.T) { map[string]any{ "task_key": "task_key_c", "job_cluster_key": "job_cluster_key_c", - "depends_on": []any{ - map[string]any{"task_key": "task_key_a"}, - map[string]any{"task_key": "task_key_b"}, - }, }, }, }, out.Job["my_job"]) From ef3cfc86695c7b8fa0f2ae4090be90161bcbfa0a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 15:41:57 +0000 Subject: [PATCH 12/12] lint fix --- libs/structs/structdiff/diff_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/structs/structdiff/diff_test.go b/libs/structs/structdiff/diff_test.go index 567287fd8c..474694d5e6 100644 --- a/libs/structs/structdiff/diff_test.go +++ b/libs/structs/structdiff/diff_test.go @@ -654,7 +654,7 @@ func TestGetStructDiffSliceKeys(t *testing.T) { func TestGetStructDiffNestedDependsOn(t *testing.T) { sliceKeys := map[string]KeyFunc{ - "tasks": taskKeyFunc, + "tasks": taskKeyFunc, "tasks[*].depends_on": depKeyFunc, } @@ -665,26 +665,26 @@ func TestGetStructDiffNestedDependsOn(t *testing.T) { }{ { name: "depends_on reordered no diff", - a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, - b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "b"}, {TaskKey: "a"}}}}}, + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "b"}, {TaskKey: "a"}}}}}, want: nil, }, { name: "depends_on field change", - a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a", Outcome: "success"}}}}}, - b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a", Outcome: "failed"}}}}}, + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a", Outcome: "success"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a", Outcome: "failed"}}}}}, want: []ResolvedChange{{Field: "tasks[task_key='c'].depends_on[task_key='a'].outcome", Old: "success", New: "failed"}}, }, { name: "depends_on element added", - a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}}}}}, - b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, want: []ResolvedChange{{Field: "tasks[task_key='c'].depends_on[task_key='b']", Old: nil, New: Dep{TaskKey: "b"}}}, }, { name: "depends_on element removed", - a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, - b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}}}}}, + a: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}, {TaskKey: "b"}}}}}, + b: Job{Tasks: []Task{{TaskKey: "c", DependsOn: []Dep{{TaskKey: "a"}}}}}, want: []ResolvedChange{{Field: "tasks[task_key='c'].depends_on[task_key='b']", Old: Dep{TaskKey: "b"}, New: nil}}, }, {