Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
fix: prompt for required resources in apps init --features on a terminal
With --features (and no --name) on a terminal, apps init skipped both the
resource prompts and the required-resource check, so it scaffolded a project
whose databricks.yml declared required variables with no value and deploy
failed. Prompt for required resources that --set did not provide, then run the
existing resolve and validation step before any project files are written.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
  • Loading branch information
MarioCadenas and Isaac committed Oct 2, 2026
commit 2d859f75efb44c2bf120a6567a8d12657e6735d1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fixed `databricks apps init --features` in an interactive terminal skipping the prompts for required resources and creating a project with unset variables that failed to deploy.
71 changes: 52 additions & 19 deletions cmd/apps/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,14 +302,7 @@ func pluginHasResourceField(p *manifest.Plugin, resourceKey, fieldName string) b
// value in resourceValues. Returns an error with a --set hint if any are missing.
func validateRequiredResources(resources []manifest.Resource, resourceValues map[string]string) error {
for _, r := range resources {
found := false
for k := range resourceValues {
if strings.HasPrefix(k, r.Key()+".") {
found = true
break
}
}
if !found {
if !hasResourceValue(r, resourceValues) {
fieldHint := "id"
if names := r.FieldNames(); len(names) > 0 {
fieldHint = names[0]
Expand All @@ -320,6 +313,33 @@ func validateRequiredResources(resources []manifest.Resource, resourceValues map
return nil
}

// hasResourceValue reports whether resourceValues holds any value for the resource.
func hasResourceValue(r manifest.Resource, resourceValues map[string]string) bool {
for k := range resourceValues {
if strings.HasPrefix(k, r.Key()+".") {
return true
}
}
return false
}

// promptForMissingResources prompts for each required resource that has no value yet
// (for example from --set) and adds the answers to resourceValues.
func promptForMissingResources(ctx context.Context, resources []manifest.Resource, resourceValues map[string]string) error {
theme := prompt.AppkitTheme()
for _, r := range resources {
if hasResourceValue(r, resourceValues) {
continue
}
values, err := promptForResource(ctx, r, theme, true)
if err != nil {
return err
}
maps.Copy(resourceValues, values)
}
return nil
}

// tmplBundle holds the generated bundle configuration strings.
type tmplBundle struct {
Variables string
Expand Down Expand Up @@ -1245,14 +1265,6 @@ func runCreate(ctx context.Context, opts createOptions) error {
return err
}
}
// Prompt for deploy/run in interactive mode when no flags were set
if isInteractive && !skipDeployRunPrompt {
var err error
shouldDeploy, runMode, err = prompt.PromptForDeployAndRun(ctx)
if err != nil {
return err
}
}
}

// Expand deprecated --warehouse-id into --set values for each plugin that has a sql-warehouse resource.
Expand Down Expand Up @@ -1298,15 +1310,26 @@ func runCreate(ctx context.Context, opts createOptions) error {
}
}

// In flags/non-interactive mode, resolve derived values and validate resources.
// With --features on a terminal (and no --name), plugin selection and the resource
// prompts above are skipped, so required resources are prompted for here instead.
featuresOnTTY := isInteractive && opts.pluginsChanged && !flagsMode && !skipPluginSelection

// In flags/non-interactive mode and for --features on a terminal, resolve derived values
// and validate resources before any project files are written.
// Agentic mode skips validation — resources are filled in later.
if !agenticMode && (flagsMode || !isInteractive) {
if !agenticMode && (flagsMode || !isInteractive || featuresOnTTY) {
resources := m.CollectResources(selectedPlugins)

// Resolve derived values for resources that support it.
if resourceValues == nil {
resourceValues = make(map[string]string)
}
if featuresOnTTY {
if err := promptForMissingResources(ctx, resources, resourceValues); err != nil {
return err
}
}

// Resolve derived values for resources that support it.
for _, r := range resources {
resolveFn, ok := prompt.GetResolveFunc(r.Type)
if !ok {
Expand All @@ -1330,6 +1353,16 @@ func runCreate(ctx context.Context, opts createOptions) error {
}
}

// Prompt for deploy/run on a terminal when no deploy/run flags were set. The fully
// interactive and pre-rendered template paths already asked above.
if isInteractive && opts.pluginsChanged && !skipPluginSelection && !skipDeployRunPrompt {
var err error
shouldDeploy, runMode, err = prompt.PromptForDeployAndRun(ctx)
if err != nil {
return err
}
}

// Apply flag values for deploy/run when in flags mode, flags were explicitly set, or non-interactive
if skipDeployRunPrompt || !isInteractive {
var err error
Expand Down
26 changes: 26 additions & 0 deletions cmd/apps/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"io"
"io/fs"
"maps"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -1457,3 +1458,28 @@ func TestSetFirstAppNameNoResources(t *testing.T) {
require.NotNil(t, bundleName)
assert.Equal(t, "myapp", bundleName.Value)
}

func TestHasResourceValue(t *testing.T) {
r := manifest.Resource{ResourceKey: "sql-warehouse"}
assert.False(t, hasResourceValue(r, nil))
assert.False(t, hasResourceValue(r, map[string]string{"sql-warehouse-2.id": "wh"}))
assert.True(t, hasResourceValue(r, map[string]string{"sql-warehouse.id": "wh"}))
}

func TestPromptForMissingResourcesSkipsProvidedValues(t *testing.T) {
// Every resource already has a value (e.g. from --set), so nothing is prompted;
// a prompt would fail here because tests have no terminal.
resources := []manifest.Resource{
{Type: "sql_warehouse", ResourceKey: "sql-warehouse"},
{Type: "postgres", ResourceKey: "postgres"},
}
values := map[string]string{
"sql-warehouse.id": "wh",
"postgres.branch": "projects/p/branches/b",
"postgres.database": "db",
}
want := maps.Clone(values)

require.NoError(t, promptForMissingResources(t.Context(), resources, values))
assert.Equal(t, want, values)
}
Loading