From 43148e8b766de7ac0497aa87e229a57374e01870 Mon Sep 17 00:00:00 2001 From: atreyadbrx Date: Thu, 23 Jul 2026 21:53:45 +0000 Subject: [PATCH] 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") + }) +}