Skip to content

Commit 70c0de7

Browse files
authored
Add --version flag to apps init for version pinning (#4403)
1 parent f2f1ea9 commit 70c0de7

File tree

2 files changed

+114
-9
lines changed

2 files changed

+114
-9
lines changed

cmd/apps/init.go

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,33 @@ import (
2424
)
2525

2626
const (
27-
templatePathEnvVar = "DATABRICKS_APPKIT_TEMPLATE_PATH"
28-
defaultTemplateURL = "https://github.com/databricks/appkit/tree/main/template"
27+
templatePathEnvVar = "DATABRICKS_APPKIT_TEMPLATE_PATH"
28+
appkitRepoURL = "https://github.com/databricks/appkit"
29+
appkitTemplateDir = "template"
30+
appkitDefaultBranch = "main"
2931
)
3032

33+
// normalizeVersion ensures the version string has a "v" prefix if it looks like a semver.
34+
// Examples: "0.3.0" -> "v0.3.0", "v0.3.0" -> "v0.3.0", "latest" -> "main"
35+
func normalizeVersion(version string) string {
36+
if version == "" {
37+
return version
38+
}
39+
if version == "latest" {
40+
return appkitDefaultBranch
41+
}
42+
// If it starts with a digit, prepend "v"
43+
if len(version) > 0 && version[0] >= '0' && version[0] <= '9' {
44+
return "v" + version
45+
}
46+
return version
47+
}
48+
3149
func newInitCmd() *cobra.Command {
3250
var (
3351
templatePath string
3452
branch string
53+
version string
3554
name string
3655
warehouseID string
3756
description string
@@ -51,10 +70,19 @@ When run without arguments, uses the default AppKit template and an interactive
5170
guides you through the setup. When run with --name, runs in non-interactive mode
5271
(all required flags must be provided).
5372
73+
By default, the command uses the latest released version of AppKit. Use --version
74+
to specify a different version, or --version latest to use the main branch.
75+
5476
Examples:
5577
# Interactive mode with default template (recommended)
5678
databricks apps init
5779
80+
# Use a specific AppKit version
81+
databricks apps init --version v0.2.0
82+
83+
# Use the latest development version (main branch)
84+
databricks apps init --version latest
85+
5886
# Non-interactive with flags
5987
databricks apps init --name my-app
6088
@@ -80,9 +108,16 @@ Environment variables:
80108
PreRunE: root.MustWorkspaceClient,
81109
RunE: func(cmd *cobra.Command, args []string) error {
82110
ctx := cmd.Context()
111+
112+
// Validate mutual exclusivity of --branch and --version
113+
if cmd.Flags().Changed("branch") && cmd.Flags().Changed("version") {
114+
return errors.New("--branch and --version are mutually exclusive")
115+
}
116+
83117
return runCreate(ctx, createOptions{
84118
templatePath: templatePath,
85119
branch: branch,
120+
version: version,
86121
name: name,
87122
nameProvided: cmd.Flags().Changed("name"),
88123
warehouseID: warehouseID,
@@ -99,7 +134,8 @@ Environment variables:
99134
}
100135

101136
cmd.Flags().StringVar(&templatePath, "template", "", "Template path (local directory or GitHub URL)")
102-
cmd.Flags().StringVar(&branch, "branch", "", "Git branch or tag (for GitHub templates)")
137+
cmd.Flags().StringVar(&branch, "branch", "", "Git branch or tag (for GitHub templates, mutually exclusive with --version)")
138+
cmd.Flags().StringVar(&version, "version", "", "AppKit version to use (default: latest release, use 'latest' for main branch)")
103139
cmd.Flags().StringVar(&name, "name", "", "Project name (prompts if not provided)")
104140
cmd.Flags().StringVar(&warehouseID, "warehouse-id", "", "SQL warehouse ID")
105141
cmd.Flags().StringVar(&description, "description", "", "App description")
@@ -114,6 +150,7 @@ Environment variables:
114150
type createOptions struct {
115151
templatePath string
116152
branch string
153+
version string
117154
name string
118155
nameProvided bool // true if --name flag was explicitly set (enables "flags mode")
119156
warehouseID string
@@ -377,18 +414,23 @@ func cloneRepo(ctx context.Context, repoURL, branch string) (string, error) {
377414
}
378415

379416
// resolveTemplate resolves a template path, handling both local paths and GitHub URLs.
417+
// branch is used for cloning (can contain "/" for feature branches).
418+
// subdir is an optional subdirectory within the repo to use (for default appkit template).
380419
// Returns the local path to use, a cleanup function (for temp dirs), and any error.
381-
func resolveTemplate(ctx context.Context, templatePath, branch string) (localPath string, cleanup func(), err error) {
420+
func resolveTemplate(ctx context.Context, templatePath, branch, subdir string) (localPath string, cleanup func(), err error) {
382421
// Case 1: Local path - return as-is
383422
if !strings.HasPrefix(templatePath, "https://") {
384423
return templatePath, nil, nil
385424
}
386425

387426
// Case 2: GitHub URL - parse and clone
388-
repoURL, subdir, urlBranch := git.ParseGitHubURL(templatePath)
427+
repoURL, urlSubdir, urlBranch := git.ParseGitHubURL(templatePath)
389428
if branch == "" {
390429
branch = urlBranch // Use branch from URL if not overridden by flag
391430
}
431+
if subdir == "" {
432+
subdir = urlSubdir // Use subdir from URL if not overridden
433+
}
392434

393435
// Clone to temp dir with spinner
394436
var tempDir string
@@ -427,9 +469,22 @@ func runCreate(ctx context.Context, opts createOptions) error {
427469
if templateSrc == "" {
428470
templateSrc = os.Getenv(templatePathEnvVar)
429471
}
430-
if templateSrc == "" {
431-
// Use default template from GitHub
432-
templateSrc = defaultTemplateURL
472+
473+
// Resolve the git reference (branch/tag) to use for default appkit template
474+
gitRef := opts.branch
475+
usingDefaultTemplate := templateSrc == ""
476+
if usingDefaultTemplate {
477+
// Using default appkit template - resolve version
478+
switch {
479+
case opts.branch != "":
480+
// --branch takes precedence (already set in gitRef)
481+
case opts.version != "":
482+
gitRef = normalizeVersion(opts.version)
483+
default:
484+
// Default: use main branch
485+
gitRef = appkitDefaultBranch
486+
}
487+
templateSrc = appkitRepoURL
433488
}
434489

435490
// Step 1: Get project name first (needed before we can check destination)
@@ -465,7 +520,15 @@ func runCreate(ctx context.Context, opts createOptions) error {
465520
}
466521

467522
// Step 2: Resolve template (handles GitHub URLs by cloning)
468-
resolvedPath, cleanup, err := resolveTemplate(ctx, templateSrc, opts.branch)
523+
// For custom templates, --branch can override the URL's branch
524+
// For default appkit template, pass gitRef directly (supports branches with "/" in name)
525+
branchForClone := opts.branch
526+
subdirForClone := ""
527+
if usingDefaultTemplate {
528+
branchForClone = gitRef
529+
subdirForClone = appkitTemplateDir
530+
}
531+
resolvedPath, cleanup, err := resolveTemplate(ctx, templateSrc, branchForClone, subdirForClone)
469532
if err != nil {
470533
return err
471534
}

cmd/apps/init_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package apps
22

33
import (
4+
"errors"
45
"testing"
56

67
"github.com/databricks/cli/libs/apps/prompt"
8+
"github.com/spf13/cobra"
79
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
811
)
912

1013
func TestIsTextFile(t *testing.T) {
@@ -169,6 +172,45 @@ func TestSubstituteVarsNoPlugins(t *testing.T) {
169172
}
170173
}
171174

175+
func TestInitCmdBranchAndVersionMutuallyExclusive(t *testing.T) {
176+
cmd := newInitCmd()
177+
cmd.PreRunE = nil // skip workspace client setup for flag validation test
178+
// Replace RunE to only test flag validation, not the full create flow
179+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
180+
if cmd.Flags().Changed("branch") && cmd.Flags().Changed("version") {
181+
return errors.New("--branch and --version are mutually exclusive")
182+
}
183+
return nil
184+
}
185+
cmd.SetArgs([]string{"--branch", "dev", "--version", "v1.0.0"})
186+
err := cmd.Execute()
187+
require.Error(t, err)
188+
assert.Contains(t, err.Error(), "--branch and --version are mutually exclusive")
189+
}
190+
191+
func TestNormalizeVersion(t *testing.T) {
192+
tests := []struct {
193+
input string
194+
expected string
195+
}{
196+
{"0.3.0", "v0.3.0"},
197+
{"1.0.0", "v1.0.0"},
198+
{"v0.3.0", "v0.3.0"},
199+
{"v1.0.0", "v1.0.0"},
200+
{"latest", "main"},
201+
{"", ""},
202+
{"main", "main"},
203+
{"feat/something", "feat/something"},
204+
}
205+
206+
for _, tt := range tests {
207+
t.Run(tt.input, func(t *testing.T) {
208+
result := normalizeVersion(tt.input)
209+
assert.Equal(t, tt.expected, result)
210+
})
211+
}
212+
}
213+
172214
func TestParseDeployAndRunFlags(t *testing.T) {
173215
tests := []struct {
174216
name string

0 commit comments

Comments
 (0)