From 4d1b0a6bf03647d18e3a2806a40d5d22479df4c1 Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Thu, 10 Sep 2026 19:06:48 +0000 Subject: [PATCH 1/3] Preserve git-backed config in bundle generate app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bundle generate app always emitted a workspace source_code_path, even for an app that deploys from Git — silently converting it to workspace source and pointing source_code_path at a local directory that has nothing downloaded into it (a git-backed app has no workspace source to download). Emit git_repository + git_source instead when the app is git-backed, taking the reference from the app's git_source and falling back to default_git_source. Co-authored-by: Isaac Signed-off-by: atreyadbrx --- .nextchanges/bundles/generate-app-git.md | 1 + bundle/generate/app.go | 61 ++++++++++++++- bundle/generate/app_test.go | 94 ++++++++++++++++++++++++ cmd/bundle/generate/app.go | 6 +- 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 .nextchanges/bundles/generate-app-git.md create mode 100644 bundle/generate/app_test.go diff --git a/.nextchanges/bundles/generate-app-git.md b/.nextchanges/bundles/generate-app-git.md new file mode 100644 index 00000000000..949c8744adb --- /dev/null +++ b/.nextchanges/bundles/generate-app-git.md @@ -0,0 +1 @@ +`databricks bundle generate app` now reproduces a git-backed app's `git_repository` and `git_source` configuration instead of emitting a workspace `source_code_path`, so generating from a Git-deployed app no longer silently converts it to workspace source. diff --git a/bundle/generate/app.go b/bundle/generate/app.go index b4a37d8d6de..234967ce072 100644 --- a/bundle/generate/app.go +++ b/bundle/generate/app.go @@ -15,9 +15,21 @@ func ConvertAppToValue(app *apps.App, sourceCodePath string) (dyn.Value, error) // The majority of fields of the app struct are read-only. // We copy the relevant fields manually. dv := map[string]dyn.Value{ - "name": dyn.NewValue(app.Name, []dyn.Location{{Line: 1}}), - "description": dyn.NewValue(app.Description, []dyn.Location{{Line: 2}}), - "source_code_path": dyn.NewValue(sourceCodePath, []dyn.Location{{Line: 3}}), + "name": dyn.NewValue(app.Name, []dyn.Location{{Line: 1}}), + "description": dyn.NewValue(app.Description, []dyn.Location{{Line: 2}}), + } + + // For a git-backed app, emit git_repository + git_source instead of a + // workspace source_code_path. Otherwise the generated bundle would silently + // down-convert the app to workspace source and point source_code_path at a + // local directory that has nothing downloaded into it. + if app.GitRepository != nil { + dv["git_repository"] = gitRepositoryValue(app.GitRepository) + if gs := gitSourceValue(app); gs.Kind() != dyn.KindNil { + dv["git_source"] = gs + } + } else { + dv["source_code_path"] = dyn.NewValue(sourceCodePath, []dyn.Location{{Line: 4}}) } if ar.Kind() != dyn.KindNil { @@ -26,3 +38,46 @@ func ConvertAppToValue(app *apps.App, sourceCodePath string) (dyn.Value, error) return dyn.V(dv), nil } + +func gitRepositoryValue(r *apps.GitRepository) dyn.Value { + m := map[string]dyn.Value{ + "url": dyn.NewValue(r.Url, []dyn.Location{{Line: 1}}), + "provider": dyn.NewValue(r.Provider, []dyn.Location{{Line: 2}}), + } + if r.AutoDeploy { + m["auto_deploy"] = dyn.NewValue(r.AutoDeploy, []dyn.Location{{Line: 3}}) + } + return dyn.NewValue(m, []dyn.Location{{Line: 3}}) +} + +// gitSourceValue returns the reference the app deploys from (branch, tag, or +// commit, plus an optional repo-relative source_code_path). It prefers the +// configured git_source and falls back to the default source of the app's most +// recent deployment. System-populated fields (resolved_commit and the nested +// git_repository) are intentionally omitted. +func gitSourceValue(app *apps.App) dyn.Value { + src := app.GitSource + if src == nil { + src = app.DefaultGitSource + } + if src == nil { + return dyn.NilValue + } + + m := map[string]dyn.Value{} + switch { + case src.Branch != "": + m["branch"] = dyn.NewValue(src.Branch, []dyn.Location{{Line: 1}}) + case src.Tag != "": + m["tag"] = dyn.NewValue(src.Tag, []dyn.Location{{Line: 1}}) + case src.Commit != "": + m["commit"] = dyn.NewValue(src.Commit, []dyn.Location{{Line: 1}}) + } + if src.SourceCodePath != "" { + m["source_code_path"] = dyn.NewValue(src.SourceCodePath, []dyn.Location{{Line: 2}}) + } + if len(m) == 0 { + return dyn.NilValue + } + return dyn.NewValue(m, []dyn.Location{{Line: 4}}) +} diff --git a/bundle/generate/app_test.go b/bundle/generate/app_test.go new file mode 100644 index 00000000000..16c6faa30b3 --- /dev/null +++ b/bundle/generate/app_test.go @@ -0,0 +1,94 @@ +package generate + +import ( + "testing" + + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/apps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertAppToValueWorkspaceSource(t *testing.T) { + app := &apps.App{Name: "my-app", Description: "desc"} + + v, err := ConvertAppToValue(app, "../src/app") + require.NoError(t, err) + + scp, err := dyn.Get(v, "source_code_path") + require.NoError(t, err) + assert.Equal(t, "../src/app", scp.MustString()) + + _, err = dyn.Get(v, "git_repository") + assert.Error(t, err) + _, err = dyn.Get(v, "git_source") + assert.Error(t, err) +} + +func TestConvertAppToValueGitBacked(t *testing.T) { + app := &apps.App{ + Name: "my-app", + Description: "desc", + GitRepository: &apps.GitRepository{ + Url: "https://github.com/my-org/my-repo", + Provider: "gitHub", + AutoDeploy: true, + }, + GitSource: &apps.GitSource{ + Branch: "main", + SourceCodePath: "apps/my-app", + // System-populated; must not be written back. + ResolvedCommit: "abc123", + }, + } + + // The workspace source path is passed but must be ignored for a git-backed app. + v, err := ConvertAppToValue(app, "../src/app") + require.NoError(t, err) + + _, err = dyn.Get(v, "source_code_path") + assert.Error(t, err, "git-backed app must not emit a workspace source_code_path") + + url, err := dyn.Get(v, "git_repository.url") + require.NoError(t, err) + assert.Equal(t, "https://github.com/my-org/my-repo", url.MustString()) + + provider, err := dyn.Get(v, "git_repository.provider") + require.NoError(t, err) + assert.Equal(t, "gitHub", provider.MustString()) + + autoDeploy, err := dyn.Get(v, "git_repository.auto_deploy") + require.NoError(t, err) + assert.True(t, autoDeploy.MustBool()) + + branch, err := dyn.Get(v, "git_source.branch") + require.NoError(t, err) + assert.Equal(t, "main", branch.MustString()) + + gscp, err := dyn.Get(v, "git_source.source_code_path") + require.NoError(t, err) + assert.Equal(t, "apps/my-app", gscp.MustString()) + + _, err = dyn.Get(v, "git_source.resolved_commit") + assert.Error(t, err, "resolved_commit is output-only and must be omitted") +} + +func TestConvertAppToValueGitBackedDefaultSourceFallback(t *testing.T) { + app := &apps.App{ + Name: "my-app", + GitRepository: &apps.GitRepository{Url: "https://github.com/my-org/my-repo", Provider: "gitHub"}, + // git_source unset; fall back to the deployed reference in default_git_source. + DefaultGitSource: &apps.GitSource{Tag: "v1.0.0"}, + } + + v, err := ConvertAppToValue(app, "../src/app") + require.NoError(t, err) + + tag, err := dyn.Get(v, "git_source.tag") + require.NoError(t, err) + assert.Equal(t, "v1.0.0", tag.MustString()) + + // auto_deploy is omitted when unset. + _, err = dyn.Get(v, "git_repository.auto_deploy") + assert.Error(t, err) +} diff --git a/cmd/bundle/generate/app.go b/cmd/bundle/generate/app.go index 845853adbdf..bec005408cd 100644 --- a/cmd/bundle/generate/app.go +++ b/cmd/bundle/generate/app.go @@ -81,8 +81,10 @@ per target environment.`, sourceCodePath := app.DefaultSourceCodePath // If the source code path is not set, we don't need to download anything. - // This is the case for apps that are not yet deployed. - if sourceCodePath != "" { + // This is the case for apps that are not yet deployed. A git-backed app + // keeps its source in Git rather than the workspace, so there is nothing + // to download and ConvertAppToValue emits git_repository/git_source instead. + if app.GitRepository == nil && sourceCodePath != "" { err = downloader.MarkDirectoryForDownload(ctx, &sourceCodePath) if err != nil { return err From 585a74d03873cfed8cfbfdabd43edb652b8a1a2d Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Fri, 11 Sep 2026 13:48:20 +0000 Subject: [PATCH 2/3] Convert git-backed generate app test to an acceptance test Per review, replace the ConvertAppToValue unit test with an acceptance test under acceptance/bundle/generate/app_git_backed that stubs a git-backed app GET response and asserts the generated bundle config carries git_repository + git_source (no workspace source_code_path). Co-authored-by: Isaac Signed-off-by: atreyadbrx --- .../generate/app_git_backed/databricks.yml | 2 + .../generate/app_git_backed/out.app.yml | 12 +++ .../generate/app_git_backed/out.test.toml | 3 + .../bundle/generate/app_git_backed/output.txt | 10 ++ .../bundle/generate/app_git_backed/script | 1 + .../bundle/generate/app_git_backed/test.toml | 18 ++++ bundle/generate/app_test.go | 94 ------------------- 7 files changed, 46 insertions(+), 94 deletions(-) create mode 100644 acceptance/bundle/generate/app_git_backed/databricks.yml create mode 100644 acceptance/bundle/generate/app_git_backed/out.app.yml create mode 100644 acceptance/bundle/generate/app_git_backed/out.test.toml create mode 100644 acceptance/bundle/generate/app_git_backed/output.txt create mode 100644 acceptance/bundle/generate/app_git_backed/script create mode 100644 acceptance/bundle/generate/app_git_backed/test.toml delete mode 100644 bundle/generate/app_test.go diff --git a/acceptance/bundle/generate/app_git_backed/databricks.yml b/acceptance/bundle/generate/app_git_backed/databricks.yml new file mode 100644 index 00000000000..33f02e64d95 --- /dev/null +++ b/acceptance/bundle/generate/app_git_backed/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: app_git_backed diff --git a/acceptance/bundle/generate/app_git_backed/out.app.yml b/acceptance/bundle/generate/app_git_backed/out.app.yml new file mode 100644 index 00000000000..a8ddc85733b --- /dev/null +++ b/acceptance/bundle/generate/app_git_backed/out.app.yml @@ -0,0 +1,12 @@ +resources: + apps: + out: + name: my_git_app + description: This is a git-backed app + git_repository: + url: https://github.com/my-org/my-repo + provider: gitHub + auto_deploy: true + git_source: + branch: main + source_code_path: apps/my-app diff --git a/acceptance/bundle/generate/app_git_backed/out.test.toml b/acceptance/bundle/generate/app_git_backed/out.test.toml new file mode 100644 index 00000000000..e1af1a235ad --- /dev/null +++ b/acceptance/bundle/generate/app_git_backed/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/generate/app_git_backed/output.txt b/acceptance/bundle/generate/app_git_backed/output.txt new file mode 100644 index 00000000000..8b097941315 --- /dev/null +++ b/acceptance/bundle/generate/app_git_backed/output.txt @@ -0,0 +1,10 @@ +Loading app 'my_git_app' configuration +App configuration successfully saved to out.app.yml +Warning: Generated configuration is not included in the bundle + +The file out.app.yml is not matched by any pattern in the 'include' section of databricks.yml, +so it will not be deployed. Add a matching entry to the 'include' section, for example: + +include: + - *.yml + diff --git a/acceptance/bundle/generate/app_git_backed/script b/acceptance/bundle/generate/app_git_backed/script new file mode 100644 index 00000000000..8f39e55343f --- /dev/null +++ b/acceptance/bundle/generate/app_git_backed/script @@ -0,0 +1 @@ +$CLI bundle generate app --existing-app-name my_git_app --config-dir . --key out diff --git a/acceptance/bundle/generate/app_git_backed/test.toml b/acceptance/bundle/generate/app_git_backed/test.toml new file mode 100644 index 00000000000..096f61774f7 --- /dev/null +++ b/acceptance/bundle/generate/app_git_backed/test.toml @@ -0,0 +1,18 @@ +[[Server]] +Pattern = "GET /api/2.0/apps/my_git_app" +Response.Body = ''' +{ + "app_id": "1234567890", + "name": "my_git_app", + "description": "This is a git-backed app", + "git_repository": { + "url": "https://github.com/my-org/my-repo", + "provider": "gitHub", + "auto_deploy": true + }, + "git_source": { + "branch": "main", + "source_code_path": "apps/my-app" + } +} +''' diff --git a/bundle/generate/app_test.go b/bundle/generate/app_test.go deleted file mode 100644 index 16c6faa30b3..00000000000 --- a/bundle/generate/app_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package generate - -import ( - "testing" - - "github.com/databricks/cli/libs/dyn" - "github.com/databricks/databricks-sdk-go/service/apps" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConvertAppToValueWorkspaceSource(t *testing.T) { - app := &apps.App{Name: "my-app", Description: "desc"} - - v, err := ConvertAppToValue(app, "../src/app") - require.NoError(t, err) - - scp, err := dyn.Get(v, "source_code_path") - require.NoError(t, err) - assert.Equal(t, "../src/app", scp.MustString()) - - _, err = dyn.Get(v, "git_repository") - assert.Error(t, err) - _, err = dyn.Get(v, "git_source") - assert.Error(t, err) -} - -func TestConvertAppToValueGitBacked(t *testing.T) { - app := &apps.App{ - Name: "my-app", - Description: "desc", - GitRepository: &apps.GitRepository{ - Url: "https://github.com/my-org/my-repo", - Provider: "gitHub", - AutoDeploy: true, - }, - GitSource: &apps.GitSource{ - Branch: "main", - SourceCodePath: "apps/my-app", - // System-populated; must not be written back. - ResolvedCommit: "abc123", - }, - } - - // The workspace source path is passed but must be ignored for a git-backed app. - v, err := ConvertAppToValue(app, "../src/app") - require.NoError(t, err) - - _, err = dyn.Get(v, "source_code_path") - assert.Error(t, err, "git-backed app must not emit a workspace source_code_path") - - url, err := dyn.Get(v, "git_repository.url") - require.NoError(t, err) - assert.Equal(t, "https://github.com/my-org/my-repo", url.MustString()) - - provider, err := dyn.Get(v, "git_repository.provider") - require.NoError(t, err) - assert.Equal(t, "gitHub", provider.MustString()) - - autoDeploy, err := dyn.Get(v, "git_repository.auto_deploy") - require.NoError(t, err) - assert.True(t, autoDeploy.MustBool()) - - branch, err := dyn.Get(v, "git_source.branch") - require.NoError(t, err) - assert.Equal(t, "main", branch.MustString()) - - gscp, err := dyn.Get(v, "git_source.source_code_path") - require.NoError(t, err) - assert.Equal(t, "apps/my-app", gscp.MustString()) - - _, err = dyn.Get(v, "git_source.resolved_commit") - assert.Error(t, err, "resolved_commit is output-only and must be omitted") -} - -func TestConvertAppToValueGitBackedDefaultSourceFallback(t *testing.T) { - app := &apps.App{ - Name: "my-app", - GitRepository: &apps.GitRepository{Url: "https://github.com/my-org/my-repo", Provider: "gitHub"}, - // git_source unset; fall back to the deployed reference in default_git_source. - DefaultGitSource: &apps.GitSource{Tag: "v1.0.0"}, - } - - v, err := ConvertAppToValue(app, "../src/app") - require.NoError(t, err) - - tag, err := dyn.Get(v, "git_source.tag") - require.NoError(t, err) - assert.Equal(t, "v1.0.0", tag.MustString()) - - // auto_deploy is omitted when unset. - _, err = dyn.Get(v, "git_repository.auto_deploy") - assert.Error(t, err) -} From 9ca317329e34848fa1a4a3a443d9a885ae507964 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Mon, 14 Sep 2026 10:57:50 +0200 Subject: [PATCH 3/3] Update generate-app-git.md --- .nextchanges/bundles/generate-app-git.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/generate-app-git.md b/.nextchanges/bundles/generate-app-git.md index 949c8744adb..160c8ae5bde 100644 --- a/.nextchanges/bundles/generate-app-git.md +++ b/.nextchanges/bundles/generate-app-git.md @@ -1 +1 @@ -`databricks bundle generate app` now reproduces a git-backed app's `git_repository` and `git_source` configuration instead of emitting a workspace `source_code_path`, so generating from a Git-deployed app no longer silently converts it to workspace source. +* `databricks bundle generate app` now reproduces a git-backed app's `git_repository` and `git_source` configuration instead of emitting a workspace `source_code_path`, so generating from a Git-deployed app no longer silently converts it to workspace source.