Skip to content

Commit 50d10d0

Browse files
authored
feat: ✨Implement azdo pipelines list command Fixes (#295)
2 parents 6fbb38d + 151af54 commit 50d10d0

12 files changed

Lines changed: 941 additions & 12 deletions

File tree

docs/azdo_help_reference.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,29 @@ Aliases
263263
view, status
264264
```
265265

266+
### `azdo pipelines list [ORGANIZATION/]PROJECT [flags]`
267+
268+
List pipeline definitions
269+
270+
```
271+
--folder-path string Filter by folder path (e.g. "user1/production")
272+
-q, --jq expression Filter JSON output using a jq expression
273+
--json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it.
274+
--max-items int Optional client-side cap on results
275+
--name string Filter by pipeline name (prefix or exact)
276+
--query-order string Order of definitions: {none|definitionNameAscending|definitionNameDescending|lastModifiedAscending|lastModifiedDescending}
277+
--repository string Filter by repository name or ID
278+
--repository-type string Repository type filter: {tfsgit|github}
279+
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
280+
--top int Maximum number of definitions to return
281+
```
282+
283+
Aliases
284+
285+
```
286+
ls, l
287+
```
288+
266289
### `azdo pipelines pool`
267290

268291
Manage agent pools

docs/azdo_pipelines.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Manage Azure DevOps pipelines
55
### Available commands
66

77
* [azdo pipelines agent](./azdo_pipelines_agent.md)
8+
* [azdo pipelines list](./azdo_pipelines_list.md)
89
* [azdo pipelines pool](./azdo_pipelines_pool.md)
910
* [azdo pipelines variable-group](./azdo_pipelines_variable-group.md)
1011

docs/azdo_pipelines_list.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
## Command `azdo pipelines list`
2+
3+
```
4+
azdo pipelines list [ORGANIZATION/]PROJECT [flags]
5+
```
6+
7+
List pipeline definitions (YAML or classic) in a project.
8+
9+
10+
### Options
11+
12+
13+
* `--folder-path` `string`
14+
15+
Filter by folder path (e.g. "user1/production")
16+
17+
* `-q`, `--jq` `expression`
18+
19+
Filter JSON output using a jq expression
20+
21+
* `--json` `fields`
22+
23+
Output JSON with the specified fields. Prefix a field with '-' to exclude it.
24+
25+
* `--max-items` `int` (default `0`)
26+
27+
Optional client-side cap on results
28+
29+
* `--name` `string`
30+
31+
Filter by pipeline name (prefix or exact)
32+
33+
* `--query-order` `string`
34+
35+
Order of definitions: {none|definitionNameAscending|definitionNameDescending|lastModifiedAscending|lastModifiedDescending}
36+
37+
* `--repository` `string`
38+
39+
Filter by repository name or ID
40+
41+
* `--repository-type` `string`
42+
43+
Repository type filter: {tfsgit|github}
44+
45+
* `-t`, `--template` `string`
46+
47+
Format JSON output using a Go template; see "azdo help formatting"
48+
49+
* `--top` `int` (default `0`)
50+
51+
Maximum number of definitions to return
52+
53+
54+
### ALIASES
55+
56+
- `ls`
57+
- `l`
58+
59+
### JSON Fields
60+
61+
`_links`, `authoredBy`, `createdDate`, `draftOf`, `drafts`, `id`, `latestBuild`, `latestCompletedBuild`, `metrics`, `name`, `path`, `project`, `quality`, `queue`, `queueStatus`, `revision`, `type`, `uri`, `url`
62+
63+
### Examples
64+
65+
```bash
66+
# List all pipelines in a project
67+
$ azdo pipelines list "my-project"
68+
69+
# List pipelines with a specific name
70+
$ azdo pipelines list "my-project" --name "my-pipeline"
71+
72+
# List pipelines using a specific repository
73+
$ azdo pipelines list "my-project" --repository "my-repo"
74+
75+
# Output as JSON
76+
$ azdo pipelines list "my-project" --json
77+
```
78+
79+
### See also
80+
81+
* [azdo pipelines](./azdo_pipelines.md)

internal/cmd/config/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ func NewCmdConfig(ctx util.CmdContext) *cobra.Command {
1717
longDoc.WriteString("Display or change configuration settings for azdo.\n\n")
1818
longDoc.WriteString("Current respected settings:\n")
1919
for _, co := range config.Options() {
20-
longDoc.WriteString(fmt.Sprintf("- %s: %s", co.Key, co.Description))
20+
fmt.Fprintf(&longDoc, "- %s: %s", co.Key, co.Description)
2121
if co.DefaultValue != "" {
22-
longDoc.WriteString(fmt.Sprintf(" (default: %q)", co.DefaultValue))
22+
fmt.Fprintf(&longDoc, " (default: %q)", co.DefaultValue)
2323
}
2424
longDoc.WriteRune('\n')
2525
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package list
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"github.com/MakeNowJust/heredoc/v2"
8+
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/build"
9+
"github.com/spf13/cobra"
10+
"github.com/tmeckel/azdo-cli/internal/cmd/util"
11+
"github.com/tmeckel/azdo-cli/internal/types"
12+
)
13+
14+
type opts struct {
15+
scope string
16+
name string
17+
repository string
18+
repositoryType string
19+
top int
20+
folderPath string
21+
queryOrder string
22+
maxItems int
23+
exporter util.Exporter
24+
}
25+
26+
func NewCmd(ctx util.CmdContext) *cobra.Command {
27+
opts := &opts{}
28+
29+
cmd := &cobra.Command{
30+
Use: "list [ORGANIZATION/]PROJECT",
31+
Short: "List pipeline definitions",
32+
Long: heredoc.Doc(`
33+
List pipeline definitions (YAML or classic) in a project.
34+
`),
35+
Example: heredoc.Doc(`
36+
# List all pipelines in a project
37+
$ azdo pipelines list "my-project"
38+
39+
# List pipelines with a specific name
40+
$ azdo pipelines list "my-project" --name "my-pipeline"
41+
42+
# List pipelines using a specific repository
43+
$ azdo pipelines list "my-project" --repository "my-repo"
44+
45+
# Output as JSON
46+
$ azdo pipelines list "my-project" --json
47+
`),
48+
Aliases: []string{
49+
"ls",
50+
"l",
51+
},
52+
Args: util.ExactArgs(1, "project argument is required"),
53+
RunE: func(cmd *cobra.Command, args []string) error {
54+
opts.scope = args[0]
55+
return runList(ctx, opts)
56+
},
57+
}
58+
59+
cmd.Flags().StringVar(&opts.name, "name", "", "Filter by pipeline name (prefix or exact)")
60+
cmd.Flags().StringVar(&opts.repository, "repository", "", "Filter by repository name or ID")
61+
util.StringEnumFlag(cmd, &opts.repositoryType, "repository-type", "", "",
62+
[]string{"tfsgit", "github"}, "Repository type filter")
63+
cmd.Flags().IntVar(&opts.top, "top", 0, "Maximum number of definitions to return")
64+
cmd.Flags().StringVar(&opts.folderPath, "folder-path", "", "Filter by folder path (e.g. \"user1/production\")")
65+
util.StringEnumFlag(cmd, &opts.queryOrder, "query-order", "", "",
66+
[]string{"none", "definitionNameAscending", "definitionNameDescending", "lastModifiedAscending", "lastModifiedDescending"},
67+
"Order of definitions")
68+
cmd.Flags().IntVar(&opts.maxItems, "max-items", 0, "Optional client-side cap on results")
69+
util.AddJSONFlags(cmd, &opts.exporter, []string{
70+
"id", "name", "path", "revision", "type", "quality", "queueStatus",
71+
"createdDate", "project", "authoredBy", "latestBuild", "latestCompletedBuild",
72+
"draftOf", "drafts", "metrics", "queue", "uri", "url", "_links",
73+
})
74+
75+
return cmd
76+
}
77+
78+
func runList(cmdCtx util.CmdContext, opts *opts) error {
79+
ios, err := cmdCtx.IOStreams()
80+
if err != nil {
81+
return err
82+
}
83+
ios.StartProgressIndicator()
84+
defer ios.StopProgressIndicator()
85+
86+
if opts.top < 0 {
87+
return util.FlagErrorf("invalid --top value %d; must be greater than 0", opts.top)
88+
}
89+
if opts.maxItems < 0 {
90+
return util.FlagErrorf("invalid --max-items value %d; must be greater than 0", opts.maxItems)
91+
}
92+
93+
scope, err := util.ParseProjectScope(cmdCtx, opts.scope)
94+
if err != nil {
95+
return util.FlagErrorf("invalid project argument: %w", err)
96+
}
97+
98+
if opts.repository != "" && opts.repositoryType == "" {
99+
opts.repositoryType = "tfsgit"
100+
}
101+
102+
buildClient, err := cmdCtx.ClientFactory().Build(cmdCtx.Context(), scope.Organization)
103+
if err != nil {
104+
return err
105+
}
106+
107+
var definitions []build.BuildDefinitionReference
108+
var continuationToken *string
109+
110+
for {
111+
args := build.GetDefinitionsArgs{
112+
Project: types.ToPtr(scope.Project),
113+
Name: types.NotZeroPtrOrNil(opts.name),
114+
RepositoryId: types.NotZeroPtrOrNil(opts.repository),
115+
RepositoryType: types.NotZeroPtrOrNil(opts.repositoryType),
116+
Top: types.PositivePtrOrNil(opts.top),
117+
Path: types.NotZeroPtrOrNil(opts.folderPath),
118+
ContinuationToken: continuationToken,
119+
}
120+
if opts.queryOrder != "" {
121+
order := build.DefinitionQueryOrder(opts.queryOrder)
122+
args.QueryOrder = &order
123+
}
124+
125+
resp, err := buildClient.GetDefinitions(cmdCtx.Context(), args)
126+
if err != nil {
127+
return err
128+
}
129+
130+
definitions = append(definitions, resp.Value...)
131+
132+
if opts.maxItems > 0 && len(definitions) >= opts.maxItems {
133+
definitions = definitions[:opts.maxItems]
134+
break
135+
}
136+
137+
if resp.ContinuationToken == "" {
138+
break
139+
}
140+
continuationToken = &resp.ContinuationToken
141+
142+
if opts.top > 0 && len(definitions) >= opts.top {
143+
break
144+
}
145+
}
146+
147+
sort.Slice(definitions, func(i, j int) bool {
148+
return types.GetValue(definitions[i].Id, 0) < types.GetValue(definitions[j].Id, 0)
149+
})
150+
151+
ios.StopProgressIndicator()
152+
153+
if opts.exporter != nil {
154+
return opts.exporter.Write(ios, definitions)
155+
}
156+
157+
tp, err := cmdCtx.Printer("table")
158+
if err != nil {
159+
return err
160+
}
161+
162+
hasDraft := false
163+
for _, def := range definitions {
164+
if types.GetValue(def.Quality, "") == "draft" {
165+
hasDraft = true
166+
break
167+
}
168+
}
169+
170+
columns := []string{"ID", "PATH", "NAME"}
171+
if hasDraft {
172+
columns = append(columns, "DRAFT")
173+
}
174+
columns = append(columns, "STATUS", "DEFAULT QUEUE")
175+
tp.AddColumns(columns...)
176+
177+
for _, def := range definitions {
178+
tp.AddField(fmt.Sprintf("%d", types.GetValue(def.Id, 0)))
179+
tp.AddField(types.GetValue(def.Path, ""))
180+
tp.AddField(types.GetValue(def.Name, ""))
181+
if hasDraft {
182+
if types.GetValue(def.Quality, "") == "draft" {
183+
tp.AddField("*")
184+
} else {
185+
tp.AddField("")
186+
}
187+
}
188+
tp.AddField(string(types.GetValue(def.QueueStatus, "")))
189+
qName := ""
190+
if def.Queue != nil {
191+
qName = types.GetValue(def.Queue.Name, "")
192+
}
193+
tp.AddField(qName)
194+
tp.EndRow()
195+
}
196+
197+
return tp.Render()
198+
}

0 commit comments

Comments
 (0)