Skip to content

Commit 30db14f

Browse files
Add --skip-install flag to databricks apps init (#5396)
Previously `apps init` always ran the project's dependency installer (npm ci, uv sync, or pip install + venv) and there was no way to opt out. Add a --skip-install flag that: - skips the background npm install kicked off during prompts - skips the per-project initializer (npm/uv/pip install plus the Node.js `npx appkit setup` step) - still surfaces the project's NextSteps() hint on success Rejects --skip-install + --run dev / --run dev-remote up front since both modes require installed dependencies. --deploy is still allowed because it runs server-side and does not need a local node_modules. ## Changes <!-- Brief summary of your changes that is easy to understand --> ## 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> Co-authored-by: Fabian Jakobs <fabian.jakobs@databricks.com>
1 parent 33a489c commit 30db14f

7 files changed

Lines changed: 124 additions & 7 deletions

File tree

cmd/apps/init.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func newInitCmd() *cobra.Command {
7878
run string
7979
setValues []string
8080
autoApprove bool
81+
skipInstall bool
8182
)
8283

8384
cmd := &cobra.Command{
@@ -162,6 +163,7 @@ Environment variables:
162163
pluginsChanged: cmd.Flags().Changed("features") || cmd.Flags().Changed("plugins"),
163164
setValues: setValues,
164165
autoApprove: autoApprove,
166+
skipInstall: skipInstall,
165167
})
166168
},
167169
}
@@ -181,6 +183,7 @@ Environment variables:
181183
cmd.Flags().BoolVar(&deploy, "deploy", false, "Deploy the app after creation")
182184
cmd.Flags().StringVar(&run, "run", "", "Run the app after creation (none, dev, dev-remote)")
183185
cmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "Skip confirmation prompts for optional resources. Optional resources are only configured when their values are provided via --set.")
186+
cmd.Flags().BoolVar(&skipInstall, "skip-install", false, "Skip installing project dependencies (e.g. npm install / uv sync). Cannot be combined with --run.")
184187

185188
return cmd
186189
}
@@ -202,6 +205,7 @@ type createOptions struct {
202205
pluginsChanged bool // true if --plugins flag was explicitly set
203206
setValues []string // --set plugin.resourceKey.field=value pairs
204207
autoApprove bool
208+
skipInstall bool
205209
}
206210

207211
// parseSetValues parses --set key=value pairs into the resourceValues map.
@@ -796,6 +800,13 @@ func awaitBackgroundNpmInstall(ctx context.Context, ch <-chan error) error {
796800
}
797801

798802
func runCreate(ctx context.Context, opts createOptions) error {
803+
// --skip-install leaves the project without installed dependencies, so
804+
// downstream `--run dev` / `--run dev-remote` would immediately fail.
805+
// Reject the combination up front rather than after the scaffold runs.
806+
if opts.skipInstall && opts.run != "" && opts.run != "none" {
807+
return errors.New("--skip-install cannot be combined with --run (dev/dev-remote require dependencies to be installed)")
808+
}
809+
799810
var selectedPlugins []string
800811
var resourceValues map[string]string
801812
var shouldDeploy bool
@@ -983,8 +994,12 @@ func runCreate(ctx context.Context, opts createOptions) error {
983994

984995
// Start npm install in the background so it runs while the user answers prompts.
985996
// This is a Node.js-only optimisation — non-Node templates skip this.
997+
// Honour --skip-install by not kicking off the background install at all.
986998
srcProjectDir := findProjectSrcDir(templateDir)
987-
npmInstallCh := startBackgroundNpmInstall(ctx, srcProjectDir, destDir, opts.name)
999+
var npmInstallCh <-chan error
1000+
if !opts.skipInstall {
1001+
npmInstallCh = startBackgroundNpmInstall(ctx, srcProjectDir, destDir, opts.name)
1002+
}
9881003

9891004
// Step 3: Load manifest from template (optional — templates without it skip plugin/resource logic)
9901005
var m *manifest.Manifest
@@ -1223,17 +1238,23 @@ func runCreate(ctx context.Context, opts createOptions) error {
12231238
// Initialize project based on type (Node.js, Python, etc.).
12241239
// For Node.js, if the background install succeeded node_modules exists
12251240
// and the initializer skips the redundant install step.
1241+
// With --skip-install we bypass Initialize entirely and instead prepend
1242+
// the install command to NextSteps so the user knows to install first.
12261243
var nextStepsCmd string
12271244
projectInitializer := initializer.GetProjectInitializer(absOutputDir)
12281245
if projectInitializer != nil {
1229-
result := projectInitializer.Initialize(ctx, absOutputDir)
1230-
if !result.Success {
1231-
if result.Error != nil {
1232-
return fmt.Errorf("%s: %w", result.Message, result.Error)
1246+
if opts.skipInstall {
1247+
nextStepsCmd = prependInstall(projectInitializer.InstallCommand(), projectInitializer.NextSteps())
1248+
} else {
1249+
result := projectInitializer.Initialize(ctx, absOutputDir)
1250+
if !result.Success {
1251+
if result.Error != nil {
1252+
return fmt.Errorf("%s: %w", result.Message, result.Error)
1253+
}
1254+
return errors.New(result.Message)
12331255
}
1234-
return errors.New(result.Message)
1256+
nextStepsCmd = projectInitializer.NextSteps()
12351257
}
1236-
nextStepsCmd = projectInitializer.NextSteps()
12371258
}
12381259

12391260
// Validate dev-remote is only supported for appkit projects
@@ -1355,6 +1376,18 @@ func runPostCreateDev(ctx context.Context, mode prompt.RunMode, projectInit init
13551376
}
13561377
}
13571378

1379+
// prependInstall composes the install command and the project's NextSteps
1380+
// suggestion into a single shell snippet, dropping either side if empty.
1381+
func prependInstall(installCmd, nextStepsCmd string) string {
1382+
if installCmd == "" {
1383+
return nextStepsCmd
1384+
}
1385+
if nextStepsCmd == "" {
1386+
return installCmd
1387+
}
1388+
return installCmd + " && " + nextStepsCmd
1389+
}
1390+
13581391
// appendUnique appends values to a slice, skipping duplicates.
13591392
func appendUnique(base []string, values ...string) []string {
13601393
seen := make(map[string]bool, len(base))

cmd/apps/init_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,3 +1148,45 @@ func TestRunCreate_NameDotAndOutputDirAreMutuallyExclusive(t *testing.T) {
11481148
require.Error(t, err)
11491149
assert.ErrorIs(t, err, prompt.ErrNameDotWithOutputDir)
11501150
}
1151+
1152+
func TestRunCreate_SkipInstallRejectsRun(t *testing.T) {
1153+
ctx := cmdio.MockDiscard(t.Context())
1154+
for _, runMode := range []string{"dev", "dev-remote"} {
1155+
t.Run(runMode, func(t *testing.T) {
1156+
err := runCreate(ctx, createOptions{
1157+
name: "my-app",
1158+
nameProvided: true,
1159+
skipInstall: true,
1160+
run: runMode,
1161+
})
1162+
require.Error(t, err)
1163+
assert.Contains(t, err.Error(), "--skip-install cannot be combined with --run")
1164+
})
1165+
}
1166+
}
1167+
1168+
func TestInitCmd_SkipInstallFlagRegistered(t *testing.T) {
1169+
cmd := newInitCmd()
1170+
flag := cmd.Flags().Lookup("skip-install")
1171+
require.NotNil(t, flag)
1172+
assert.Equal(t, "false", flag.DefValue)
1173+
}
1174+
1175+
func TestPrependInstall(t *testing.T) {
1176+
tests := []struct {
1177+
name string
1178+
install string
1179+
nextSteps string
1180+
want string
1181+
}{
1182+
{"both set", "npm ci", "npm run dev", "npm ci && npm run dev"},
1183+
{"empty install", "", "npm run dev", "npm run dev"},
1184+
{"empty next steps", "npm ci", "", "npm ci"},
1185+
{"both empty", "", "", ""},
1186+
}
1187+
for _, tt := range tests {
1188+
t.Run(tt.name, func(t *testing.T) {
1189+
assert.Equal(t, tt.want, prependInstall(tt.install, tt.nextSteps))
1190+
})
1191+
}
1192+
}

libs/apps/initializer/initializer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ type Initializer interface {
2424
// NextSteps returns the next steps message for this project type.
2525
NextSteps() string
2626

27+
// InstallCommand returns the shell command that installs the project's
28+
// dependencies (and any prerequisites like a virtualenv). It is shown
29+
// to the user when init was run with --skip-install so the user knows
30+
// what to run before NextSteps.
31+
InstallCommand() string
32+
2733
// RunDev starts the local development server.
2834
RunDev(ctx context.Context, workDir string) error
2935

libs/apps/initializer/initializer_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@ func TestNextSteps(t *testing.T) {
8787
assert.Contains(t, pythonPip.NextSteps(), ".venv")
8888
}
8989

90+
func TestInstallCommand(t *testing.T) {
91+
nodejs := &InitializerNodeJs{}
92+
assert.Equal(t, "npm ci", nodejs.InstallCommand())
93+
94+
pythonUv := &InitializerPythonUv{}
95+
assert.Equal(t, "uv sync", pythonUv.InstallCommand())
96+
97+
// pip's install command creates the venv and installs via the full pip
98+
// path so it composes with NextSteps (which activates the venv).
99+
pythonPip := &InitializerPythonPip{}
100+
got := pythonPip.InstallCommand()
101+
assert.Contains(t, got, "venv .venv")
102+
assert.Contains(t, got, "pip install -r requirements.txt")
103+
assert.NotContains(t, got, "activate", "should not activate; NextSteps handles activation")
104+
}
105+
90106
func TestSupportsDevRemote(t *testing.T) {
91107
// Node.js without appkit
92108
nodejs := &InitializerNodeJs{workDir: ""}

libs/apps/initializer/nodejs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ func (i *InitializerNodeJs) NextSteps() string {
5252
return "npm run dev"
5353
}
5454

55+
func (i *InitializerNodeJs) InstallCommand() string {
56+
// Mirrors runNpmInstall — `npm ci` reproduces the lockfile and is what
57+
// the background install in cmd/apps would have run.
58+
return "npm ci"
59+
}
60+
5561
func (i *InitializerNodeJs) RunDev(ctx context.Context, workDir string) error {
5662
cmdio.LogString(ctx, "Starting development server (npm run dev)...")
5763
cmd := exec.CommandContext(ctx, "npm", "run", "dev")

libs/apps/initializer/python_pip.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ func (i *InitializerPythonPip) NextSteps() string {
5151
return "source .venv/bin/activate && python app.py"
5252
}
5353

54+
func (i *InitializerPythonPip) InstallCommand() string {
55+
// Create the venv and install via the full pip path so the suggestion
56+
// composes with NextSteps (which activates the venv) without
57+
// activating twice.
58+
if runtime.GOOS == "windows" {
59+
return "python -m venv .venv && .venv\\Scripts\\pip install -r requirements.txt"
60+
}
61+
return "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt"
62+
}
63+
5464
func (i *InitializerPythonPip) RunDev(ctx context.Context, workDir string) error {
5565
cmd := detectPythonCommand(workDir)
5666
cmdStr := strings.Join(cmd, " ")

libs/apps/initializer/python_uv.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ func (i *InitializerPythonUv) NextSteps() string {
4242
return "uv run python app.py"
4343
}
4444

45+
func (i *InitializerPythonUv) InstallCommand() string {
46+
return "uv sync"
47+
}
48+
4549
func (i *InitializerPythonUv) RunDev(ctx context.Context, workDir string) error {
4650
appCmd := detectPythonCommand(workDir)
4751
cmdStr := "uv run " + strings.Join(appCmd, " ")

0 commit comments

Comments
 (0)