• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

astronomer / astro-cli / 28970289308

08 Jul 2026 07:35PM UTC coverage: 43.611% (-1.7%) from 45.328%
28970289308

Pull #2198

github

web-flow
Merge c33eb2682 into 44bc04a46
Pull Request #2198: Expose non-DAG bundle deploys and bundle management

376 of 3398 new or added lines in 11 files covered. (11.07%)

17 existing lines in 2 files now uncovered.

25531 of 58542 relevant lines covered (43.61%)

8.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.27
/cmd/cloud/deploy.go
1
package cloud
2

3
import (
4
        "fmt"
5
        "os"
6
        "time"
7

8
        "github.com/pkg/errors"
9
        "github.com/spf13/cobra"
10

11
        cloud "github.com/astronomer/astro-cli/cloud/deploy"
12
        "github.com/astronomer/astro-cli/cmd/utils"
13
        "github.com/astronomer/astro-cli/config"
14
        "github.com/astronomer/astro-cli/pkg/git"
15
        "github.com/astronomer/astro-cli/pkg/util"
16
)
17

18
var (
19
        forceDeploy       bool
20
        forcePrompt       bool
21
        saveDeployConfig  bool
22
        pytest            bool
23
        parse             bool
24
        dags              bool
25
        waitForDeploy     bool
26
        waitTime          time.Duration
27
        image             bool
28
        dagsPath          string
29
        pytestFile        string
30
        envFile           string
31
        imageName         string
32
        deploymentName    string
33
        deployDescription string
34
        noDagsBaseDir     bool
35
        dagBundleName     string
36
        nonDags           bool
37
        nonDagsMountPath  string
38
        nonDagsBundleType string
39
        nonDagsBundlePath string
40
        deployExample     = `
41
Specify the ID of the Deployment on Astronomer you would like to deploy this project to:
42

43
  $ astro deploy <deployment ID>
44

45
Menu will be presented if you do not specify a deployment ID:
46

47
  $ astro deploy
48
`
49

50
        DeployImage      = cloud.Deploy
51
        EnsureProjectDir = utils.EnsureProjectDir
52
        buildSecrets     = []string{}
53
)
54

55
const (
56
        registryUncommitedChangesMsg = "Project directory has uncommitted changes, use `astro deploy [deployment-id] -f` to force deploy."
57

58
        deployWaitTime = 300 * time.Second
59

60
        imageNameFlag = "image-name"
61
        nonDagsFlag   = "non-dags"
62
)
63

64
func NewDeployCmd() *cobra.Command {
28✔
65
        cmd := &cobra.Command{
28✔
66
                Use:   "deploy DEPLOYMENT-ID",
28✔
67
                Short: "Deploy your project to a Deployment on Astro",
28✔
68
                Long:  "Deploy your project to a Deployment on Astro. This command bundles your project files into a Docker image and pushes that Docker image to Astronomer. In Deployments with Remote Execution enabled, this only updates the Orchestration Plane components (the API Server and Scheduler). For all other components, use `astro remote deploy` instead. It does not include any metadata associated with your local Airflow environment.",
28✔
69
                Args:  cobra.MaximumNArgs(1),
28✔
70
                PreRunE: func(cmd *cobra.Command, args []string) error {
55✔
71
                        if cmd.Flags().Changed(imageNameFlag) || cmd.Flags().Changed(nonDagsFlag) {
41✔
72
                                return nil
14✔
73
                        }
14✔
74
                        return EnsureProjectDir(cmd, args)
13✔
75
                },
76
                RunE:    deploy,
77
                Example: deployExample,
78
        }
79
        cmd.Flags().BoolVarP(&forceDeploy, "force", "f", false, "Force deploy even if project contains errors or uncommitted changes")
28✔
80
        cmd.Flags().BoolVarP(&forcePrompt, "prompt", "p", false, "Force prompt to choose target deployment")
28✔
81
        cmd.Flags().BoolVarP(&saveDeployConfig, "save", "s", false, "Save deployment in config for future deploys")
28✔
82
        cmd.Flags().StringVar(&workspaceID, "workspace-id", "", "Workspace for your Deployment")
28✔
83
        cmd.Flags().BoolVar(&pytest, "pytest", false, "Deploy code to Astro only if the specified Pytests are passed")
28✔
84
        cmd.Flags().StringVarP(&envFile, "env", "e", ".env", "Location of file containing environment variables for Pytests")
28✔
85
        cmd.Flags().StringVarP(&pytestFile, "test", "t", "", "Location of Pytests or specific Pytest file. All Pytest files must be located in the tests directory")
28✔
86
        cmd.Flags().StringVarP(&imageName, imageNameFlag, "i", "", "Name of a custom image to deploy, or image name with custom tag when used with --client")
28✔
87
        cmd.Flags().BoolVarP(&dags, "dags", "d", false, "Push only DAGs to your Astro Deployment")
28✔
88
        cmd.Flags().BoolVar(&noDagsBaseDir, "no-dags-base-dir", false, "Exclude the dags directory prefix from the bundle. Use for Airflow 3.x deployments where sys.path includes the bundle root")
28✔
89
        cmd.Flags().StringVar(&dagBundleName, "dag-bundle-name", "", "Deploy DAGs to a named DAG bundle on the Deployment instead of the default bundle. Requires Airflow 3, and the bundle must already exist on the Deployment")
28✔
90
        cmd.Flags().MarkHidden("dag-bundle-name") //nolint:errcheck
28✔
91
        cmd.Flags().BoolVarP(&image, "image", "", false, "Push only an image to your Astro Deployment. If you have DAG Deploy enabled your DAGs will not be affected.")
28✔
92
        cmd.Flags().StringVar(&dagsPath, "dags-path", "", "If set deploy dags from this path instead of the dags from working directory")
28✔
93
        cmd.Flags().StringVarP(&deploymentName, "deployment-name", "n", "", "Name of the deployment to deploy to")
28✔
94
        cmd.Flags().BoolVar(&parse, "parse", false, "Succeed only if all DAGs in your Astro project parse without errors")
28✔
95
        cmd.Flags().BoolVarP(&waitForDeploy, "wait", "w", false, "Wait for the Deployment to become healthy before ending the command")
28✔
96
        cmd.Flags().DurationVar(&waitTime, "wait-time", deployWaitTime, "Wait time for the Deployment to become healthy before ending the command. Can only be used with --wait=true")
28✔
97
        cmd.Flags().MarkHidden("dags-path") //nolint:errcheck
28✔
98
        cmd.Flags().StringVarP(&deployDescription, "description", "", "", "Add a description for more context on this deploy")
28✔
99
        cmd.Flags().StringSliceVar(&buildSecrets, "build-secrets", []string{}, "Mimics docker build --secret flag. See https://docs.docker.com/build/building/secrets/ for more information. Example input id=mysecret,src=secrets.txt")
28✔
100
        cmd.Flags().Bool("force-upgrade-to-af3", false, "This flag is no longer required for Airflow 2 to Airflow 3 upgrades. Support will be removed in a future release.")
28✔
101
        cmd.Flags().MarkDeprecated("force-upgrade-to-af3", "this flag is no longer required for Airflow 2 to Airflow 3 upgrades. Support will be removed in a future release.") //nolint:errcheck
28✔
102
        cmd.Flags().BoolVar(&nonDags, nonDagsFlag, false, "Deploy a non-DAG bundle from a separate directory, instead of your Astro project. Requires --non-dags-mount-path")
28✔
103
        cmd.Flags().StringVar(&nonDagsMountPath, "non-dags-mount-path", "", "Path to mount the non-DAG bundle in Airflow, for reference by DAGs. Used with --non-dags")
28✔
104
        cmd.Flags().StringVar(&nonDagsBundleType, "non-dags-bundle-type", "none", "Free-form label identifying the kind of non-DAG bundle (e.g. dbt). Any value is accepted. Defaults to \"none\". Used with --non-dags")
28✔
105
        cmd.Flags().StringVar(&nonDagsBundlePath, "non-dags-local-path", "", "Path to the non-DAG bundle to deploy. Default current directory. Used with --non-dags")
28✔
106
        cmd.Flags().MarkHidden(nonDagsFlag)            //nolint:errcheck
28✔
107
        cmd.Flags().MarkHidden("non-dags-mount-path")  //nolint:errcheck
28✔
108
        cmd.Flags().MarkHidden("non-dags-bundle-type") //nolint:errcheck
28✔
109
        cmd.Flags().MarkHidden("non-dags-local-path")  //nolint:errcheck
28✔
110

28✔
111
        annotateDeployFlag(cmd, "image", "image")
28✔
112
        annotateDeployFlag(cmd, imageNameFlag, "image")
28✔
113
        annotateDeployFlag(cmd, "build-secrets", "image")
28✔
114
        annotateDeployFlag(cmd, "dags", "dag")
28✔
115
        annotateDeployFlag(cmd, "no-dags-base-dir", "dag")
28✔
116
        annotateDeployFlag(cmd, "dag-bundle-name", "dag")
28✔
117
        annotateDeployFlag(cmd, "dags-path", "dag")
28✔
118
        annotateDeployFlag(cmd, "pytest", "test")
28✔
119
        annotateDeployFlag(cmd, "test", "test")
28✔
120
        annotateDeployFlag(cmd, "env", "test")
28✔
121
        annotateDeployFlag(cmd, "parse", "test")
28✔
122
        annotateDeployFlag(cmd, nonDagsFlag, "non-dags")
28✔
123
        annotateDeployFlag(cmd, "non-dags-mount-path", "non-dags")
28✔
124
        annotateDeployFlag(cmd, "non-dags-bundle-type", "non-dags")
28✔
125
        annotateDeployFlag(cmd, "non-dags-local-path", "non-dags")
28✔
126
        cmd.SetUsageTemplate(deployFlagsUsageTemplate)
28✔
127
        return cmd
28✔
128
}
129

130
func deployTests(parse, pytest, forceDeploy bool, pytestFile string) string {
11✔
131
        if pytest && pytestFile == "" {
16✔
132
                pytestFile = "all-tests"
5✔
133
        }
5✔
134

135
        if !parse && !pytest && !forceDeploy || parse && !pytest && !forceDeploy || parse && !pytest && forceDeploy {
14✔
136
                pytestFile = "parse"
3✔
137
        }
3✔
138

139
        if parse && pytest {
14✔
140
                pytestFile = "parse-and-all-tests"
3✔
141
        }
3✔
142

143
        return pytestFile
11✔
144
}
145

146
func deploy(cmd *cobra.Command, args []string) error {
26✔
147
        deploymentID = ""
26✔
148

26✔
149
        // Get deploymentId from args, if passed
26✔
150
        if len(args) > 0 {
51✔
151
                deploymentID = args[0]
25✔
152
        }
25✔
153

154
        if cmd.Flags().Changed("wait-time") && !waitForDeploy {
26✔
155
                return errors.New("cannot use --wait-time with --wait=false")
×
156
        }
×
157

158
        if deploymentID == "" || forcePrompt || workspaceID == "" {
52✔
159
                var err error
26✔
160
                workspaceID, err = coalesceWorkspace()
26✔
161
                if err != nil {
26✔
162
                        return errors.Wrap(err, "failed to find a valid workspace")
×
163
                }
×
164
        }
165

166
        if dags && image {
26✔
167
                return errors.New("cannot use both --dags and --image together. Run 'astro deploy' to update both your image and dags")
×
168
        }
×
169

170
        if dagBundleName != "" && image {
26✔
NEW
171
                return errors.New("cannot use --dag-bundle-name with --image; named DAG bundles apply only to deploys that include DAGs")
×
NEW
172
        }
×
173

174
        if cmd.Flags().Changed(imageNameFlag) {
33✔
175
                for _, f := range []string{"dags", "dags-path", "no-dags-base-dir", "pytest", "parse", "build-secrets", "dag-bundle-name"} {
35✔
176
                        if cmd.Flags().Changed(f) {
34✔
177
                                return fmt.Errorf("cannot use --%s with --image-name; --image-name implies an image-only deploy", f)
6✔
178
                        }
6✔
179
                }
180
        }
181

182
        if nonDags {
27✔
183
                return deployNonDagsBundle(cmd, args)
7✔
184
        }
7✔
185

186
        // Save deploymentId in config if specified
187
        if deploymentID != "" && saveDeployConfig {
14✔
188
                err := config.CFG.ProjectDeployment.SetProjectString(deploymentID)
1✔
189
                if err != nil {
1✔
190
                        return nil
×
191
                }
×
192
        }
193

194
        if git.HasUncommittedChanges("") && !forceDeploy {
13✔
195
                fmt.Println(registryUncommitedChangesMsg)
×
196
                return nil
×
197
        }
×
198

199
        // case for astro deploy --dags whose default operation should be not running any tests
200
        if dags && !parse && !pytest {
15✔
201
                pytestFile = ""
2✔
202
        } else {
13✔
203
                pytestFile = deployTests(parse, pytest, forceDeploy, pytestFile)
11✔
204
        }
11✔
205

206
        // Silence Usage as we have now validated command input
207
        cmd.SilenceUsage = true
13✔
208

13✔
209
        BuildSecretString := util.GetbuildSecretString(buildSecrets)
13✔
210

13✔
211
        deployInput := cloud.InputDeploy{
13✔
212
                Path:              config.WorkingPath,
13✔
213
                RuntimeID:         deploymentID,
13✔
214
                WsID:              workspaceID,
13✔
215
                Pytest:            pytestFile,
13✔
216
                EnvFile:           envFile,
13✔
217
                ImageName:         imageName,
13✔
218
                DeploymentName:    deploymentName,
13✔
219
                Prompt:            forcePrompt,
13✔
220
                Dags:              dags,
13✔
221
                NoDagsBaseDir:     noDagsBaseDir,
13✔
222
                Image:             image,
13✔
223
                WaitForStatus:     waitForDeploy,
13✔
224
                WaitTime:          waitTime,
13✔
225
                DagsPath:          dagsPath,
13✔
226
                Description:       deployDescription,
13✔
227
                BuildSecretString: BuildSecretString,
13✔
228
                Force:             forceDeploy,
13✔
229
                DagBundleName:     dagBundleName,
13✔
230
        }
13✔
231

13✔
232
        return DeployImage(deployInput, astroV1Client, astroV1Alpha1Client)
13✔
233
}
234

235
func deployNonDagsBundle(cmd *cobra.Command, args []string) error {
7✔
236
        for _, f := range []string{"dags", "image", imageNameFlag, "dag-bundle-name", "pytest", "parse", "build-secrets", "dags-path", "no-dags-base-dir"} {
62✔
237
                if cmd.Flags().Changed(f) {
56✔
238
                        return fmt.Errorf("cannot use --%s with --non-dags; --non-dags performs a non-DAG bundle deploy", f)
1✔
239
                }
1✔
240
        }
241

242
        if nonDagsMountPath == "" {
7✔
243
                return errors.New("--non-dags-mount-path is required with --non-dags")
1✔
244
        }
1✔
245

246
        if nonDagsBundlePath == "" {
7✔
247
                nonDagsBundlePath = config.WorkingPath
2✔
248
        }
2✔
249

250
        info, err := os.Stat(nonDagsBundlePath)
5✔
251
        if err != nil {
6✔
252
                if os.IsNotExist(err) {
2✔
253
                        return fmt.Errorf("bundle path %s does not exist", nonDagsBundlePath)
1✔
254
                }
1✔
NEW
255
                return fmt.Errorf("failed to access bundle path %s: %w", nonDagsBundlePath, err)
×
256
        }
257
        if !info.IsDir() {
5✔
258
                return fmt.Errorf("bundle path %s is not a directory", nonDagsBundlePath)
1✔
259
        }
1✔
260

261
        withinAstroProject, err := config.IsWithinProjectDir(nonDagsBundlePath)
3✔
262
        if err != nil {
3✔
NEW
263
                return fmt.Errorf("failed to verify bundle path is not within an Astro project: %w", err)
×
NEW
264
        }
×
265
        if withinAstroProject {
4✔
266
                return errors.New("bundle path is within an Astro project. Non-DAG bundles must be a separate directory")
1✔
267
        }
1✔
268

269
        targetDeploymentID, err := resolveDeploymentIDFromArgsFlags(args, workspaceID, deploymentName)
2✔
270
        if err != nil {
2✔
NEW
271
                return err
×
NEW
272
        }
×
273

274
        cmd.SilenceUsage = true
2✔
275

2✔
276
        deployBundleInput := &cloud.DeployBundleInput{
2✔
277
                BundlePath:    nonDagsBundlePath,
2✔
278
                MountPath:     nonDagsMountPath,
2✔
279
                DeploymentID:  targetDeploymentID,
2✔
280
                BundleType:    nonDagsBundleType,
2✔
281
                Description:   deployDescription,
2✔
282
                Wait:          waitForDeploy,
2✔
283
                WaitTime:      waitTime,
2✔
284
                AstroV1Client: astroV1Client,
2✔
285
        }
2✔
286
        return DeployBundle(deployBundleInput)
2✔
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc