Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
77dd1c3
Overload apps start/stop to auto-detect app name from project
fjakobs Jan 24, 2026
2cfc57a
Add --target flag support and human-readable output for apps commands
fjakobs Jan 24, 2026
d20aa28
Make apps start/stop commands idempotent
fjakobs Jan 25, 2026
64239a6
Fix apps start/stop to show clean output in text mode
fjakobs Jan 25, 2026
4af9fce
Display app URL after starting app
fjakobs Jan 25, 2026
f4cd060
Improve app URL output formatting for better readability
fjakobs Jan 25, 2026
e15421f
Add bundle mode support to apps logs and unify code
fjakobs Jan 25, 2026
107de3c
Reduce code duplication in apps bundle commands and improve test cove…
fjakobs Jan 25, 2026
a5a1914
Remove code duplication and clean up test assertions
fjakobs Jan 25, 2026
6bf596e
Inline logs bundle handling and split start/stop bundle files
fjakobs Jan 26, 2026
40a9cf4
Add support for legacy app templates in apps init
fjakobs Jan 23, 2026
162bc12
Add databricks.yml generation for legacy app templates
fjakobs Jan 23, 2026
1aa7cb2
Use direct git clone instead of bundle init for legacy templates
fjakobs Jan 23, 2026
bc5ab65
Refactor apps init to eliminate code duplication
fjakobs Jan 23, 2026
237f32c
Improve apps init: add workspace host, use uv, fix resources, organiz…
fjakobs Jan 23, 2026
834de63
Fix apps init: convert camelCase to snake_case, improve defaults, cle…
fjakobs Jan 23, 2026
93af37e
Add --env-file .env flag to uv run commands
fjakobs Jan 23, 2026
1467e91
Add EnvFileBuilder for generating .env from app.yml
fjakobs Jan 23, 2026
420e5a3
Fix EnvFileBuilder to use databricks.yml resource names
fjakobs Jan 23, 2026
bcf38ae
Integrate EnvFileBuilder into import and legacy template init
fjakobs Jan 23, 2026
0b7dc3e
Fix .env generation for legacy templates - generate before inlining
fjakobs Jan 23, 2026
e522ae2
Clean up EnvFileBuilder: remove .env header comments and fix formatting
fjakobs Jan 23, 2026
2391125
Fix .env generation to support camelCase valueFrom in app.yml
fjakobs Jan 23, 2026
56b72e8
Refactor .env generation to use template with profile-aware MLflow co…
fjakobs Jan 24, 2026
9eb60e6
Add support for optional start_command in app template manifests
fjakobs Jan 24, 2026
3348d82
Change default UV initializer start command to "uv run start-app"
fjakobs Jan 24, 2026
4387b01
tweak .env file
fjakobs Jan 24, 2026
fe021f7
Add automatic .gitignore creation for legacy templates and imports
fjakobs Jan 24, 2026
ebdc9c1
Use Python 3.11 for virtual environment initialization
fjakobs Jan 24, 2026
6a940f4
Add user_api_scopes support to legacy template initialization
fjakobs Jan 27, 2026
b8b121c
Add "destroy" alias for "databricks apps delete" command
fjakobs Jan 27, 2026
617dce8
Refactor apps code: extract shared utilities and eliminate duplication
fjakobs Jan 28, 2026
ecf4ea0
Extract resource name constants and improve code organization
fjakobs Jan 28, 2026
dc1ad85
Refactor legacy template code into dedicated package structure
fjakobs Jan 28, 2026
005110a
Eliminate code duplication and add comprehensive tests
fjakobs Jan 29, 2026
5f80501
Fix createAppProgressCallback to use spinner interface instead of cha…
fjakobs Jan 29, 2026
8c45ba0
Consolidate duplicate camelToSnake implementations
fjakobs Jan 29, 2026
4ba5c87
Refactor legacy template resource handling with registry pattern
fjakobs Jan 29, 2026
012bc30
Initialize venv for legacy templates in apps init and import
fjakobs Jan 29, 2026
4d92af2
Use Go templates for all YAML generation in legacy templates
fjakobs Jan 29, 2026
70327b9
Replace Confirm with Select for deploy prompts
fjakobs Jan 29, 2026
688e3d3
Add resource bindings for UC volumes in legacy templates
fjakobs Jan 29, 2026
11d1d5d
Add framework type filtering for legacy templates in apps init
fjakobs Jan 29, 2026
42c2429
Show framework types at same level as AppKit in template selection
fjakobs Jan 29, 2026
03c4386
Hide file count in success message when zero
fjakobs Jan 29, 2026
7be60f7
make import output nicer
fjakobs Jan 29, 2026
bff824d
Add local run instructions to import success message
fjakobs Jan 29, 2026
2142461
Unify project initializer usage in import command
fjakobs Jan 29, 2026
92f67d3
Change terminology from 'bundle' to 'project' in import command
fjakobs Jan 29, 2026
522b879
Use PrintSuccess in import and remove fileCount parameter
fjakobs Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 157 additions & 155 deletions cmd/apps/import.go

Large diffs are not rendered by default.

217 changes: 217 additions & 0 deletions cmd/apps/import_resources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package apps

import (
"testing"

"github.com/databricks/databricks-sdk-go/service/apps"
"github.com/stretchr/testify/assert"
)

func TestBuildResourcesMap(t *testing.T) {
t.Run("with SQL warehouse", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "sql-warehouse",
SqlWarehouse: &apps.AppResourceSqlWarehouse{
Id: "abc123",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "abc123", resources["sql-warehouse"])
})

t.Run("with serving endpoint", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "serving-endpoint",
ServingEndpoint: &apps.AppResourceServingEndpoint{
Name: "my-endpoint",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "my-endpoint", resources["serving-endpoint"])
})

t.Run("with experiment", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "experiment",
Experiment: &apps.AppResourceExperiment{
ExperimentId: "exp-456",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "exp-456", resources["experiment"])
})

t.Run("with database", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "database",
Database: &apps.AppResourceDatabase{
DatabaseName: "my-db",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "my-db", resources["database"])
})

t.Run("with Genie space", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "genie-space",
GenieSpace: &apps.AppResourceGenieSpace{
SpaceId: "space-123",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "space-123", resources["genie-space"])
})

t.Run("with job", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "job",
Job: &apps.AppResourceJob{
Id: "job-789",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "job-789", resources["job"])
})

t.Run("with UC securable", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "uc-securable",
UcSecurable: &apps.AppResourceUcSecurable{
SecurableFullName: "catalog.schema.table",
},
},
},
}

resources := buildResourcesMap(app)
assert.Equal(t, "catalog.schema.table", resources["uc-securable"])
})

t.Run("with multiple resources", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "sql-warehouse",
SqlWarehouse: &apps.AppResourceSqlWarehouse{
Id: "warehouse-1",
},
},
{
Name: "experiment",
Experiment: &apps.AppResourceExperiment{
ExperimentId: "exp-2",
},
},
{
Name: "serving-endpoint",
ServingEndpoint: &apps.AppResourceServingEndpoint{
Name: "endpoint-3",
},
},
},
}

resources := buildResourcesMap(app)
assert.Len(t, resources, 3)
assert.Equal(t, "warehouse-1", resources["sql-warehouse"])
assert.Equal(t, "exp-2", resources["experiment"])
assert.Equal(t, "endpoint-3", resources["serving-endpoint"])
})

t.Run("with empty resources", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{},
}

resources := buildResourcesMap(app)
assert.Empty(t, resources)
})

t.Run("with nil resources", func(t *testing.T) {
app := &apps.App{
Resources: nil,
}

resources := buildResourcesMap(app)
assert.Empty(t, resources)
})

t.Run("skips resources with empty name", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "",
SqlWarehouse: &apps.AppResourceSqlWarehouse{
Id: "warehouse-1",
},
},
{
Name: "experiment",
Experiment: &apps.AppResourceExperiment{
ExperimentId: "exp-2",
},
},
},
}

resources := buildResourcesMap(app)
assert.Len(t, resources, 1)
assert.NotContains(t, resources, "")
assert.Equal(t, "exp-2", resources["experiment"])
})

t.Run("skips resources with nil type", func(t *testing.T) {
app := &apps.App{
Resources: []apps.AppResource{
{
Name: "no-type-resource",
// All type fields are nil
},
{
Name: "experiment",
Experiment: &apps.AppResourceExperiment{
ExperimentId: "exp-2",
},
},
},
}

resources := buildResourcesMap(app)
assert.Len(t, resources, 1)
assert.NotContains(t, resources, "no-type-resource")
assert.Equal(t, "exp-2", resources["experiment"])
})
}
81 changes: 76 additions & 5 deletions cmd/apps/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"testing"

"github.com/databricks/cli/cmd/apps/internal"
"github.com/databricks/cli/libs/dyn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -120,7 +121,7 @@ env: []`,

// Run function
appValue := tt.inputValue
filename, err := inlineAppConfigFile(&appValue)
filename, err := internal.InlineAppConfigFile(&appValue)

if tt.expectError {
assert.Error(t, err)
Expand Down Expand Up @@ -173,7 +174,7 @@ func TestInlineAppConfigFileErrors(t *testing.T) {
require.NoError(t, err)

appValue := dyn.V(map[string]dyn.Value{"name": dyn.V("test")})
_, err = inlineAppConfigFile(&appValue)
_, err = internal.InlineAppConfigFile(&appValue)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse")
})
Expand All @@ -192,7 +193,7 @@ func TestInlineAppConfigFileErrors(t *testing.T) {
require.NoError(t, err)

appValue := dyn.V("not a map")
_, err = inlineAppConfigFile(&appValue)
_, err = internal.InlineAppConfigFile(&appValue)
assert.Error(t, err)
assert.Contains(t, err.Error(), "app value is not a map")
})
Expand Down Expand Up @@ -231,7 +232,7 @@ func TestInlineAppConfigFileErrors(t *testing.T) {
}

appValue := dyn.V(map[string]dyn.Value{"name": dyn.V("test")})
_, err = inlineAppConfigFile(&appValue)
_, err = internal.InlineAppConfigFile(&appValue)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read")
})
Expand Down Expand Up @@ -263,7 +264,7 @@ resources:
"description": dyn.V("existing description"),
})

filename, err := inlineAppConfigFile(&appValue)
filename, err := internal.InlineAppConfigFile(&appValue)
require.NoError(t, err)
assert.Equal(t, "app.yml", filename)

Expand All @@ -283,6 +284,76 @@ resources:
})
}

func TestInlineAppConfigFileCamelCaseConversion(t *testing.T) {
tmpDir := t.TempDir()
originalDir, err := os.Getwd()
require.NoError(t, err)
defer func() {
_ = os.Chdir(originalDir)
}()
err = os.Chdir(tmpDir)
require.NoError(t, err)

// Create app.yml with camelCase field names (as might come from API)
err = os.WriteFile("app.yml", []byte(`command: ["python", "app.py"]
env:
- name: FOO
valueFrom: some-secret
- name: BAR
value: baz`), 0o644)
require.NoError(t, err)

appValue := dyn.V(map[string]dyn.Value{
"name": dyn.V("test-app"),
})

filename, err := internal.InlineAppConfigFile(&appValue)
require.NoError(t, err)
assert.Equal(t, "app.yml", filename)

// Verify that camelCase fields are converted to snake_case
appMap := appValue.MustMap()
var configValue dyn.Value
for _, pair := range appMap.Pairs() {
if pair.Key.MustString() == "config" {
configValue = pair.Value
break
}
}

require.NotEqual(t, dyn.KindInvalid, configValue.Kind(), "config section should exist")
configMap := configValue.MustMap()

var envValue dyn.Value
for _, pair := range configMap.Pairs() {
if pair.Key.MustString() == "env" {
envValue = pair.Value
break
}
}

require.NotEqual(t, dyn.KindInvalid, envValue.Kind(), "env should exist in config")
envList := envValue.MustSequence()
require.Len(t, envList, 2, "should have 2 env vars")

// Check first env var has value_from (snake_case), not valueFrom (camelCase)
firstEnv := envList[0].MustMap()
var hasValueFrom bool
var hasValueFromCamel bool
for _, pair := range firstEnv.Pairs() {
key := pair.Key.MustString()
if key == "value_from" {
hasValueFrom = true
}
if key == "valueFrom" {
hasValueFromCamel = true
}
}

assert.True(t, hasValueFrom, "should have value_from (snake_case) field")
assert.False(t, hasValueFromCamel, "should NOT have valueFrom (camelCase) field")
}

func TestPathContainsBundleFolder(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading