Skip to content

feat: Implement azdo pipelines build show command #241

Description

@tmeckel

Sub-issue of #213. Sibling of #211 (list). Hardened spec — do not re-derive decisions. Mirrors internal/cmd/pr/view/view.go and uses Go text templates via internal/template.Template (the same engine used by azdo pr view).

Command Description

Display the details of a single Azure Pipelines build by integer ID. The command fetches the matching Build via the Builds REST 7.1 endpoint and renders it as a Go text template. The build's SourceVersion (commit SHA), Definition, Queue, and RequestedBy are rendered with their respective child types when present.

GET https://dev.azure.com/{organization}/{project}/_apis/build/builds/{buildId}?api-version=7.1

Locked Decisions (do not re-derive)

# Decision Rationale
1 Use the vendored SDK build.Client.GetBuild (not raw HTTP). Mock already generated at internal/mocks/build_client_mock.go:387-402. Consistent with the list sibling (which uses GetBuilds); the SDK is what the entire build subgroup uses.
2 The build is identified by an integer positional ID (e.g. azdo pipelines build show Fabrikam 12345). The --id flag is not offered. Mirrors az pipelines build show ergonomics. The ID is a single integer — clean as a positional.
3 ID is required. Non-numeric or zero / negative values rejected with util.FlagErrorf before the SDK call. Cheap pre-flight validation; REST 4xx is unhelpful.
4 Use the Go text template engine from internal/template/template.go with an //go:embed show.tpl file. Mirror internal/cmd/pr/view/view.go and view.tpl structure: bold, hyperlink, s, timeago, timefmt, markdown, pluck, join, truncate, stripprefix, tablerow, tablerender. The user explicitly requested template rendering like pr/view.
5 Aliases: view, status. Primary name is show. cmd.Use: "show [ORGANIZATION/]PROJECT BUILD_ID", cmd.Aliases: []string{"view", "status"}. Mirrors the pr/view aliasing pattern but with show as the primary.
6 JSON output passes the raw SDK *build.Build to opts.exporter.Write. No view struct. Symmetric with #211.
7 No confirmation prompt. Show is read-only. Show is non-destructive.
8 --raw flag dumps the full SDK build with spew.Dump to stderr for debugging. Mirrors pr/view --raw.
9 No new SDK client, no new helper, no new package beyond internal/cmd/pipelines/build/show. Reuse SDK call from the vendored build package. Mandate: minimal code.
10 Mock for GetBuild is already generated at internal/mocks/build_client_mock.go:387-402. Do not regenerate. Verified.

Command Signature

azdo pipelines build show [ORGANIZATION/]PROJECT BUILD_ID
  • Aliases: view, status
  • cobra.ExactArgs(2)util.ExactArgs(2, "project and build id required").
  • Positional parsing: args[0] → project scope (via util.ParseProjectScope); args[1] → integer BUILD_ID (parsed in RunE).
  • BUILD_ID is the second positional so azdo pipelines build show 12345 is rejected.

Flags

Flag Maps to Notes
--raw (bool) debug dump spew.Dump of full SDK build to stderr
--json / --jq / --template util.AddJSONFlags JSON export of raw SDK build

JSON Output Contract

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",
})

Pass the raw *build.Build returned by the SDK (Decision 6).

Template Output Contract (show.tpl)

The default template renders (see internal/cmd/pr/view/view.tpl for the established pattern):

url:           
id:            
build number:  
status:        (e.g., completed, inProgress)
result:        (e.g., succeeded, failed, canceled) - when status is completed
reason:        (e.g., manual, individualCI, schedule)
definition:    () - when present
queue:         () - when present
source branch: 
source version:  (short SHA, 8 chars when available)
requested by:   () - when present
requested for:  () - when present
priority:      
queue time:     ()
start time:     () - when started
finish time:    () - when finished
duration:        (e.g., "2m 13s") - when started and finished
tags:          ; ; ...  (if non-empty)

Command Wiring

  • Package path: internal/cmd/pipelines/build/show
  • Files:
    • show.goNewCmd(ctx util.CmdContext) *cobra.Command + showOptions + runShow
    • show.tpl — Go text template
    • show_test.go — table-driven gomock tests
  • Update internal/cmd/pipelines/build/build.go to add showcmd "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/build/show" and cmd.AddCommand(showcmd.NewCmd(ctx)). Update the Example block.
  • Existing higher-level parents must already remain wired: pipelinesbuildshow.

API Surface

Reuse the already-vendored client. No new SDK clients required.

  • build.Client.GetBuildBuilds - Get (REST 7.1)
  • build.GetBuildArgs struct: {Project *string, BuildId *int}.
  • Template engine: internal/template.Template with bold, hyperlink, s, timeago, timefmt, markdown, pluck, join, truncate, stripprefix, tablerow, tablerender.

Mock for GetBuild is already generated at internal/mocks/build_client_mock.go:387-402. No mock regeneration needed.

Implementation Approach (TDD, reuse-first, minimal)

Phase 1 — RED (tests first). Mirror setupFakeDeps from internal/cmd/boards/workitem/list/list_test.go:765-844. Add show_test.go with the following table-driven / behaviour tests, all using t.Parallel() and gomock (require for preconditions, assert for verifications):

  • TestNewCmd_RegistersAsShowLeaf — asserts cmd.Name() == "show", cmd.Aliases contains view and status, cmd.Use starts with show [ORGANIZATION/]PROJECT BUILD_ID.
  • TestNewCmd_RequiresTwoArgs — runs with []string{"Fabrikam"}; asserts cobra.ExactArgs(2) triggers "project and build id required".
  • TestRunShow_BuildIDMustBeInteger — sets BUILD_ID "abc"; asserts util.FlagErrorf returned.
  • TestRunShow_BuildIDMustBePositive — sets BUILD_ID 0 and -1; asserts util.FlagErrorf returned.
  • TestRunShow_BasicCall — sets [Fabrikam, 12345]; stubs build.EXPECT().GetBuild(gomock.Any(), gomock.Any()); asserts *args.BuildId == 12345, *args.Project == "Fabrikam".
  • TestRunShow_TemplateOutput_BasicFields — mocks return *Build{Id, BuildNumber, Status, Result, SourceBranch, SourceVersion, Url}; asserts rendered output contains all field labels and values.
  • TestRunShow_TemplateOutput_Hyperlink — asserts the url: line uses ANSI hyperlink escape sequence.
  • TestRunShow_TemplateOutput_DurationFormatted — mocks return StartTime: 2024-01-01T12:00:00Z, FinishTime: 2024-01-01T12:02:13Z; asserts duration: 2m 13s rendered.
  • TestRunShow_TemplateOutput_NoDuration_NotStarted — mocks return FinishTime: nil; asserts duration: line is omitted.
  • TestRunShow_TemplateOutput_Tags — mocks return Tags: []string{"release", "nightly"}; asserts both tags rendered.
  • TestRunShow_TemplateOutput_NoTags — mocks return no tags; asserts tags: line is omitted.
  • TestRunShow_TemplateOutput_DefinitionAndQueue_Nested — mocks return Definition: &BuildDefinition{Name, Id} and Queue: &Pool{Name, Id}; asserts both rendered.
  • TestRunShow_TemplateOutput_ResultOnlyWhenCompleted — mocks return Status: "inProgress", Result: ""; asserts result: line is omitted.
  • TestRunShow_TemplateOutput_ResultShownWhenCompleted — mocks return Status: "completed", Result: "succeeded"; asserts result: succeeded rendered.
  • TestRunShow_JSONOutput — sets --json; mocks return build; asserts JSON contains id, buildNumber, status, result, url, sourceBranch, sourceVersion, definition, queue, requestedBy, tags.
  • TestRunShow_RawFlag — sets --raw; asserts spew.Dump was invoked.
  • TestRunShow_ProjectScopeParsing — table-driven: myorg/Fabrikam, Fabrikam, invalid ("org/proj/extra"), empty.
  • TestRunShow_InvalidProjectScope — asserts util.FlagErrorWrap returned.
  • TestRunShow_ClientFactoryError — stubs factory to return error; asserts wrapped error.
  • TestRunShow_SDKError — stubs SDK to return error; asserts wrapped error.
  • TestRunShow_OrganizationFromConfigDefault — when scopeArg is Fabrikam (no org), asserts clientFact.Build(ctx, defaultOrg) is called with the configured default.

Phase 2 — GREEN (minimal implementation). Strict reuse rules:

  • No new helpers beyond the inline parseBuildID(raw string) (int, error) (~5 lines) for the integer-parse + positive check, and formatDuration(start, finish *time.Time) string (~10 lines) for the human-readable duration.
  • Reuse util.ParseProjectScope, util.AddJSONFlags, util.FlagErrorf/FlagErrorWrap, util.ExactArgs, types.GetValue, types.ToPtr, ios.StartProgressIndicator/StopProgressIndicator, iostreams.Test as-is.
  • Reuse internal/template.New(...).WithFuncs(...).Parse(show.tpl).ExecuteData(data) — exact same pattern as internal/cmd/pr/view/view.go:483-549.
  • Progress indicator: ios.StartProgressIndicator() + defer ios.StopProgressIndicator(); call ios.StopProgressIndicator() immediately before template execution.
  • Output split: JSON via opts.exporter.Write(ios, res) passing the raw SDK *Build; template via template.New(...).ExecuteData(templateData{Build: res}).
  • Debug log at the point of the SDK call: organization, project, buildId.

Target delta: show.go ≤ ~120 LOC, show.tpl ≤ ~50 LOC, show_test.go ≤ ~400 LOC (22 tests), parent build.go +3 LOC, docs/pipelines_build_show.md regenerated via make docs. No changes to list.go or other build siblings.

Tooling and Verification Checklist

  • Run gofmt / gofumpt on touched files
  • go test ./internal/cmd/pipelines/build/...
  • go test ./...
  • make lint
  • make docs

Reference Existing Patterns

  • internal/cmd/pr/view/view.goprimary template-engine reference (Primary: view.go:483-549; viewOptions struct at view.go:23-31; template embed at view.go:33-34).
  • internal/cmd/pr/view/view.tplprimary template-file reference (45 lines; same field-bullet style).
  • internal/cmd/boards/workitem/show/show.go (sibling under feat: Implement azdo boards work-item show command #238) — copy structure for the show flow, raw-SDK JSON output, progress lifecycle.
  • internal/cmd/pipelines/build/list/list.go (sibling under feat: Implement azdo pipelines build list command #211) — closest sibling; reuse JSON/table split pattern, default columns for --format table.
  • internal/cmd/boards/workitem/list/list_test.go:765-844setupFakeDeps / stub* fixture; copy structure.
  • internal/mocks/build_client_mock.go:387-402 — mock for GetBuild (already generated, do not regenerate).
  • internal/azdo/factory.go:61-67ClientFactory().Build(...) accessor (reuse).
  • internal/template/template.go — template engine + funcs (reuse, do not reimplement).

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions