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

astronomer / astro-cli / 8768240b-3e4a-4e38-9670-179e44067502

25 Mar 2026 03:43PM UTC coverage: 36.204% (+0.1%) from 36.077%
8768240b-3e4a-4e38-9670-179e44067502

Pull #2043

circleci

jlaneve
fix(ai-84): review cleanup — centralize output flags, align with kubectl conventions

- Extract output.Flags struct with AddFlags/Resolve to eliminate 10
  copy-pasted format-resolution blocks and 30 per-command flag vars
- Rename --format → --output/-o and --output → --template to match
  kubectl/helm/gh conventions (industry standard flag naming)
- Standardize WithFormat parameter ordering (out io.Writer last)
- Remove duplicate GetCurrentContext calls in ListWithFormat functions
- Add cmd-level JSON output tests for deployment, workspace, organization
- Remove all nolint:dupl annotations added by prior commits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pull Request #2043: [AI-84] --json output and --format consistency pass

331 of 584 new or added lines in 13 files covered. (56.68%)

16 existing lines in 1 file now uncovered.

24762 of 68395 relevant lines covered (36.2%)

8.61 hits per line

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

88.15
/cloud/workspace/workspace.go
1
package workspace
2

3
import (
4
        httpContext "context"
5
        "fmt"
6
        "io"
7
        "os"
8
        "strconv"
9

10
        "github.com/pkg/errors"
11

12
        astrocore "github.com/astronomer/astro-cli/astro-client-core"
13
        "github.com/astronomer/astro-cli/config"
14
        "github.com/astronomer/astro-cli/context"
15
        "github.com/astronomer/astro-cli/pkg/ansi"
16
        "github.com/astronomer/astro-cli/pkg/input"
17
        "github.com/astronomer/astro-cli/pkg/output"
18
        "github.com/astronomer/astro-cli/pkg/printutil"
19
)
20

21
var (
22
        errInvalidWorkspaceKey = errors.New("invalid workspace selection")
23
        ErrInvalidName         = errors.New("no name provided for the workspace. Retry with a valid name")
24
        ErrInvalidTokenName    = errors.New("no name provided for the workspace token. Retry with a valid name")
25
        ErrWorkspaceNotFound   = errors.New("no workspace was found for the ID you provided")
26
        ErrNoWorkspaceExists   = errors.New("no workspace was found in your organization")
27
        ErrWrongEnforceInput   = errors.New("the input to the `--enforce-cicd` flag")
28
)
29

30
func newTableOut() *printutil.Table {
2✔
31
        return &printutil.Table{
2✔
32
                Padding:        []int{44, 50},
2✔
33
                DynamicPadding: true,
2✔
34
                Header:         []string{"NAME", "ID"},
2✔
35
                ColorRowCode:   [2]string{"\033[1;32m", "\033[0m"},
2✔
36
        }
2✔
37
}
2✔
38

39
// GetCurrentWorkspace gets the current workspace set in context config
40
// Returns a string representing the current workspace and an error if it doesn't exist
41
func GetCurrentWorkspace() (string, error) {
3✔
42
        c, err := config.GetCurrentContext()
3✔
43
        if err != nil {
4✔
44
                return "", err
1✔
45
        }
1✔
46

47
        if c.Workspace == "" {
3✔
48
                return "", errors.New("current workspace context not set, you can switch to a workspace with \n\tastro workspace switch WORKSPACEID")
1✔
49
        }
1✔
50

51
        return c.Workspace, nil
1✔
52
}
53

54
// ListData returns workspace list data for structured output
55
func ListData(client astrocore.CoreClient) (*WorkspaceList, error) {
3✔
56
        c, err := config.GetCurrentContext()
3✔
57
        if err != nil {
3✔
NEW
58
                return nil, err
×
NEW
59
        }
×
60

61
        ws, err := GetWorkspaces(client)
3✔
62
        if err != nil {
4✔
63
                return nil, err
1✔
64
        }
1✔
65

66
        result := &WorkspaceList{
2✔
67
                Workspaces: make([]WorkspaceInfo, 0, len(ws)),
2✔
68
        }
2✔
69

2✔
70
        for i := range ws {
4✔
71
                isCurrent := c.Workspace == ws[i].Id
2✔
72
                result.Workspaces = append(result.Workspaces, WorkspaceInfo{
2✔
73
                        Name:      ws[i].Name,
2✔
74
                        ID:        ws[i].Id,
2✔
75
                        IsCurrent: isCurrent,
2✔
76
                })
2✔
77
        }
2✔
78

79
        return result, nil
2✔
80
}
81

82
// List all workspaces
83
func List(client astrocore.CoreClient, out io.Writer) error {
2✔
84
        return ListWithFormat(client, output.FormatTable, "", out)
2✔
85
}
2✔
86

87
// ListWithFormat lists workspaces with the specified output format
88
func ListWithFormat(client astrocore.CoreClient, format output.Format, template string, out io.Writer) error {
4✔
89
        // For JSON or template output, use structured data
4✔
90
        if format == output.FormatJSON || format == output.FormatTemplate {
5✔
91
                data, err := ListData(client)
1✔
92
                if err != nil {
1✔
NEW
93
                        return err
×
NEW
94
                }
×
95

96
                printer := output.New(output.Options{
1✔
97
                        Format:   format,
1✔
98
                        Template: template,
1✔
99
                        Out:      out,
1✔
100
                })
1✔
101

1✔
102
                return printer.Print(data)
1✔
103
        }
104

105
        // Default table format
106
        c, err := config.GetCurrentContext()
3✔
107
        if err != nil {
3✔
108
                return err
×
109
        }
×
110

111
        ws, err := GetWorkspaces(client)
3✔
112
        if err != nil {
4✔
113
                return err
1✔
114
        }
1✔
115

116
        tab := newTableOut()
2✔
117
        for i := range ws {
4✔
118
                name := ws[i].Name
2✔
119
                workspace := ws[i].Id
2✔
120

2✔
121
                var color bool
2✔
122

2✔
123
                if c.Workspace == ws[i].Id {
2✔
124
                        color = true
×
125
                } else {
2✔
126
                        color = false
2✔
127
                }
2✔
128
                tab.AddRow([]string{name, workspace}, color)
2✔
129
        }
130

131
        tab.Print(out)
2✔
132

2✔
133
        return nil
2✔
134
}
135

136
var GetWorkspaceSelection = func(client astrocore.CoreClient, out io.Writer) (string, error) {
6✔
137
        tab := printutil.Table{
6✔
138
                Padding:        []int{5, 44, 50},
6✔
139
                DynamicPadding: true,
6✔
140
                Header:         []string{"#", "NAME", "ID"},
6✔
141
                ColorRowCode:   [2]string{"\033[1;32m", "\033[0m"},
6✔
142
        }
6✔
143

6✔
144
        var c config.Context
6✔
145
        c, err := config.GetCurrentContext()
6✔
146
        if err != nil {
7✔
147
                return "", err
1✔
148
        }
1✔
149

150
        ws, err := GetWorkspaces(client)
5✔
151
        if err != nil {
6✔
152
                return "", err
1✔
153
        }
1✔
154

155
        deployMap := map[string]astrocore.Workspace{}
4✔
156
        for i := range ws {
10✔
157
                index := i + 1
6✔
158

6✔
159
                color := c.Workspace == ws[i].Id
6✔
160
                tab.AddRow([]string{strconv.Itoa(index), ws[i].Name, ws[i].Id}, color)
6✔
161

6✔
162
                deployMap[strconv.Itoa(index)] = ws[i]
6✔
163
        }
6✔
164
        tab.Print(out)
4✔
165
        choice := input.Text("\n> ")
4✔
166
        selected, ok := deployMap[choice]
4✔
167
        if !ok {
6✔
168
                return "", errInvalidWorkspaceKey
2✔
169
        }
2✔
170

171
        return selected.Id, nil
2✔
172
}
173

174
func Switch(workspaceNameOrID string, client astrocore.CoreClient, out io.Writer) error {
5✔
175
        var wsID string
5✔
176
        if workspaceNameOrID == "" {
7✔
177
                id, err := GetWorkspaceSelection(client, out)
2✔
178
                if err != nil {
3✔
179
                        return err
1✔
180
                }
1✔
181

182
                wsID = id
1✔
183
        } else {
3✔
184
                ws, err := GetWorkspaces(client)
3✔
185
                if err != nil {
5✔
186
                        return err
2✔
187
                }
2✔
188
                for i := range ws {
3✔
189
                        if ws[i].Name == workspaceNameOrID || ws[i].Id == workspaceNameOrID {
3✔
190
                                wsID = ws[i].Id
1✔
191
                        }
1✔
192
                }
193

194
                if wsID == "" {
1✔
195
                        return errors.Wrap(err, "workspace id/name could not be found")
×
196
                }
×
197
        }
198

199
        c, err := config.GetCurrentContext()
2✔
200
        if err != nil {
2✔
201
                return err
×
202
        }
×
203

204
        err = c.SetContextKey("workspace", wsID)
2✔
205
        if err != nil {
2✔
206
                return err
×
207
        }
×
208

209
        err = c.SetContextKey("last_used_workspace", wsID)
2✔
210
        if err != nil {
2✔
211
                return err
×
212
        }
×
213

214
        err = c.SetOrganizationContext(c.Organization, c.OrganizationProduct)
2✔
215
        if err != nil {
2✔
216
                return err
×
217
        }
×
218

219
        err = config.PrintCurrentCloudContext(out)
2✔
220
        if err != nil {
2✔
221
                return err
×
222
        }
×
223

224
        return nil
2✔
225
}
226

227
func validateEnforceCD(enforceCD string) (bool, error) {
6✔
228
        var enforce bool
6✔
229
        switch {
6✔
230
        case enforceCD == "OFF" || enforceCD == "":
×
231
                enforce = false
×
232
        case enforceCD == "ON":
5✔
233
                enforce = true
5✔
234
        default:
1✔
235
                return false, ErrWrongEnforceInput
1✔
236
        }
237
        return enforce, nil
5✔
238
}
239

240
func Create(name, description, enforceCD string, out io.Writer, client astrocore.CoreClient) error {
5✔
241
        if name == "" {
5✔
242
                return ErrInvalidName
×
243
        }
×
244
        ctx, err := context.GetCurrentContext()
5✔
245
        if err != nil {
6✔
246
                return err
1✔
247
        }
1✔
248
        enforce, err := validateEnforceCD(enforceCD)
4✔
249
        if err != nil {
5✔
250
                return err
1✔
251
        }
1✔
252
        workspaceCreateRequest := astrocore.CreateWorkspaceJSONRequestBody{
3✔
253
                ApiKeyOnlyDeploymentsDefault: &enforce,
3✔
254
                Description:                  &description,
3✔
255
                Name:                         name,
3✔
256
        }
3✔
257
        resp, err := client.CreateWorkspaceWithResponse(httpContext.Background(), ctx.Organization, workspaceCreateRequest)
3✔
258
        if err != nil {
4✔
259
                return err
1✔
260
        }
1✔
261
        err = astrocore.NormalizeAPIError(resp.HTTPResponse, resp.Body)
2✔
262
        if err != nil {
3✔
263
                return err
1✔
264
        }
1✔
265
        fmt.Fprintf(out, "Astro Workspace %s was successfully created\n", name)
1✔
266
        return nil
1✔
267
}
268

269
func Update(id, name, description, enforceCD string, out io.Writer, client astrocore.CoreClient) error {
7✔
270
        ctx, err := context.GetCurrentContext()
7✔
271
        if err != nil {
8✔
272
                return err
1✔
273
        }
1✔
274
        workspaces, err := GetWorkspaces(client)
6✔
275
        if err != nil {
6✔
276
                return err
×
277
        }
×
278
        var workspace astrocore.Workspace
6✔
279
        if id == "" {
8✔
280
                workspace, err = selectWorkspace(workspaces)
2✔
281
                if workspace.Id == "" {
3✔
282
                        return ErrNoWorkspaceExists
1✔
283
                }
1✔
284
                if err != nil {
1✔
285
                        return err
×
286
                }
×
287
        } else {
4✔
288
                for i := range workspaces {
12✔
289
                        if workspaces[i].Id == id {
12✔
290
                                workspace = workspaces[i]
4✔
291
                        }
4✔
292
                }
293
                if workspace.Id == "" {
4✔
294
                        return ErrWorkspaceNotFound
×
295
                }
×
296
        }
297
        workspaceID := workspace.Id
5✔
298

5✔
299
        workspaceUpdateRequest := astrocore.UpdateWorkspaceRequest{}
5✔
300

5✔
301
        if name == "" {
8✔
302
                workspaceUpdateRequest.Name = workspace.Name
3✔
303
        } else {
5✔
304
                workspaceUpdateRequest.Name = name
2✔
305
        }
2✔
306

307
        if description == "" {
8✔
308
                workspaceUpdateRequest.Description = workspace.Description
3✔
309
        } else {
5✔
310
                workspaceUpdateRequest.Description = &description
2✔
311
        }
2✔
312
        if enforceCD == "" {
8✔
313
                workspaceUpdateRequest.ApiKeyOnlyDeploymentsDefault = workspace.ApiKeyOnlyDeploymentsDefault
3✔
314
        } else {
5✔
315
                enforce, err := validateEnforceCD(enforceCD)
2✔
316
                if err != nil {
2✔
317
                        return err
×
318
                }
×
319
                workspaceUpdateRequest.ApiKeyOnlyDeploymentsDefault = enforce
2✔
320
        }
321
        resp, err := client.UpdateWorkspaceWithResponse(httpContext.Background(), ctx.Organization, workspaceID, workspaceUpdateRequest)
5✔
322
        if err != nil {
6✔
323
                return err
1✔
324
        }
1✔
325
        err = astrocore.NormalizeAPIError(resp.HTTPResponse, resp.Body)
4✔
326
        if err != nil {
5✔
327
                return err
1✔
328
        }
1✔
329
        fmt.Fprintf(out, "Astro Workspace %s was successfully updated\n", workspace.Name)
3✔
330
        return nil
3✔
331
}
332

333
func Delete(id string, out io.Writer, client astrocore.CoreClient) error {
6✔
334
        ctx, err := context.GetCurrentContext()
6✔
335
        if err != nil {
7✔
336
                return err
1✔
337
        }
1✔
338
        workspaces, err := GetWorkspaces(client)
5✔
339
        if err != nil {
5✔
340
                return err
×
341
        }
×
342
        var workspace astrocore.Workspace
5✔
343
        if id == "" {
7✔
344
                workspace, err = selectWorkspace(workspaces)
2✔
345
                if workspace.Id == "" {
3✔
346
                        return ErrNoWorkspaceExists
1✔
347
                }
1✔
348
                if err != nil {
1✔
349
                        return err
×
350
                }
×
351
        } else {
3✔
352
                for i := range workspaces {
6✔
353
                        if workspaces[i].Id == id {
6✔
354
                                workspace = workspaces[i]
3✔
355
                        }
3✔
356
                }
357
                if workspace.Id == "" {
3✔
358
                        return ErrWorkspaceNotFound
×
359
                }
×
360
        }
361
        workspaceID := workspace.Id
4✔
362
        resp, err := client.DeleteWorkspaceWithResponse(httpContext.Background(), ctx.Organization, workspaceID)
4✔
363
        if err != nil {
5✔
364
                return err
1✔
365
        }
1✔
366
        err = astrocore.NormalizeAPIError(resp.HTTPResponse, resp.Body)
3✔
367
        if err != nil {
4✔
368
                return err
1✔
369
        }
1✔
370
        fmt.Fprintf(out, "Astro Workspace %s was successfully deleted\n", workspace.Name)
2✔
371
        return nil
2✔
372
}
373

374
func selectWorkspace(workspaces []astrocore.Workspace) (astrocore.Workspace, error) {
4✔
375
        if len(workspaces) == 0 {
6✔
376
                return astrocore.Workspace{}, nil
2✔
377
        }
2✔
378

379
        if len(workspaces) == 1 {
3✔
380
                fmt.Println("Only one Workspace was found. Using the following Workspace by default: \n" +
1✔
381
                        fmt.Sprintf("\n Workspace Name: %s", ansi.Bold(workspaces[0].Name)) +
1✔
382
                        fmt.Sprintf("\n Workspace ID: %s\n", ansi.Bold(workspaces[0].Id)))
1✔
383

1✔
384
                return workspaces[0], nil
1✔
385
        }
1✔
386

387
        table := printutil.Table{
1✔
388
                Padding:        []int{30, 50, 10, 50, 10, 10, 10},
1✔
389
                DynamicPadding: true,
1✔
390
                Header:         []string{"#", "WORKSPACENAME", "ID", "CICD ENFORCEMENT"},
1✔
391
        }
1✔
392

1✔
393
        fmt.Println("\nPlease select the workspace you would like to update:")
1✔
394

1✔
395
        workspaceMap := map[string]astrocore.Workspace{}
1✔
396
        for i := range workspaces {
3✔
397
                index := i + 1
2✔
398
                table.AddRow([]string{
2✔
399
                        strconv.Itoa(index),
2✔
400
                        workspaces[i].Name,
2✔
401
                        workspaces[i].Id,
2✔
402
                        strconv.FormatBool(workspaces[i].ApiKeyOnlyDeploymentsDefault),
2✔
403
                }, false)
2✔
404
                workspaceMap[strconv.Itoa(index)] = workspaces[i]
2✔
405
        }
2✔
406

407
        table.Print(os.Stdout)
1✔
408
        choice := input.Text("\n> ")
1✔
409
        selected, ok := workspaceMap[choice]
1✔
410
        if !ok {
1✔
411
                return astrocore.Workspace{}, errInvalidWorkspaceKey
×
412
        }
×
413
        return selected, nil
1✔
414
}
415

416
func GetWorkspaces(client astrocore.CoreClient) ([]astrocore.Workspace, error) {
25✔
417
        ctx, err := context.GetCurrentContext()
25✔
418
        if err != nil {
26✔
419
                return []astrocore.Workspace{}, err
1✔
420
        }
1✔
421

422
        sorts := []astrocore.ListWorkspacesParamsSorts{"name:asc"}
24✔
423
        limit := 1000
24✔
424
        workspaceListParams := &astrocore.ListWorkspacesParams{
24✔
425
                Limit: &limit,
24✔
426
                Sorts: &sorts,
24✔
427
        }
24✔
428

24✔
429
        resp, err := client.ListWorkspacesWithResponse(httpContext.Background(), ctx.Organization, workspaceListParams)
24✔
430
        if err != nil {
28✔
431
                return []astrocore.Workspace{}, err
4✔
432
        }
4✔
433
        err = astrocore.NormalizeAPIError(resp.HTTPResponse, resp.Body)
20✔
434
        if err != nil {
20✔
435
                return []astrocore.Workspace{}, err
×
436
        }
×
437

438
        workspaces := resp.JSON200.Workspaces
20✔
439

20✔
440
        return workspaces, nil
20✔
441
}
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