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 @@ -369,6 +369,22 @@ Aliases
l, ls
```

#### `azdo pipelines runs show [ORGANIZATION/]PROJECT RUN_ID [flags]`

Show details of a pipeline run

```
-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_runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Manage pipeline runs in an Azure DevOps project.
### Available commands

* [azdo pipelines runs list](./azdo_pipelines_runs_list.md)
* [azdo pipelines runs show](./azdo_pipelines_runs_show.md)

### See also

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

```
azdo pipelines runs show [ORGANIZATION/]PROJECT RUN_ID [flags]
```

Display the details of a single Azure Pipelines run.

Mirrors 'az pipelines runs show'.


### 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

`buildNumber`, `definition`, `finishTime`, `id`, `lastChangedBy`, `parameters`, `priority`, `queue`, `queueTime`, `reason`, `requestedBy`, `requestedFor`, `result`, `retainedByRelease`, `sourceBranch`, `sourceVersion`, `startTime`, `status`, `tags`, `triggerInfo`, `url`

### Examples

```bash
# Show a run by ID using the default organization
azdo pipelines runs show Fabrikam 12345

# Show a run by ID with explicit organization
azdo pipelines runs show MyOrg/Fabrikam 12345

# Export as JSON
azdo pipelines runs show Fabrikam 12345 --json id,buildNumber,status,result
```

### See also

* [azdo pipelines runs](./azdo_pipelines_runs.md)
2 changes: 2 additions & 0 deletions internal/cmd/pipelines/runs/runs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs/list"
"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs/show"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
)

Expand All @@ -15,5 +16,6 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {
}

cmd.AddCommand(list.NewCmd(ctx))
cmd.AddCommand(show.NewCmd(ctx))
return cmd
}
151 changes: 151 additions & 0 deletions internal/cmd/pipelines/runs/show/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package show

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

"github.com/MakeNowJust/heredoc/v2"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/build"

"github.com/spf13/cobra"
"go.uber.org/zap"

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

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 RUN_ID",
Short: "Show details of a pipeline run",
Long: heredoc.Doc(`
Display the details of a single Azure Pipelines run.

Mirrors 'az pipelines runs show'.
`),
Example: heredoc.Doc(`
# Show a run by ID using the default organization
azdo pipelines runs show Fabrikam 12345

# Show a run by ID with explicit organization
azdo pipelines runs show MyOrg/Fabrikam 12345

# Export as JSON
azdo pipelines runs show Fabrikam 12345 --json id,buildNumber,status,result
`),
Aliases: []string{"view", "status"},
Args: util.ExactArgs(2, "project and run id required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.scopeArg = args[0]
rawID := args[1]
return runShow(ctx, opts, rawID)
},
}

util.AddJSONFlags(cmd, &opts.exporter, []string{
"id", "buildNumber", "status", "result", "queueTime", "startTime", "finishTime",
"url", "definition", "queue", "requestedBy", "requestedFor", "lastChangedBy",
"sourceVersion", "sourceBranch", "reason", "priority", "tags", "parameters",
"triggerInfo", "retainedByRelease",
})

return cmd
}

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

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

runID, err := strconv.Atoi(rawID)
if err != nil {
return util.FlagErrorf("invalid run id %q: must be an integer", rawID)
}
if runID <= 0 {
return util.FlagErrorf("invalid run id %q: must be a positive integer", rawID)
}
scope, err := util.ParseProjectScope(ctx, opts.scopeArg)
if err != nil {
return util.FlagErrorWrap(err)
}

zap.L().Debug(
"fetching pipeline run",
zap.String("organization", scope.Organization),
zap.String("project", scope.Project),
zap.Int("runId", runID),
)

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

project := scope.Project
res, err := client.GetBuild(ctx.Context(), build.GetBuildArgs{
Project: &project,
BuildId: &runID,
})
if err != nil {
return fmt.Errorf("GetBuild: %w", err)
}

ios.StopProgressIndicator()

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

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
}
},
"formatDuration": func(start, finish *azuredevops.Time) string {
if start == nil || finish == nil {
return ""
}
return template.FormatDuration(finish.Time.Sub(start.Time))
},
"hasItems": template.HasItems,
"hasText": template.HasText,
"s": template.StringOrEmpty,
})

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

return t.ExecuteData(res)
}
54 changes: 54 additions & 0 deletions internal/cmd/pipelines/runs/show/show.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{{- if hasText .Url }}
{{bold "url:"}} {{hyperlink (s .Url) (s .Url)}}
{{- end }}
{{- if hasText .Id }}
{{bold "id:"}} {{.Id}}
{{- end }}
{{- if hasText .BuildNumber }}
{{bold "build number:"}} {{s .BuildNumber}}
{{- end }}
{{- if hasText .Status }}
{{bold "status:"}} {{s .Status}}
{{- end }}
{{- if and (eq (s .Status) "completed") (hasText .Result) }}
{{bold "result:"}} {{s .Result}}
{{- end }}
{{- if hasText .Reason }}
{{bold "reason:"}} {{s .Reason}}
{{- end }}
{{- if and .Definition (hasText (formatEntity .Definition.Name .Definition.Id)) }}
{{bold "definition:"}} {{formatEntity .Definition.Name .Definition.Id}}
{{- end }}
{{- if and .Queue (hasText (formatEntity .Queue.Name .Queue.Id)) }}
{{bold "queue:"}} {{formatEntity .Queue.Name .Queue.Id}}
{{- end }}
{{- if hasText .SourceBranch }}
{{bold "source branch:"}} {{s .SourceBranch}}
{{- end }}
{{- if hasText .SourceVersion }}
{{bold "source version:"}} {{truncate 8 (s .SourceVersion)}}
{{- end }}
{{- if and .RequestedBy (hasText (formatEntity .RequestedBy.DisplayName .RequestedBy.UniqueName)) }}
{{bold "requested by:"}} {{formatEntity .RequestedBy.DisplayName .RequestedBy.UniqueName}}
{{- end }}
{{- if and .RequestedFor (hasText (formatEntity .RequestedFor.DisplayName .RequestedFor.UniqueName)) }}
{{bold "requested for:"}} {{formatEntity .RequestedFor.DisplayName .RequestedFor.UniqueName}}
{{- end }}
{{- if hasText .Priority }}
{{bold "priority:"}} {{s .Priority}}
{{- end }}
{{- if .QueueTime }}
{{bold "queue time:"}} {{timeago .QueueTime.Time}} ({{timefmt "2006-01-02 15:04:05" .QueueTime.Time}})
{{- end }}
{{- if .StartTime }}
{{bold "start time:"}} {{timeago .StartTime.Time}} ({{timefmt "2006-01-02 15:04:05" .StartTime.Time}})
{{- end }}
{{- if .FinishTime }}
{{bold "finish time:"}} {{timeago .FinishTime.Time}} ({{timefmt "2006-01-02 15:04:05" .FinishTime.Time}})
{{- end }}
{{- if and .StartTime .FinishTime }}
{{bold "duration:"}} {{formatDuration .StartTime .FinishTime}}
{{- end }}
{{- if hasItems .Tags }}
{{bold "tags:"}}{{range $i, $tag := .Tags}}{{if gt $i 0}}; {{end}}{{$tag}}{{end}}
{{- end }}
Loading
Loading