Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,22 @@ Aliases
view, status
```

### `azdo pipelines show [ORGANIZATION/]PROJECT/PIPELINE [flags]`

Show details of a pipeline definition

```
-q, --jq expression Filter JSON output using a jq expression
--json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it.
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
```

Aliases

```
view, status
```

### `azdo pipelines variable-group`

Manage Azure DevOps variable groups
Expand Down
1 change: 1 addition & 0 deletions docs/azdo_pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Manage Azure DevOps pipelines
* [azdo pipelines list](./azdo_pipelines_list.md)
* [azdo pipelines pool](./azdo_pipelines_pool.md)
* [azdo pipelines runs](./azdo_pipelines_runs.md)
* [azdo pipelines show](./azdo_pipelines_show.md)
* [azdo pipelines variable-group](./azdo_pipelines_variable-group.md)

### ALIASES
Expand Down
57 changes: 57 additions & 0 deletions docs/azdo_pipelines_show.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Command `azdo pipelines show`

```
azdo pipelines show [ORGANIZATION/]PROJECT/PIPELINE [flags]
```

Display the details of a single Azure Pipelines definition.

The pipeline may be specified by ID (integer) or name (string).
When the organization segment is omitted the default organization
from configuration is used.


### Options


* `-q`, `--jq` `expression`

Filter JSON output using a jq expression

* `--json` `fields`

Output JSON with the specified fields. Prefix a field with '-' to exclude it.

* `-t`, `--template` `string`

Format JSON output using a Go template; see "azdo help formatting"


### ALIASES

- `view`
- `status`

### JSON Fields

`_links`, `authoredBy`, `createdDate`, `description`, `id`, `name`, `path`, `process`, `quality`, `queue`, `repository`, `revision`, `type`, `url`

### Examples

```bash
# Show a pipeline by ID using the default organization
azdo pipelines show Fabrikam/42

# Show a pipeline by name
azdo pipelines show Fabrikam/My Pipeline

# Show with explicit organization
azdo pipelines show MyOrg/Fabrikam/42

# Export as JSON
azdo pipelines show Fabrikam/42 --json id,name,revision
```

### See also

* [azdo pipelines](./azdo_pipelines.md)
2 changes: 2 additions & 0 deletions internal/cmd/pipelines/pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/list"
"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/pool"
"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs"
"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/show"
"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/variablegroup"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
)
Expand All @@ -19,6 +20,7 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {

cmd.AddCommand(list.NewCmd(ctx))
cmd.AddCommand(runs.NewCmd(ctx))
cmd.AddCommand(show.NewCmd(ctx))
cmd.AddCommand(variablegroup.NewCmd(ctx))
cmd.AddCommand(agent.NewCmd(ctx))
cmd.AddCommand(pool.NewCmd(ctx))
Expand Down
190 changes: 190 additions & 0 deletions internal/cmd/pipelines/show/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package show

import (
_ "embed"
"fmt"
"strconv"

"github.com/MakeNowJust/heredoc/v2"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/build"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
"github.com/spf13/cobra"
"go.uber.org/zap"

"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/template"
"github.com/tmeckel/azdo-cli/internal/types"
)

type showOptions struct {
exporter util.Exporter
scopeArg string
}

//go:embed show.tpl
var showTmpl string

func NewCmd(ctx util.CmdContext) *cobra.Command {
opts := &showOptions{}

cmd := &cobra.Command{
Use: "show [ORGANIZATION/]PROJECT/PIPELINE",
Short: "Show details of a pipeline definition",
Long: heredoc.Doc(`
Display the details of a single Azure Pipelines definition.

The pipeline may be specified by ID (integer) or name (string).
When the organization segment is omitted the default organization
from configuration is used.
`),
Example: heredoc.Doc(`
# Show a pipeline by ID using the default organization
azdo pipelines show Fabrikam/42

# Show a pipeline by name
azdo pipelines show Fabrikam/My Pipeline

# Show with explicit organization
azdo pipelines show MyOrg/Fabrikam/42

# Export as JSON
azdo pipelines show Fabrikam/42 --json id,name,revision
`),
Aliases: []string{"view", "status"},
Args: util.ExactArgs(1, "pipeline target is required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.scopeArg = args[0]
return runShow(ctx, opts)
},
}

util.AddJSONFlags(cmd, &opts.exporter, []string{
"id", "name", "revision", "description", "path", "type", "url", "_links",
"process", "repository", "queue", "authoredBy", "createdDate", "quality",
})

return cmd
}

func runShow(ctx util.CmdContext, opts *showOptions) error {
ios, err := ctx.IOStreams()
if err != nil {
return err
}

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

scope, err := util.ParseProjectTargetWithDefaultOrganization(ctx, opts.scopeArg)
if err != nil {
return util.FlagErrorWrap(err)
}

clientFact := ctx.ClientFactory()

buildClient, err := clientFact.Build(ctx.Context(), scope.Organization)
if err != nil {
return fmt.Errorf("failed to create Build client: %w", err)
}

logger := zap.L().With(
zap.String("organization", scope.Organization),
zap.String("project", scope.Project),
zap.String("pipeline", scope.Targets[0]),
)

raw := scope.Targets[0]
pipelineID, err := strconv.Atoi(raw)
if err == nil && pipelineID <= 0 {
return fmt.Errorf("pipeline id must be greater than zero: %q", raw)
}
if err != nil {
defs, err := buildClient.GetDefinitions(ctx.Context(), build.GetDefinitionsArgs{
Project: types.ToPtr(scope.Project),
Name: types.ToPtr(raw),
})
if err != nil {
return fmt.Errorf("failed to query pipeline definitions: %w", err)
}

if defs == nil || len(defs.Value) == 0 {
return fmt.Errorf("pipeline %q not found", raw)
}

if len(defs.Value) > 1 {
return fmt.Errorf("pipeline %q is ambiguous: %d matches found", raw, len(defs.Value))
}

pipelineID = types.GetValue(defs.Value[0].Id, 0)
if pipelineID <= 0 {
return fmt.Errorf("pipeline %q returned empty id", raw)
}
}

logger.Debug("fetching pipeline definition", zap.Int("pipelineId", pipelineID))

definition, err := buildClient.GetDefinition(ctx.Context(), build.GetDefinitionArgs{
Project: types.ToPtr(scope.Project),
DefinitionId: types.ToPtr(pipelineID),
})
if err != nil {
return fmt.Errorf("failed to fetch pipeline definition: %w", err)
}

ios.StopProgressIndicator()

if opts.exporter != nil {
return opts.exporter.Write(ios, definition)
}

t := template.New(
ios.Out,
ios.TerminalWidth(),
ios.ColorEnabled(),
).
WithTheme(ios.TerminalTheme()).
WithFuncs(map[string]any{
"formatEntity": func(primary, secondary any) string {
first := template.StringOrEmpty(primary)
second := template.StringOrEmpty(secondary)
switch {
case first != "" && second != "":
return fmt.Sprintf("%s (%s)", first, second)
case first != "":
return first
default:
return second
}
},
"identityDisplay": func(id *webapi.IdentityRef) string {
if id == nil {
return ""
}
display := types.GetValue(id.DisplayName, "")
unique := types.GetValue(id.UniqueName, "")
switch {
case display != "" && unique != "":
return fmt.Sprintf("%s (%s)", display, unique)
case display != "":
return display
default:
return unique
}
},
"hasItems": template.HasItems,
"hasText": template.HasText,
"s": template.StringOrEmpty,
"int": func(v *int) string {
if v == nil {
return ""
}
return strconv.Itoa(*v)
},
})

if err := t.Parse(showTmpl); err != nil {
return err
}

return t.ExecuteData(*definition)
}
40 changes: 40 additions & 0 deletions internal/cmd/pipelines/show/show.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{{- if hasText .Url }}
{{bold "url:"}} {{hyperlink (s .Url) (s .Url)}}
{{- end }}
{{- if hasText .Id }}
{{bold "id:"}} {{.Id}}
{{- end }}
{{- if hasText .Name }}
{{bold "name:"}} {{s .Name}}
{{- end }}
{{- if hasText .Revision }}
{{bold "revision:"}} {{.Revision}}
{{- end }}
{{- if hasText .Path }}
{{bold "path:"}} {{s .Path}}
{{- end }}
{{- if hasText .Type }}
{{bold "type:"}} {{s .Type}}
{{- end }}
{{- if .Process }}
{{bold "process:"}} {{s .Process}}
{{- end }}
{{- if and .Repository (hasText (formatEntity .Repository.Name .Repository.Id)) }}
{{bold "repository:"}} {{formatEntity .Repository.Name .Repository.Id}}
{{- end }}
{{- if and .Queue (hasText (formatEntity .Queue.Name .Queue.Id)) }}
{{bold "queue:"}} {{formatEntity .Queue.Name .Queue.Id}}
{{- end }}
{{- if .AuthoredBy }}
{{bold "authored by:"}} {{identityDisplay .AuthoredBy}}
{{- end }}
{{- if .CreatedDate }}
{{bold "created on:"}} {{timeago .CreatedDate.Time}} ({{timefmt "2006-01-02 15:04:05" .CreatedDate.Time}})
{{- end }}
{{- if hasText .Description }}
{{bold "description:"}}
{{markdown (s .Description)}}
{{- end }}
{{- if hasText .Quality }}
{{bold "quality:"}} {{s .Quality}}
{{- end }}
Loading
Loading