Skip to content
Draft
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
2 changes: 2 additions & 0 deletions cmd/experimental/experimental.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package experimental

import (
mcp "github.com/databricks/cli/experimental/aitools/cmd"
dev "github.com/databricks/cli/experimental/dev/cmd"
"github.com/spf13/cobra"
)

Expand All @@ -20,6 +21,7 @@ These commands provide early access to new features that are still under
development. They may change or be removed in future versions without notice.`,
}

cmd.AddCommand(dev.New())
cmd.AddCommand(mcp.NewMcpCmd())

return cmd
Expand Down
3 changes: 3 additions & 0 deletions cmd/workspace/apps/overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ func listDeploymentsOverride(listDeploymentsCmd *cobra.Command, listDeploymentsR
}

func createOverride(createCmd *cobra.Command, createReq *apps.CreateAppRequest) {
createCmd.Short = `Create an app in your workspace.`
createCmd.Long = `Create an app in your workspace.`

originalRunE := createCmd.RunE
createCmd.RunE = func(cmd *cobra.Command, args []string) error {
err := originalRunE(cmd, args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"sql_warehouse_id": {
"type": "string",
"description": "SQL Warehouse ID",
"default": "",
"order": 2
},
"profile": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{if ne .profile ""}}DATABRICKS_CONFIG_PROFILE={{.profile}}{{else}}DATABRICKS_HOST={{workspace_host}}{{end}}
DATABRICKS_WAREHOUSE_ID={{.sql_warehouse_id}}
DATABRICKS_APP_PORT=8000
DATABRICKS_APP_NAME=minimal
DATABRICKS_APP_NAME={{.project_name}}
FLASK_RUN_HOST=0.0.0.0

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@
"typecheck": "tsc -p ./tsconfig.server.json --noEmit && tsc -p ./tsconfig.client.json --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:ast-grep": "tsx scripts/lint-ast-grep.ts",
"lint:ast-grep": "appkit-lint",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"test": "vitest run && npm run test:smoke",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:smoke": "playwright install chromium && playwright test tests/smoke.spec.ts",
"clean": "rm -rf client/dist dist build node_modules .smoke-test test-results playwright-report",
"typegen": "tsx scripts/generate-types.ts"
"typegen": "appkit-generate-types",
"setup": "appkit-setup --write"
},
"keywords": [],
"author": "",
Expand Down
24 changes: 24 additions & 0 deletions experimental/dev/cmd/app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package app

import (
"github.com/spf13/cobra"
)

func New() *cobra.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we drop the dev for now so iso databricks experimental dev apps ... we use databricks experimental apps ...?

It's already pretty long.

cmd := &cobra.Command{
Use: "app",
Short: "Manage Databricks applications",
Long: `Manage Databricks applications.

Provides a streamlined interface for creating, managing, and monitoring
full-stack Databricks applications built with TypeScript, React, and
Tailwind CSS.`,
}

cmd.AddCommand(newInitCmd())
cmd.AddCommand(newImportCmd())
cmd.AddCommand(newDeployCmd())
cmd.AddCommand(newDevRemoteCmd())

return cmd
}
228 changes: 228 additions & 0 deletions experimental/dev/cmd/app/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package app

import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"sync"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/resources"
"github.com/databricks/cli/bundle/run"
"github.com/databricks/cli/cmd/bundle/utils"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/log"
"github.com/spf13/cobra"
)

func newDeployCmd() *cobra.Command {
var (
force bool
skipBuild bool
)

cmd := &cobra.Command{
Use: "deploy",
Short: "Build, deploy the AppKit application and run it",
Long: `Build, deploy the AppKit application and run it.

This command runs a deployment pipeline:
1. Builds the frontend (npm run build)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not run the build. This makes it specific to node.js and it's also not required as Databricks Apps runs npm build during deployments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO app deploy should work for all apps.

2. Deploys the bundle to the workspace
3. Runs the app

Examples:
# Deploy to default target
databricks experimental dev app deploy

# Deploy to a specific target
databricks experimental dev app deploy --target prod

# Skip frontend build (if already built)
databricks experimental dev app deploy --skip-build

# Force deploy (override git branch validation)
databricks experimental dev app deploy --force

# Set bundle variables
databricks experimental dev app deploy --var="warehouse_id=abc123"`,
Args: root.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runDeploy(cmd, force, skipBuild)
},
}

cmd.Flags().StringP("target", "t", "", "Deployment target (e.g., dev, prod)")
cmd.Flags().BoolVar(&force, "force", false, "Force-override Git branch validation")
cmd.Flags().BoolVar(&skipBuild, "skip-build", false, "Skip npm build step")
cmd.Flags().StringSlice("var", []string{}, `Set values for variables defined in bundle config. Example: --var="key=value"`)

return cmd
}

func runDeploy(cmd *cobra.Command, force, skipBuild bool) error {
ctx := cmd.Context()

// Check for bundle configuration
if _, err := os.Stat("databricks.yml"); os.IsNotExist(err) {
return errors.New("no databricks.yml found; run this command from a bundle directory")
}

// Step 1: Build frontend (unless skipped)
if !skipBuild {
if err := runNpmTypegen(ctx); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

running typegen make sense but only if it is a node.js app and we have a package.json

return err
}
if err := runNpmBuild(ctx); err != nil {
return err
}
}

// Step 2: Deploy bundle
cmdio.LogString(ctx, "Deploying bundle...")
b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{
InitFunc: func(b *bundle.Bundle) {
b.Config.Bundle.Force = force
},
AlwaysPull: true,
FastValidate: true,
Build: true,
Deploy: true,
})
if err != nil {
return fmt.Errorf("deploy failed: %w", err)
}
log.Infof(ctx, "Deploy completed")

// Step 3: Detect and run app
appKey, err := detectApp(b)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we have more than one app?

if err != nil {
return err
}

log.Infof(ctx, "Running app: %s", appKey)
if err := runApp(ctx, b, appKey); err != nil {
cmdio.LogString(ctx, "✔ Deployment succeeded, but failed to start app")
return fmt.Errorf("failed to run app: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should print a message telling the user to run databricks apps logs to debug this.

}

cmdio.LogString(ctx, "✔ Deployment complete!")
return nil
}

// syncBuffer is a thread-safe buffer for capturing command output.
type syncBuffer struct {
mu sync.Mutex
buf bytes.Buffer
}

func (b *syncBuffer) Write(p []byte) (n int, err error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Write(p)
}

func (b *syncBuffer) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.String()
}

// runNpmTypegen runs npm run typegen in the current directory.
func runNpmTypegen(ctx context.Context) error {
if _, err := exec.LookPath("npm"); err != nil {
return errors.New("npm not found: please install Node.js")
}

var output syncBuffer

err := RunWithSpinnerCtx(ctx, "Generating types...", func() error {
cmd := exec.CommandContext(ctx, "npm", "run", "typegen")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd := exec.CommandContext(ctx, "npm", "run", "typegen")
cmd := exec.CommandContext(ctx, "npm", "run", "typegen", "--if-present")

cmd.Stdout = &output
cmd.Stderr = &output
return cmd.Run()
})
if err != nil {
out := output.String()
if out != "" {
return fmt.Errorf("typegen failed:\n%s", out)
}
return fmt.Errorf("typegen failed: %w", err)
}
return nil
}

// runNpmBuild runs npm run build in the current directory.
func runNpmBuild(ctx context.Context) error {
if _, err := exec.LookPath("npm"); err != nil {
return errors.New("npm not found: please install Node.js")
}

var output syncBuffer

err := RunWithSpinnerCtx(ctx, "Building frontend...", func() error {
cmd := exec.CommandContext(ctx, "npm", "run", "build")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd := exec.CommandContext(ctx, "npm", "run", "build")
cmd := exec.CommandContext(ctx, "npm", "run", "build", "--if-present")

cmd.Stdout = &output
cmd.Stderr = &output
return cmd.Run()
})
if err != nil {
out := output.String()
if out != "" {
return fmt.Errorf("build failed:\n%s", out)
}
return fmt.Errorf("build failed: %w", err)
}
return nil
}

// detectApp finds the single app in the bundle configuration.
func detectApp(b *bundle.Bundle) (string, error) {
apps := b.Config.Resources.Apps

if len(apps) == 0 {
return "", errors.New("no apps found in bundle configuration")
}

if len(apps) > 1 {
return "", errors.New("multiple apps found in bundle, cannot auto-detect")
}

for key := range apps {
return key, nil
}

return "", errors.New("unexpected error detecting app")
}

// runApp runs the specified app using the runner interface.
func runApp(ctx context.Context, b *bundle.Bundle, appKey string) error {
ref, err := resources.Lookup(b, appKey, run.IsRunnable)
if err != nil {
return fmt.Errorf("failed to lookup app: %w", err)
}

runner, err := run.ToRunner(b, ref)
if err != nil {
return fmt.Errorf("failed to create runner: %w", err)
}

output, err := runner.Run(ctx, &run.Options{})
if err != nil {
return fmt.Errorf("failed to run app: %w", err)
}

if output != nil {
resultString, err := output.String()
if err != nil {
return err
}
log.Infof(ctx, "App output: %s", resultString)
}

return nil
}
Loading