From 8378776d6592a29e60cf93ed173d5a65df78a63c Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Thu, 23 Jul 2026 21:53:45 +0000 Subject: [PATCH 1/4] apps: add git flags to create/update/deploy The apps create/update/deploy commands accept a git repository and git deployment source, but the code generator emits these nested objects as `// TODO: complex arg` so they were only reachable via --json. Add ergonomic top-level flags for the GA git fields: - create/update: --git-url, --git-provider (App.GitRepository) - deploy: --git-branch, --git-tag, --git-commit, --git-source-code-path (AppDeployment.GitSource) The nested SDK pointers stay nil unless a git flag is set, so non-git requests are unchanged. Validation matches the API contract: url and provider must be set together; branch/tag/commit are mutually exclusive; source-code-path requires a ref. Co-authored-by: Isaac --- cmd/workspace/apps/git_flags.go | 113 +++++++++++++++++++++++++++ cmd/workspace/apps/git_flags_test.go | 95 ++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 cmd/workspace/apps/git_flags.go create mode 100644 cmd/workspace/apps/git_flags_test.go diff --git a/cmd/workspace/apps/git_flags.go b/cmd/workspace/apps/git_flags.go new file mode 100644 index 00000000000..fa1305f7727 --- /dev/null +++ b/cmd/workspace/apps/git_flags.go @@ -0,0 +1,113 @@ +package apps + +import ( + "fmt" + + "github.com/databricks/databricks-sdk-go/service/apps" + "github.com/spf13/cobra" +) + +// The apps create/update/deploy commands accept a git repository and a git +// deployment source, but the code generator emits these nested objects as +// `// TODO: complex arg` and only exposes them via --json. These overrides add +// ergonomic top-level flags for the GA git fields so users can point an app at +// a repo and deploy a specific ref without hand-writing JSON. +// +// The SDK models GitRepository and GitSource as optional pointers on the +// request. We leave them nil unless the user sets a git flag; allocating them +// unconditionally would send an empty object on every non-git create/deploy and +// change the request the server sees. + +// gitRepositoryFlags binds --git-url/--git-provider onto an *apps.GitRepository +// pointer field (App.GitRepository), used by both create and update. It returns +// a PreRunE that allocates the struct only when a flag was set. +func gitRepositoryFlags(cmd *cobra.Command, target **apps.GitRepository) func(*cobra.Command, []string) error { + var url, provider string + cmd.Flags().StringVar(&url, "git-url", "", "URL of the Git repository the app deploys from.") + cmd.Flags().StringVar(&provider, "git-provider", "", "Git provider. Case insensitive. Supported values: gitHub, gitHubEnterprise, bitbucketCloud, bitbucketServer, azureDevOpsServices, gitLab, gitLabEnterpriseEdition, awsCodeCommit.") + + return func(cmd *cobra.Command, args []string) error { + urlSet := cmd.Flags().Changed("git-url") + providerSet := cmd.Flags().Changed("git-provider") + if !urlSet && !providerSet { + return nil + } + // The server requires both url and provider together, so fail early + // rather than shipping a half-populated repository it will reject. + if urlSet != providerSet { + return fmt.Errorf("--git-url and --git-provider must be set together") + } + *target = &apps.GitRepository{Url: url, Provider: provider} + return nil + } +} + +// gitSourceFlags binds the deploy-time git source flags onto an *apps.GitSource +// pointer field (AppDeployment.GitSource). It returns a PreRunE that allocates +// the struct only when a flag was set. +func gitSourceFlags(cmd *cobra.Command, target **apps.GitSource) func(*cobra.Command, []string) error { + var branch, tag, commit, sourceCodePath string + cmd.Flags().StringVar(&branch, "git-branch", "", "Git branch to deploy from.") + cmd.Flags().StringVar(&tag, "git-tag", "", "Git tag to deploy from.") + cmd.Flags().StringVar(&commit, "git-commit", "", "Git commit SHA to deploy from.") + cmd.Flags().StringVar(&sourceCodePath, "git-source-code-path", "", "Relative path to the app source code within the Git repository. Defaults to the repository root.") + + // branch, tag, and commit are a proto oneof (a single git reference) — the + // server accepts at most one. + cmd.MarkFlagsMutuallyExclusive("git-branch", "git-tag", "git-commit") + + return func(cmd *cobra.Command, args []string) error { + refSet := cmd.Flags().Changed("git-branch") || + cmd.Flags().Changed("git-tag") || + cmd.Flags().Changed("git-commit") + pathSet := cmd.Flags().Changed("git-source-code-path") + if !refSet && !pathSet { + return nil + } + // A source-code path without a reference has no repository to resolve + // against — the reference is what selects the code to deploy. + if pathSet && !refSet { + return fmt.Errorf("--git-source-code-path requires one of --git-branch, --git-tag, or --git-commit") + } + *target = &apps.GitSource{ + Branch: branch, + Tag: tag, + Commit: commit, + SourceCodePath: sourceCodePath, + } + return nil + } +} + +// chainPreRunE runs fn after any PreRunE already set on the command, preserving +// the generated cmd.PreRunE (e.g. root.MustWorkspaceClient) rather than +// replacing it. +func chainPreRunE(cmd *cobra.Command, fn func(*cobra.Command, []string) error) { + prev := cmd.PreRunE + cmd.PreRunE = func(cmd *cobra.Command, args []string) error { + if prev != nil { + if err := prev(cmd, args); err != nil { + return err + } + } + return fn(cmd, args) + } +} + +func gitCreateOverride(createCmd *cobra.Command, createReq *apps.CreateAppRequest) { + chainPreRunE(createCmd, gitRepositoryFlags(createCmd, &createReq.App.GitRepository)) +} + +func gitUpdateOverride(updateCmd *cobra.Command, updateReq *apps.UpdateAppRequest) { + chainPreRunE(updateCmd, gitRepositoryFlags(updateCmd, &updateReq.App.GitRepository)) +} + +func gitDeployOverride(deployCmd *cobra.Command, deployReq *apps.CreateAppDeploymentRequest) { + chainPreRunE(deployCmd, gitSourceFlags(deployCmd, &deployReq.AppDeployment.GitSource)) +} + +func init() { + createOverrides = append(createOverrides, gitCreateOverride) + updateOverrides = append(updateOverrides, gitUpdateOverride) + deployOverrides = append(deployOverrides, gitDeployOverride) +} diff --git a/cmd/workspace/apps/git_flags_test.go b/cmd/workspace/apps/git_flags_test.go new file mode 100644 index 00000000000..17c78f97cd2 --- /dev/null +++ b/cmd/workspace/apps/git_flags_test.go @@ -0,0 +1,95 @@ +package apps + +import ( + "testing" + + "github.com/databricks/databricks-sdk-go/service/apps" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// runGitRepositoryFlags wires gitRepositoryFlags onto a fresh command, sets the +// given flags, runs the PreRunE, and returns the resulting pointer + error. +func runGitRepositoryFlags(t *testing.T, argv []string) (*apps.GitRepository, error) { + t.Helper() + cmd := &cobra.Command{} + var target *apps.GitRepository + pre := gitRepositoryFlags(cmd, &target) + require.NoError(t, cmd.ParseFlags(argv)) + return target, pre(cmd, nil) +} + +func runGitSourceFlags(t *testing.T, argv []string) (*apps.GitSource, error) { + t.Helper() + cmd := &cobra.Command{} + var target *apps.GitSource + pre := gitSourceFlags(cmd, &target) + require.NoError(t, cmd.ParseFlags(argv)) + return target, pre(cmd, nil) +} + +func TestGitRepositoryFlags(t *testing.T) { + t.Run("no flags leaves target nil", func(t *testing.T) { + target, err := runGitRepositoryFlags(t, nil) + require.NoError(t, err) + assert.Nil(t, target) + }) + + t.Run("url and provider populate the struct", func(t *testing.T) { + target, err := runGitRepositoryFlags(t, []string{ + "--git-url", "https://github.com/databricks/git_app_repo.git", + "--git-provider", "gitHub", + }) + require.NoError(t, err) + require.NotNil(t, target) + assert.Equal(t, "https://github.com/databricks/git_app_repo.git", target.Url) + assert.Equal(t, "gitHub", target.Provider) + }) + + t.Run("url without provider errors", func(t *testing.T) { + _, err := runGitRepositoryFlags(t, []string{"--git-url", "https://github.com/x/y"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "must be set together") + }) + + t.Run("provider without url errors", func(t *testing.T) { + _, err := runGitRepositoryFlags(t, []string{"--git-provider", "gitHub"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "must be set together") + }) +} + +func TestGitSourceFlags(t *testing.T) { + t.Run("no flags leaves target nil", func(t *testing.T) { + target, err := runGitSourceFlags(t, nil) + require.NoError(t, err) + assert.Nil(t, target) + }) + + t.Run("branch populates the struct", func(t *testing.T) { + target, err := runGitSourceFlags(t, []string{"--git-branch", "main"}) + require.NoError(t, err) + require.NotNil(t, target) + assert.Equal(t, "main", target.Branch) + assert.Empty(t, target.Tag) + assert.Empty(t, target.Commit) + }) + + t.Run("commit with source-code-path populates both", func(t *testing.T) { + target, err := runGitSourceFlags(t, []string{ + "--git-commit", "abc123", + "--git-source-code-path", "my-app", + }) + require.NoError(t, err) + require.NotNil(t, target) + assert.Equal(t, "abc123", target.Commit) + assert.Equal(t, "my-app", target.SourceCodePath) + }) + + t.Run("source-code-path without a ref errors", func(t *testing.T) { + _, err := runGitSourceFlags(t, []string{"--git-source-code-path", "my-app"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "requires one of") + }) +} From f526e5e8cb6b8bb2a434b0fc72637a50d8c9aea8 Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Tue, 18 Aug 2026 00:17:38 +0000 Subject: [PATCH 2/4] apps: use errors.New for static error strings golangci-lint's perfsprint linter flags fmt.Errorf with a static string and no format verbs. Both git flag validation errors are constant strings, so switch them to errors.New. Co-authored-by: Isaac --- cmd/workspace/apps/git_flags.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/workspace/apps/git_flags.go b/cmd/workspace/apps/git_flags.go index fa1305f7727..df93b890287 100644 --- a/cmd/workspace/apps/git_flags.go +++ b/cmd/workspace/apps/git_flags.go @@ -1,7 +1,7 @@ package apps import ( - "fmt" + "errors" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/spf13/cobra" @@ -35,7 +35,7 @@ func gitRepositoryFlags(cmd *cobra.Command, target **apps.GitRepository) func(*c // The server requires both url and provider together, so fail early // rather than shipping a half-populated repository it will reject. if urlSet != providerSet { - return fmt.Errorf("--git-url and --git-provider must be set together") + return errors.New("--git-url and --git-provider must be set together") } *target = &apps.GitRepository{Url: url, Provider: provider} return nil @@ -67,7 +67,7 @@ func gitSourceFlags(cmd *cobra.Command, target **apps.GitSource) func(*cobra.Com // A source-code path without a reference has no repository to resolve // against — the reference is what selects the code to deploy. if pathSet && !refSet { - return fmt.Errorf("--git-source-code-path requires one of --git-branch, --git-tag, or --git-commit") + return errors.New("--git-source-code-path requires one of --git-branch, --git-tag, or --git-commit") } *target = &apps.GitSource{ Branch: branch, From 2ef32c065216c8634fa1e4486d87c2b32170efd8 Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Mon, 24 Aug 2026 17:24:01 +0000 Subject: [PATCH 3/4] apps: reject workspace source-code-path combined with git flags deployment_source is a proto oneof: an app deployment draws its code from either a workspace path (--source-code-path) or a Git source, not both. Reject the combination in the deploy git-source PreRunE so we don't build a request the server and bundle validation treat as invalid. Addresses review feedback from @atilafassina on #6182. Co-authored-by: Isaac --- cmd/workspace/apps/git_flags.go | 7 +++++++ cmd/workspace/apps/git_flags_test.go | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/cmd/workspace/apps/git_flags.go b/cmd/workspace/apps/git_flags.go index df93b890287..48a3b9239fe 100644 --- a/cmd/workspace/apps/git_flags.go +++ b/cmd/workspace/apps/git_flags.go @@ -64,6 +64,13 @@ func gitSourceFlags(cmd *cobra.Command, target **apps.GitSource) func(*cobra.Com if !refSet && !pathSet { return nil } + // deployment_source is a proto oneof: a deployment draws its code from + // either a workspace path (--source-code-path) or a Git source, never + // both. Reject the combination rather than shipping a request the server + // and bundle validation treat as invalid. + if cmd.Flags().Changed("source-code-path") { + return errors.New("--source-code-path (workspace source) cannot be combined with the --git-* flags; a deployment uses either a workspace path or a Git source") + } // A source-code path without a reference has no repository to resolve // against — the reference is what selects the code to deploy. if pathSet && !refSet { diff --git a/cmd/workspace/apps/git_flags_test.go b/cmd/workspace/apps/git_flags_test.go index 17c78f97cd2..67c3373bf5f 100644 --- a/cmd/workspace/apps/git_flags_test.go +++ b/cmd/workspace/apps/git_flags_test.go @@ -23,6 +23,10 @@ func runGitRepositoryFlags(t *testing.T, argv []string) (*apps.GitRepository, er func runGitSourceFlags(t *testing.T, argv []string) (*apps.GitSource, error) { t.Helper() cmd := &cobra.Command{} + // The generated deploy command registers --source-code-path (the workspace + // source). Register it here too so the workspace-vs-Git mutual-exclusion + // guard can be exercised. + cmd.Flags().String("source-code-path", "", "") var target *apps.GitSource pre := gitSourceFlags(cmd, &target) require.NoError(t, cmd.ParseFlags(argv)) @@ -92,4 +96,19 @@ func TestGitSourceFlags(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "requires one of") }) + + t.Run("workspace source-code-path combined with a git ref errors", func(t *testing.T) { + _, err := runGitSourceFlags(t, []string{ + "--git-branch", "main", + "--source-code-path", "/Workspace/Users/me/app", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot be combined") + }) + + t.Run("workspace source-code-path alone leaves git source nil", func(t *testing.T) { + target, err := runGitSourceFlags(t, []string{"--source-code-path", "/Workspace/Users/me/app"}) + require.NoError(t, err) + assert.Nil(t, target) + }) } From 92eb235ab22b07e45567666afa4d4e16581f5243 Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Mon, 24 Aug 2026 20:15:16 +0000 Subject: [PATCH 4/4] apps: update acceptance golden for new git flags The apps update usage now lists --git-url and --git-provider, so refresh the cmd/workspace/apps acceptance output.txt to match. Co-authored-by: Isaac --- acceptance/cmd/workspace/apps/output.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acceptance/cmd/workspace/apps/output.txt b/acceptance/cmd/workspace/apps/output.txt index 94977c24a2a..defd761c7d1 100644 --- a/acceptance/cmd/workspace/apps/output.txt +++ b/acceptance/cmd/workspace/apps/output.txt @@ -97,6 +97,8 @@ Flags: --compute-size ComputeSize Supported values: [LARGE, MEDIUM, XLARGE] --description string The description of the app. --forward-user-access-token Forward the user's access token to the app. + --git-provider string Git provider. Case insensitive. Supported values: gitHub, gitHubEnterprise, bitbucketCloud, bitbucketServer, azureDevOpsServices, gitLab, gitLabEnterpriseEdition, awsCodeCommit. + --git-url string URL of the Git repository the app deploys from. -h, --help help for update --json JSON either inline JSON string or @path/to/file.json with request body (default JSON (0 bytes)) --source-code-path string