diff --git a/cmd/apps/bundle_helpers.go b/cmd/apps/bundle_helpers.go index 6b1712de56..04992c592e 100644 --- a/cmd/apps/bundle_helpers.go +++ b/cmd/apps/bundle_helpers.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" "strings" "time" @@ -27,12 +29,49 @@ func makeArgsOptionalWithBundle(cmd *cobra.Command, usage string) { return fmt.Errorf("accepts at most 1 arg(s), received %d", len(args)) } if !hasBundleConfig() && len(args) != 1 { - return fmt.Errorf("accepts 1 arg(s), received %d", len(args)) + return missingAppNameError() } return nil } } +// missingAppNameError returns an error message that explains what the positional +// argument should be, and attempts to infer a suggestion from the local environment. +func missingAppNameError() error { + hint := inferAppNameHint() + msg := `missing required argument: APP_NAME + +Usage: databricks apps APP_NAME + +APP_NAME is the name of the Databricks app to operate on. +Alternatively, run this command from a project directory containing +databricks.yml to auto-detect the app name.` + + if hint != "" { + msg += "\n\nDid you mean?\n databricks apps deploy " + hint + } + + return errors.New(msg) +} + +// inferAppNameHint tries to suggest an app name from the local environment. +// Only returns a hint if the current directory looks like a Databricks app +// (contains app.yml or app.yaml), using the directory name as the suggestion. +func inferAppNameHint() string { + wd, err := os.Getwd() + if err != nil { + return "" + } + + for _, filename := range []string{"app.yml", "app.yaml"} { + if _, err := os.Stat(filepath.Join(wd, filename)); err == nil { + return filepath.Base(wd) + } + } + + return "" +} + // getAppNameFromArgs returns the app name from args or detects it from the bundle. // Returns (appName, fromBundle, error). func getAppNameFromArgs(cmd *cobra.Command, args []string) (string, bool, error) { diff --git a/cmd/apps/bundle_helpers_test.go b/cmd/apps/bundle_helpers_test.go index 359d365f86..0a9a9526b3 100644 --- a/cmd/apps/bundle_helpers_test.go +++ b/cmd/apps/bundle_helpers_test.go @@ -3,6 +3,8 @@ package apps import ( "context" "errors" + "os" + "path/filepath" "testing" "github.com/databricks/databricks-sdk-go/service/apps" @@ -106,6 +108,72 @@ func TestFormatAppStatusMessage(t *testing.T) { }) } +func TestInferAppNameHint(t *testing.T) { + t.Run("returns empty when no app config exists", func(t *testing.T) { + t.Chdir(t.TempDir()) + + assert.Equal(t, "", inferAppNameHint()) + }) + + t.Run("returns dir name when app.yml exists", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + err := os.WriteFile(filepath.Join(dir, "app.yml"), []byte("command: [\"python\"]"), 0o644) + assert.NoError(t, err) + + assert.Equal(t, filepath.Base(dir), inferAppNameHint()) + }) + + t.Run("returns dir name when app.yaml exists", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("command: [\"python\"]"), 0o644) + assert.NoError(t, err) + + assert.Equal(t, filepath.Base(dir), inferAppNameHint()) + }) + + t.Run("returns empty when cwd has been deleted", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + os.Remove(dir) + + assert.Equal(t, "", inferAppNameHint()) + }) +} + +func TestMissingAppNameError(t *testing.T) { + t.Run("includes APP_NAME and usage info", func(t *testing.T) { + t.Chdir(t.TempDir()) + + err := missingAppNameError() + assert.Contains(t, err.Error(), "APP_NAME") + assert.Contains(t, err.Error(), "databricks.yml") + assert.NotContains(t, err.Error(), "Did you mean") + }) + + t.Run("includes hint when app.yml exists", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + writeErr := os.WriteFile(filepath.Join(dir, "app.yml"), []byte("command: [\"python\"]"), 0o644) + assert.NoError(t, writeErr) + + err := missingAppNameError() + assert.Contains(t, err.Error(), "Did you mean") + assert.Contains(t, err.Error(), filepath.Base(dir)) + }) + + t.Run("gracefully handles deleted cwd", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + os.Remove(dir) + + err := missingAppNameError() + assert.Contains(t, err.Error(), "APP_NAME") + assert.NotContains(t, err.Error(), "Did you mean") + }) +} + func TestMakeArgsOptionalWithBundle(t *testing.T) { t.Run("updates command usage", func(t *testing.T) { cmd := &cobra.Command{}