Skip to content

Commit 6c0f55d

Browse files
authored
improve apps init and validation commands (#4365)
## Changes This PR adds project-type-aware initialization and validation for Databricks Apps. ### Changes **Project Initializers** (libs/apps/initializer/) - Added Initializer interface with support for Node.js, Python/pip, and Python/uv projects - Each initializer handles: dependency installation, virtual environment setup, and local dev server startup - Detects project type from package.json, pyproject.toml, or requirements.txt - Reads app command from app.yaml or auto-detects (e.g., streamlit projects) **Validation Options** - Added `--skip-tests` flag to apps validate and apps deploy commands - Deploy defaults to `--skip-tests=true` for faster iteration - Validate defaults to `--skip-tests=false` for full validation **Interactive vs Flags Mode** - Fixed flag handling so explicit flags (--name, --template, --deploy, --run) are respected in non-interactive mode - Prevents unexpected prompts when flags are provided ## Why <!-- Why are these changes needed? Provide the context that the reviewer might be missing. For example, were there any decisions behind the change that are not reflected in the code itself? --> ## Tests <!-- How have you tested the changes? --> <!-- If your PR needs to be included in the release notes for next release, add a separate entry in NEXT_CHANGELOG.md as part of your PR. --> --------- Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
1 parent b2ac77e commit 6c0f55d

File tree

14 files changed

+868
-110
lines changed

14 files changed

+868
-110
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
/acceptance/pipelines/ @jefferycheng1 @kanterov @lennartkats-db
55
/cmd/pipelines/ @jefferycheng1 @kanterov @lennartkats-db
66
/cmd/labs/ @alexott @nfx
7+
/cmd/apps/ @databricks/eng-app-devex
8+
/libs/apps/ @databricks/eng-app-devex
79
/cmd/workspace/apps/ @databricks/eng-app-devex
810
/libs/apps/ @databricks/eng-app-devex
911
/acceptance/apps/ @databricks/eng-app-devex

cmd/apps/deploy_bundle.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command
3939
var (
4040
force bool
4141
skipValidation bool
42+
skipTests bool
4243
)
4344

4445
deployCmd.Flags().BoolVar(&force, "force", false, "Force-override Git branch validation")
4546
deployCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "Skip project validation (build, typecheck, lint)")
47+
deployCmd.Flags().BoolVar(&skipTests, "skip-tests", true, "Skip running tests during validation")
4648

4749
// Update the command usage to reflect that APP_NAME is optional when in bundle mode
4850
deployCmd.Use = "deploy [APP_NAME]"
@@ -68,7 +70,7 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command
6870
// Try to load bundle configuration
6971
b := root.TryConfigureBundle(cmd)
7072
if b != nil {
71-
return runBundleDeploy(cmd, force, skipValidation)
73+
return runBundleDeploy(cmd, force, skipValidation, skipTests)
7274
}
7375
}
7476

@@ -109,7 +111,7 @@ Examples:
109111
}
110112

111113
// runBundleDeploy executes the enhanced deployment flow for bundle directories.
112-
func runBundleDeploy(cmd *cobra.Command, force, skipValidation bool) error {
114+
func runBundleDeploy(cmd *cobra.Command, force, skipValidation, skipTests bool) error {
113115
ctx := cmd.Context()
114116

115117
// Get current working directory for validation
@@ -122,7 +124,10 @@ func runBundleDeploy(cmd *cobra.Command, force, skipValidation bool) error {
122124
if !skipValidation {
123125
validator := validation.GetProjectValidator(workDir)
124126
if validator != nil {
125-
result, err := validator.Validate(ctx, workDir)
127+
opts := validation.ValidateOptions{
128+
SkipTests: skipTests,
129+
}
130+
result, err := validator.Validate(ctx, workDir, opts)
126131
if err != nil {
127132
return fmt.Errorf("validation error: %w", err)
128133
}

cmd/apps/init.go

Lines changed: 115 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/charmbracelet/huh"
1515
"github.com/databricks/cli/cmd/root"
1616
"github.com/databricks/cli/libs/apps/features"
17+
"github.com/databricks/cli/libs/apps/initializer"
1718
"github.com/databricks/cli/libs/apps/prompt"
1819
"github.com/databricks/cli/libs/cmdctx"
1920
"github.com/databricks/cli/libs/cmdio"
@@ -80,15 +81,19 @@ Environment variables:
8081
RunE: func(cmd *cobra.Command, args []string) error {
8182
ctx := cmd.Context()
8283
return runCreate(ctx, createOptions{
83-
templatePath: templatePath,
84-
branch: branch,
85-
name: name,
86-
warehouseID: warehouseID,
87-
description: description,
88-
outputDir: outputDir,
89-
features: featuresFlag,
90-
deploy: deploy,
91-
run: run,
84+
templatePath: templatePath,
85+
branch: branch,
86+
name: name,
87+
nameProvided: cmd.Flags().Changed("name"),
88+
warehouseID: warehouseID,
89+
description: description,
90+
outputDir: outputDir,
91+
features: featuresFlag,
92+
deploy: deploy,
93+
deployChanged: cmd.Flags().Changed("deploy"),
94+
run: run,
95+
runChanged: cmd.Flags().Changed("run"),
96+
featuresChanged: cmd.Flags().Changed("features"),
9297
})
9398
},
9499
}
@@ -107,15 +112,19 @@ Environment variables:
107112
}
108113

109114
type createOptions struct {
110-
templatePath string
111-
branch string
112-
name string
113-
warehouseID string
114-
description string
115-
outputDir string
116-
features []string
117-
deploy bool
118-
run string
115+
templatePath string
116+
branch string
117+
name string
118+
nameProvided bool // true if --name flag was explicitly set (enables "flags mode")
119+
warehouseID string
120+
description string
121+
outputDir string
122+
features []string
123+
deploy bool
124+
deployChanged bool // true if --deploy flag was explicitly set
125+
run string
126+
runChanged bool // true if --run flag was explicitly set
127+
featuresChanged bool // true if --features flag was explicitly set
119128
}
120129

121130
// templateVars holds the variables for template substitution.
@@ -170,7 +179,8 @@ func parseDeployAndRunFlags(deploy bool, run string) (bool, prompt.RunMode, erro
170179

171180
// promptForFeaturesAndDeps prompts for features and their dependencies.
172181
// Used when the template uses the feature-fragment system.
173-
func promptForFeaturesAndDeps(ctx context.Context, preSelectedFeatures []string) (*prompt.CreateProjectConfig, error) {
182+
// skipDeployRunPrompt indicates whether to skip prompting for deploy/run (because flags were provided).
183+
func promptForFeaturesAndDeps(ctx context.Context, preSelectedFeatures []string, skipDeployRunPrompt bool) (*prompt.CreateProjectConfig, error) {
174184
config := &prompt.CreateProjectConfig{
175185
Dependencies: make(map[string]string),
176186
Features: preSelectedFeatures,
@@ -260,10 +270,12 @@ func promptForFeaturesAndDeps(ctx context.Context, preSelectedFeatures []string)
260270
}
261271
prompt.PrintAnswered(ctx, "Description", config.Description)
262272

263-
// Step 4: Deploy and run options
264-
config.Deploy, config.RunMode, err = prompt.PromptForDeployAndRun(ctx)
265-
if err != nil {
266-
return nil, err
273+
// Step 4: Deploy and run options (skip if any deploy/run flag was provided)
274+
if !skipDeployRunPrompt {
275+
config.Deploy, config.RunMode, err = prompt.PromptForDeployAndRun(ctx)
276+
if err != nil {
277+
return nil, err
278+
}
267279
}
268280

269281
return config, nil
@@ -474,11 +486,17 @@ func runCreate(ctx context.Context, opts createOptions) error {
474486
// Step 3: Determine template type and gather configuration
475487
usesFeatureFragments := features.HasFeaturesDirectory(templateDir)
476488

489+
// When --name is provided, user is in "flags mode" - use defaults instead of prompting
490+
flagsMode := opts.nameProvided
491+
477492
if usesFeatureFragments {
478493
// Feature-fragment template: prompt for features and their dependencies
479-
if isInteractive && len(selectedFeatures) == 0 {
480-
// Need to prompt for features (but we already have the name)
481-
config, err := promptForFeaturesAndDeps(ctx, selectedFeatures)
494+
// Skip deploy/run prompts if in flags mode or if deploy/run flags were explicitly set
495+
skipDeployRunPrompt := flagsMode || opts.deployChanged || opts.runChanged
496+
497+
if isInteractive && !opts.featuresChanged && !flagsMode {
498+
// Interactive mode without --features flag: prompt for features, dependencies, description
499+
config, err := promptForFeaturesAndDeps(ctx, selectedFeatures, skipDeployRunPrompt)
482500
if err != nil {
483501
return err
484502
}
@@ -487,15 +505,41 @@ func runCreate(ctx context.Context, opts createOptions) error {
487505
if config.Description != "" {
488506
opts.description = config.Description
489507
}
490-
shouldDeploy = config.Deploy
491-
runMode = config.RunMode
508+
// Use prompted values for deploy/run (only set if we prompted)
509+
if !skipDeployRunPrompt {
510+
shouldDeploy = config.Deploy
511+
runMode = config.RunMode
512+
}
492513

493514
// Get warehouse from dependencies if provided
494515
if wh, ok := dependencies["sql_warehouse_id"]; ok && wh != "" {
495516
opts.warehouseID = wh
496517
}
518+
} else if isInteractive && opts.featuresChanged && !flagsMode {
519+
// Interactive mode with --features flag: validate features, prompt for deploy/run if no flags
520+
flagValues := map[string]string{
521+
"warehouse-id": opts.warehouseID,
522+
}
523+
if len(selectedFeatures) > 0 {
524+
if err := features.ValidateFeatureDependencies(selectedFeatures, flagValues); err != nil {
525+
return err
526+
}
527+
}
528+
dependencies = make(map[string]string)
529+
if opts.warehouseID != "" {
530+
dependencies["sql_warehouse_id"] = opts.warehouseID
531+
}
532+
533+
// Prompt for deploy/run if no flags were set
534+
if !skipDeployRunPrompt {
535+
var err error
536+
shouldDeploy, runMode, err = prompt.PromptForDeployAndRun(ctx)
537+
if err != nil {
538+
return err
539+
}
540+
}
497541
} else {
498-
// Non-interactive or features provided via flag
542+
// Flags mode or non-interactive: validate features and use flag values
499543
flagValues := map[string]string{
500544
"warehouse-id": opts.warehouseID,
501545
}
@@ -508,6 +552,10 @@ func runCreate(ctx context.Context, opts createOptions) error {
508552
if opts.warehouseID != "" {
509553
dependencies["sql_warehouse_id"] = opts.warehouseID
510554
}
555+
}
556+
557+
// Apply flag values for deploy/run when in flags mode, flags were explicitly set, or non-interactive
558+
if skipDeployRunPrompt || !isInteractive {
511559
var err error
512560
shouldDeploy, runMode, err = parseDeployAndRunFlags(opts.deploy, opts.run)
513561
if err != nil {
@@ -562,11 +610,13 @@ func runCreate(ctx context.Context, opts createOptions) error {
562610
}
563611
}
564612

565-
// Prompt for description and post-creation actions
566-
if isInteractive {
567-
if opts.description == "" {
568-
opts.description = prompt.DefaultAppDescription
569-
}
613+
// Set default description if not provided
614+
if opts.description == "" {
615+
opts.description = prompt.DefaultAppDescription
616+
}
617+
618+
// Only prompt for deploy/run if not in flags mode and no deploy/run flags were set
619+
if isInteractive && !flagsMode && !opts.deployChanged && !opts.runChanged {
570620
var deployVal bool
571621
var runVal prompt.RunMode
572622
deployVal, runVal, err = prompt.PromptForDeployAndRun(ctx)
@@ -576,6 +626,7 @@ func runCreate(ctx context.Context, opts createOptions) error {
576626
shouldDeploy = deployVal
577627
runMode = runVal
578628
} else {
629+
// Flags mode or explicit flags: use flag values (or defaults if not set)
579630
var err error
580631
shouldDeploy, runMode, err = parseDeployAndRunFlags(opts.deploy, opts.run)
581632
if err != nil {
@@ -659,21 +710,34 @@ func runCreate(ctx context.Context, opts createOptions) error {
659710
return runErr
660711
}
661712

662-
// Run npm install
663-
runErr = runNpmInstall(ctx, absOutputDir)
664-
if runErr != nil {
665-
return runErr
713+
// Initialize project based on type (Node.js, Python, etc.)
714+
var nextStepsCmd string
715+
projectInitializer := initializer.GetProjectInitializer(absOutputDir)
716+
if projectInitializer != nil {
717+
result := projectInitializer.Initialize(ctx, absOutputDir)
718+
if !result.Success {
719+
if result.Error != nil {
720+
return fmt.Errorf("%s: %w", result.Message, result.Error)
721+
}
722+
return errors.New(result.Message)
723+
}
724+
nextStepsCmd = projectInitializer.NextSteps()
666725
}
667726

668-
// Run npm run setup
669-
runErr = runNpmSetup(ctx, absOutputDir)
670-
if runErr != nil {
671-
return runErr
727+
// Validate dev-remote is only supported for appkit projects
728+
if runMode == prompt.RunModeDevRemote {
729+
if projectInitializer == nil || !projectInitializer.SupportsDevRemote() {
730+
return errors.New("--run=dev-remote is only supported for Node.js projects with @databricks/appkit")
731+
}
672732
}
673733

674734
// Show next steps only if user didn't choose to deploy or run
675735
showNextSteps := !shouldDeploy && runMode == prompt.RunModeNone
676-
prompt.PrintSuccess(ctx, opts.name, absOutputDir, fileCount, showNextSteps)
736+
if showNextSteps {
737+
prompt.PrintSuccess(ctx, opts.name, absOutputDir, fileCount, nextStepsCmd)
738+
} else {
739+
prompt.PrintSuccess(ctx, opts.name, absOutputDir, fileCount, "")
740+
}
677741

678742
// Execute post-creation actions (deploy and/or run)
679743
if shouldDeploy || runMode != prompt.RunModeNone {
@@ -694,7 +758,7 @@ func runCreate(ctx context.Context, opts createOptions) error {
694758

695759
if runMode != prompt.RunModeNone {
696760
cmdio.LogString(ctx, "")
697-
if err := runPostCreateDev(ctx, runMode); err != nil {
761+
if err := runPostCreateDev(ctx, runMode, projectInitializer, absOutputDir); err != nil {
698762
return err
699763
}
700764
}
@@ -716,15 +780,15 @@ func runPostCreateDeploy(ctx context.Context) error {
716780
}
717781

718782
// runPostCreateDev runs the dev or dev-remote command in the current directory.
719-
func runPostCreateDev(ctx context.Context, mode prompt.RunMode) error {
783+
func runPostCreateDev(ctx context.Context, mode prompt.RunMode, projectInit initializer.Initializer, workDir string) error {
720784
switch mode {
721785
case prompt.RunModeDev:
722-
cmdio.LogString(ctx, "Starting development server (npm run dev)...")
723-
cmd := exec.CommandContext(ctx, "npm", "run", "dev")
724-
cmd.Stdout = os.Stdout
725-
cmd.Stderr = os.Stderr
726-
cmd.Stdin = os.Stdin
727-
return cmd.Run()
786+
if projectInit != nil {
787+
return projectInit.RunDev(ctx, workDir)
788+
}
789+
// Fallback for unknown project types
790+
cmdio.LogString(ctx, "⚠ Unknown project type, cannot start development server automatically")
791+
return nil
728792
case prompt.RunModeDevRemote:
729793
cmdio.LogString(ctx, "Starting remote development server...")
730794
executable, err := os.Executable()
@@ -741,39 +805,6 @@ func runPostCreateDev(ctx context.Context, mode prompt.RunMode) error {
741805
}
742806
}
743807

744-
// runNpmInstall runs npm install in the project directory.
745-
func runNpmInstall(ctx context.Context, projectDir string) error {
746-
// Check if npm is available
747-
if _, err := exec.LookPath("npm"); err != nil {
748-
cmdio.LogString(ctx, "⚠ npm not found. Please install Node.js and run 'npm install' manually.")
749-
return nil
750-
}
751-
752-
return prompt.RunWithSpinnerCtx(ctx, "Installing dependencies...", func() error {
753-
cmd := exec.CommandContext(ctx, "npm", "install")
754-
cmd.Dir = projectDir
755-
cmd.Stdout = nil // Suppress output
756-
cmd.Stderr = nil
757-
return cmd.Run()
758-
})
759-
}
760-
761-
// runNpmSetup runs npx appkit-setup in the project directory.
762-
func runNpmSetup(ctx context.Context, projectDir string) error {
763-
// Check if npx is available
764-
if _, err := exec.LookPath("npx"); err != nil {
765-
return nil
766-
}
767-
768-
return prompt.RunWithSpinnerCtx(ctx, "Running setup...", func() error {
769-
cmd := exec.CommandContext(ctx, "npx", "appkit-setup", "--write")
770-
cmd.Dir = projectDir
771-
cmd.Stdout = nil // Suppress output
772-
cmd.Stderr = nil
773-
return cmd.Run()
774-
})
775-
}
776-
777808
// renameFiles maps source file names to destination names (for files that can't use special chars).
778809
var renameFiles = map[string]string{
779810
"_gitignore": ".gitignore",

0 commit comments

Comments
 (0)