diff --git a/.nextchanges/cli/apps-init-skip-examples.md b/.nextchanges/cli/apps-init-skip-examples.md new file mode 100644 index 00000000000..f32feffb074 --- /dev/null +++ b/.nextchanges/cli/apps-init-skip-examples.md @@ -0,0 +1 @@ +* Add `--skip-examples` to `databricks apps init` to leave out the template's example files (sample SQL queries and the example test). ([#7591](https://github.com/databricks/cli/pull/7591)) diff --git a/cmd/apps/init.go b/cmd/apps/init.go index 6c02d9879dc..d66c40e1a81 100644 --- a/cmd/apps/init.go +++ b/cmd/apps/init.go @@ -97,6 +97,7 @@ func newInitCmd() *cobra.Command { setValues []string autoApprove bool skipInstall bool + skipExamples bool packageManager string authMode string ) @@ -191,6 +192,7 @@ Environment variables: setValues: setValues, autoApprove: autoApprove, skipInstall: skipInstall, + skipExamples: skipExamples, packageManager: packageManager, packageManagerChanged: cmd.Flags().Changed("package-manager"), authMode: authMode, @@ -215,6 +217,7 @@ Environment variables: 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.") cmd.Flags().StringVar(&authMode, "auth-mode", "", "Default resource auth mode: obo (on behalf of the user) or sp (service principal). With obo, resources that cannot be accessed on behalf of the user use sp. Prompts if not provided in interactive mode, otherwise sp.") cmd.Flags().BoolVar(&skipInstall, "skip-install", false, "Skip installing project dependencies (e.g. npm install / uv sync). Cannot be combined with --run.") + cmd.Flags().BoolVar(&skipExamples, "skip-examples", false, "Leave out the template's example files (sample SQL queries and the example test) that a real app replaces") cmd.Flags().StringVar(&packageManager, "package-manager", "", "Node.js package manager (pnpm, npm; default: inferred from template)") return cmd @@ -243,6 +246,7 @@ type createOptions struct { setValues []string // --set plugin.resourceKey.field=value pairs autoApprove bool skipInstall bool + skipExamples bool packageManager string packageManagerChanged bool authMode string // --auth-mode default for all resources @@ -527,6 +531,9 @@ type templateVars struct { DotEnv dotEnvVars AppEnv string PackageManager string + // SkipExamples is set by --skip-examples. Templates leave their example + // files out when it is true; a missing key (older CLIs) keeps them. + SkipExamples bool // Plugins maps plugin name to its metadata // Missing keys return nil, enabling {{if .plugins.analytics}} conditionals. Plugins map[string]*pluginVar @@ -1807,6 +1814,7 @@ func runCreate(ctx context.Context, opts createOptions) (runErr error) { }, AppEnv: generator.GenerateAppEnv(selectedPluginList, genConfig), PackageManager: selectedManager.Name, + SkipExamples: opts.skipExamples, Plugins: plugins, } @@ -2271,6 +2279,7 @@ func templateData(vars templateVars) map[string]any { "appDescription": vars.AppDescription, "workspaceHost": vars.WorkspaceHost, "packageManager": vars.PackageManager, + "skipExamples": vars.SkipExamples, "bundle": map[string]any{ "variables": vars.Bundle.Variables, "resources": vars.Bundle.Resources, diff --git a/cmd/apps/init_test.go b/cmd/apps/init_test.go index 013e3a9253a..dfadc30d8cd 100644 --- a/cmd/apps/init_test.go +++ b/cmd/apps/init_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "flag" + "fmt" "io" "io/fs" "os" @@ -273,6 +274,24 @@ func TestPackageManagerTemplateVariable(t *testing.T) { } } +func TestSkipExamplesTemplateVariable(t *testing.T) { + ctx := t.Context() + input := "{{if not .skipExamples}}x{{end}}" + for _, tt := range []struct { + skipExamples bool + expected string + }{ + {false, "x"}, + {true, ""}, + } { + vars := testVars() + vars.SkipExamples = tt.skipExamples + result, err := executeTemplate(ctx, "test.sql", []byte(input), vars) + require.NoError(t, err) + assert.Equal(t, tt.expected, string(result), "skipExamples=%v", tt.skipExamples) + } +} + func TestExecuteTemplateInvalidSyntaxReturnsOriginal(t *testing.T) { ctx := t.Context() vars := templateVars{ProjectName: "my-app"} @@ -1342,6 +1361,61 @@ func TestInitCmd_SkipInstallFlagRegistered(t *testing.T) { assert.Equal(t, "false", flag.DefValue) } +func TestInitCmd_SkipExamplesFlagRegistered(t *testing.T) { + cmd := newInitCmd() + flag := cmd.Flags().Lookup("skip-examples") + require.NotNil(t, flag) + assert.Equal(t, "false", flag.DefValue) +} + +// TestRunCreateSkipExamples scaffolds a template whose example files are +// gated on {{if not .skipExamples}}. With --skip-examples they render empty, +// so they are not written and config/queries/ is removed once it is empty. +func TestRunCreateSkipExamples(t *testing.T) { + src := t.TempDir() + files := map[string]string{ + "appkit.plugins.json": `{"plugins":{}}`, + "README.md": "# {{.projectName}}\n", + "config/queries/hello_world.sql": "{{if not .skipExamples -}}\nSELECT 1\n{{- end}}\n", + "server/server.ts": "export {};\n", + "server/example.test.ts": "{{if not .skipExamples -}}\ntest('x', () => {});\n{{- end}}\n", + } + for name, content := range files { + path := filepath.Join(src, filepath.FromSlash(name)) + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) + } + + for _, skipExamples := range []bool{false, true} { + t.Run(fmt.Sprintf("skipExamples=%v", skipExamples), func(t *testing.T) { + output := t.TempDir() + ctx := cmdctx.SetWorkspaceClient(cmdio.MockDiscard(t.Context()), nil) + err := runCreate(ctx, createOptions{ + templatePath: src, + name: "test-app", + nameProvided: true, + outputDir: output, + skipInstall: true, + skipExamples: skipExamples, + }) + require.NoError(t, err) + + app := filepath.Join(output, "test-app") + assert.FileExists(t, filepath.Join(app, "README.md")) + assert.FileExists(t, filepath.Join(app, "server", "server.ts")) + if skipExamples { + assert.NoDirExists(t, filepath.Join(app, "config")) + assert.NoFileExists(t, filepath.Join(app, "server", "example.test.ts")) + return + } + got, err := os.ReadFile(filepath.Join(app, "config", "queries", "hello_world.sql")) + require.NoError(t, err) + assert.Equal(t, "SELECT 1\n", string(got)) + assert.FileExists(t, filepath.Join(app, "server", "example.test.ts")) + }) + } +} + func TestPrependInstall(t *testing.T) { tests := []struct { name string