Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
130 changes: 102 additions & 28 deletions cmd/apps/deploy_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"time"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
Expand Down Expand Up @@ -39,12 +40,10 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command
var (
force bool
skipValidation bool
skipTests bool
)

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

makeArgsOptionalWithBundle(deployCmd, "deploy [APP_NAME]")

Expand All @@ -53,7 +52,7 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command
if len(args) == 0 {
b := root.TryConfigureBundle(cmd)
if b != nil {
return runBundleDeploy(cmd, force, skipValidation, skipTests)
return runBundleDeploy(cmd, b, force, skipValidation)
}
}

Expand All @@ -65,10 +64,14 @@ func BundleDeployOverrideWithWrapper(wrapError ErrorWrapper) func(*cobra.Command

When run from a Databricks Apps project directory (containing databricks.yml)
without an APP_NAME argument, this command runs an enhanced deployment pipeline:
1. Validates the project (build, typecheck, lint for Node.js projects)
1. Validates the project if needed (code changed or never validated)
2. Deploys the project to the workspace
3. Runs the app

Validation is automatically run when:
- No previous validation state exists
- Code has changed since last validation (checksum mismatch)

When an APP_NAME argument is provided (or when not in a project directory),
creates an app deployment using the API directly.

Expand All @@ -77,7 +80,7 @@ Arguments:
When provided in a project directory, uses API deploy instead of project deploy.

Examples:
# Deploy from a project directory (enhanced flow with validation)
# Deploy from a project directory (auto-validates if needed)
databricks apps deploy

# Deploy from a specific target
Expand All @@ -86,7 +89,7 @@ Examples:
# Deploy a specific app using the API (even from a project directory)
databricks apps deploy my-app

# Deploy from project with validation skip
# Skip validation entirely
databricks apps deploy --skip-validation

# Force deploy (override git branch validation)
Expand All @@ -95,36 +98,24 @@ Examples:
}

// runBundleDeploy executes the enhanced deployment flow for project directories.
func runBundleDeploy(cmd *cobra.Command, force, skipValidation, skipTests bool) error {
func runBundleDeploy(cmd *cobra.Command, initialBundle *bundle.Bundle, force, skipValidation bool) error {
ctx := cmd.Context()

workDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}

// Step 1: Validate project (unless skipped)
if !skipValidation {
validator := validation.GetProjectValidator(workDir)
if validator != nil {
opts := validation.ValidateOptions{
SkipTests: skipTests,
}
result, err := validator.Validate(ctx, workDir, opts)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
// Get state directory from the initial bundle (before ProcessBundle reinitializes it)
stateDir := initialBundle.GetLocalStateDir(ctx)

if !result.Success {
if result.Details != nil {
cmdio.LogString(ctx, result.Details.Error())
}
return errors.New("validation failed - fix errors before deploying")
}
cmdio.LogString(ctx, "✅ "+result.Message)
} else {
log.Debugf(ctx, "No validator found for project type, skipping validation")
// Step 1: Validate if needed (unless --skip-validation)
if !skipValidation {
if err := validateIfNeeded(cmd, workDir, stateDir); err != nil {
return err
}
} else {
log.Debugf(ctx, "Skipping validation (--skip-validation)")
}

// Step 2: Deploy project
Expand Down Expand Up @@ -157,6 +148,11 @@ func runBundleDeploy(cmd *cobra.Command, force, skipValidation, skipTests bool)
return fmt.Errorf("failed to run app: %w. Run `databricks apps logs` to view logs", err)
}

// Step 4: Update state to deployed
if err := updateStateToDeployed(stateDir); err != nil {
log.Warnf(ctx, "Failed to update state file: %v", err)
}

cmdio.LogString(ctx, "✔ Deployment complete!")
return nil
}
Expand Down Expand Up @@ -207,3 +203,81 @@ func runBundleApp(ctx context.Context, b *bundle.Bundle, appKey string) error {

return nil
}

// validateIfNeeded checks validation state and runs validation if needed.
// stateDir is the bundle's local state directory for storing validation state.
func validateIfNeeded(cmd *cobra.Command, workDir, stateDir string) error {
ctx := cmd.Context()

state, err := validation.LoadState(stateDir)
if err != nil {
return fmt.Errorf("failed to load validation state: %w", err)
}

currentChecksum, err := validation.ComputeChecksum(workDir)
if err != nil {
return fmt.Errorf("failed to compute checksum: %w", err)
}

// Check if validation is needed
needsValidation := state == nil || currentChecksum != state.Checksum
if !needsValidation {
log.Debugf(ctx, "Validation state up-to-date (checksum: %s...)", state.Checksum[:12])
return nil
}

if state == nil {
log.Infof(ctx, "No previous validation state, running validation...")
} else {
log.Infof(ctx, "Code changed since last validation, re-validating...")
}

// Run validation
opts := validation.ValidateOptions{}
validator := validation.GetProjectValidator(workDir)
if validator != nil {
result, err := validator.Validate(ctx, workDir, opts)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}

if !result.Success {
if result.Details != nil {
cmdio.LogString(ctx, result.Details.Error())
}
return errors.New("validation failed - fix errors before deploying")
}
cmdio.LogString(ctx, "✅ "+result.Message)
} else {
log.Debugf(ctx, "No validator found for project type, skipping validation checks")
}

// Save state
newState := &validation.State{
State: validation.StateValidated,
ValidatedAt: time.Now().UTC(),
Checksum: currentChecksum,
}
if err := validation.SaveState(stateDir, newState); err != nil {
return fmt.Errorf("failed to save state: %w", err)
}

return nil
}

// updateStateToDeployed updates the state file to mark the project as deployed.
// stateDir is the bundle's local state directory.
func updateStateToDeployed(stateDir string) error {
state, err := validation.LoadState(stateDir)
if err != nil {
return err
}

if state == nil {
// No state file, nothing to update
return nil
}

state.State = validation.StateDeployed
return validation.SaveState(stateDir, state)
}
68 changes: 48 additions & 20 deletions cmd/apps/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"errors"
"fmt"
"os"
"time"

"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/apps/validation"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/log"
"github.com/spf13/cobra"
)

Expand All @@ -20,22 +23,21 @@ func newValidateCmd() *cobra.Command {
This command detects the project type and runs the appropriate validation:
- Node.js projects (package.json): runs npm install, build, typecheck, lint, and tests

After successful validation, a state file is saved that allows deployment
without re-running validation (unless code changes are detected).

Examples:
# Validate the current directory
databricks apps validate

# Validate a specific directory
databricks apps validate --path ./my-app

# Run a quick validation without tests
databricks apps validate --skip-tests`,
databricks apps validate --path ./my-app`,
RunE: func(cmd *cobra.Command, args []string) error {
return runValidate(cmd)
},
}

cmd.Flags().String("path", "", "Path to the project directory (defaults to current directory)")
cmd.Flags().Bool("skip-tests", false, "Skip running tests for faster validation")

return cmd
}
Expand All @@ -53,31 +55,57 @@ func runValidate(cmd *cobra.Command) error {
}
}

// Get validation options
skipTests, _ := cmd.Flags().GetBool("skip-tests")
opts := validation.ValidateOptions{
SkipTests: skipTests,
// Try to get bundle context for state storage
var stateDir string
b := root.TryConfigureBundle(cmd)
if b != nil {
stateDir = b.GetLocalStateDir(ctx)
}

opts := validation.ValidateOptions{}

// Get validator for project type
validator := validation.GetProjectValidator(projectPath)
if validator == nil {
return errors.New("no supported project type detected (looking for package.json)")
if validator != nil {
// Run validation
result, err := validator.Validate(ctx, projectPath, opts)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}

if !result.Success {
if result.Details != nil {
cmdio.LogString(ctx, result.Details.Error())
}
return errors.New("validation failed")
}
cmdio.LogString(ctx, "✅ "+result.Message)
} else {
cmdio.LogString(ctx, "✅ No validator found for project type, skipping validation checks")
}

// Save state only if we have a bundle context
if stateDir == "" {
log.Debugf(ctx, "No bundle context, skipping state save")
return nil
}

// Run validation
result, err := validator.Validate(ctx, projectPath, opts)
// Compute checksum and save state
checksum, err := validation.ComputeChecksum(projectPath)
if err != nil {
return fmt.Errorf("validation error: %w", err)
return fmt.Errorf("failed to compute checksum: %w", err)
}

if !result.Success {
if result.Details != nil {
cmdio.LogString(ctx, result.Details.Error())
}
return errors.New("validation failed")
state := &validation.State{
State: validation.StateValidated,
ValidatedAt: time.Now().UTC(),
Checksum: checksum,
}

if err := validation.SaveState(stateDir, state); err != nil {
return fmt.Errorf("failed to save state: %w", err)
}

cmdio.LogString(ctx, "✅ "+result.Message)
cmdio.LogString(ctx, fmt.Sprintf("State saved (checksum: %s...)", checksum[:12]))
return nil
}
7 changes: 3 additions & 4 deletions libs/apps/validation/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type validationStep struct {
command string
errorPrefix string
displayName string
skipIf func(workDir string, opts ValidateOptions) bool // Optional: skip step if this returns true
skipIf func(workDir string) bool // optional: skip step if this returns true
}

func (v *ValidationNodeJs) Validate(ctx context.Context, workDir string, opts ValidateOptions) (*ValidateResult, error) {
Expand All @@ -36,7 +36,7 @@ func (v *ValidationNodeJs) Validate(ctx context.Context, workDir string, opts Va
command: "npm install",
errorPrefix: "Failed to install dependencies",
displayName: "Installing dependencies",
skipIf: func(workDir string, _ ValidateOptions) bool { return hasNodeModules(workDir) },
skipIf: func(workDir string) bool { return hasNodeModules(workDir) },
},
{
name: "generate",
Expand Down Expand Up @@ -67,13 +67,12 @@ func (v *ValidationNodeJs) Validate(ctx context.Context, workDir string, opts Va
command: "npm run test --if-present",
errorPrefix: "Failed to run tests",
displayName: "Running tests",
skipIf: func(_ string, opts ValidateOptions) bool { return opts.SkipTests },
},
}

for _, step := range steps {
// Check if step should be skipped
if step.skipIf != nil && step.skipIf(workDir, opts) {
if step.skipIf != nil && step.skipIf(workDir) {
log.Debugf(ctx, "skipping %s (condition met)", step.name)
cmdio.LogString(ctx, "⏭️ Skipped "+step.displayName)
continue
Expand Down
Loading
Loading