You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sub-issue of #214. Sibling of #212 (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).
> Why this is structurally identical to build show (#TBD build show): The Azure DevOps REST API does not differentiate "builds" from "runs" — they are the same Build resource. The runs subgroup exposes the same data through azdo pipelines runs show to match the Azure CLI's user-facing split. Internally both build show and runs show call build.Client.GetBuild. The default table columns and template differ slightly (modern Pipelines API surface), but the implementation structure is identical. The body below is co-written with build show and explicitly references it.
Command Description
Display the details of a single Azure Pipelines run by integer ID. The command fetches the matching Build via the Builds REST 7.1 endpoint (same call as build show) and renders it as a Go text template, with default columns tailored to the modern Pipelines API surface.
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). Same SDK call as build show. Mock shared at internal/mocks/build_client_mock.go:387-402.
The Azure DevOps REST API does not differentiate "builds" from "runs". Both runs show and build show call GetBuild.
2
The run is identified by an integer positional ID (e.g. azdo pipelines runs show Fabrikam 12345). The --id flag is not offered.
Mirrors az pipelines runs 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 RUN_ID", cmd.Aliases: []string{"view", "status"}.
Mirrors the pr/view aliasing pattern but with show as the primary. RUN_ID (instead of BUILD_ID) reflects the user-facing language of the modern Pipelines API.
6
JSON output passes the raw SDK *build.Build to opts.exporter.Write. No view struct.
--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/runs/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 (shared with build show). Do not regenerate.
Verified.
11
Implementation duplication with build show is intentional and minimal. The only divergence is the table column labels (PIPELINE vs DEFINITION) and the RUN_ID positional name in the Use: string. The show.tpl, JSON field list, and SDK call are byte-identical. If future divergence grows, extract a shared internal/cmd/pipelines/build/show helper.
Mirrors the split between build list (#211) and runs list (#212) — both already share GetBuilds SDK call.
Command Signature
azdo pipelines runs show [ORGANIZATION/]PROJECT RUN_ID
Aliases: view, status
cobra.ExactArgs(2) — util.ExactArgs(2, "project and run id required").
show.tpl — Go text template (byte-identical to build/show/show.tpl initially; consider extracting to a shared internal/cmd/pipelines/shared/show.tpl only if divergence appears)
show_test.go — table-driven gomock tests (mirror build/show/show_test.go; only the Use: string assertions and table-column labels differ)
Update internal/cmd/pipelines/runs/runs.go to add showcmd "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs/show" and cmd.AddCommand(showcmd.NewCmd(ctx)). Update the Example block.
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:
TestNewCmd_RegistersAsShowLeaf — asserts cmd.Name() == "show", cmd.Aliases contains view and status, cmd.Use starts with show [ORGANIZATION/]PROJECT RUN_ID.
TestNewCmd_RequiresTwoArgs — runs with []string{"Fabrikam"}; asserts cobra.ExactArgs(2) triggers "project and run id required".
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 parseRunID(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. Consider sharing formatDuration with build/show via internal/cmd/pipelines/build/show/shared if it stabilizes (do not pre-extract).
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, runId.
Target delta: show.go ≤ ~120 LOC, show.tpl ≤ ~50 LOC, show_test.go ≤ ~400 LOC (22 tests), parent runs.go +3 LOC, docs/pipelines_runs_show.md regenerated via make docs. No changes to list.go or other runs siblings.
Tooling and Verification Checklist
Run gofmt / gofumpt on touched files
go test ./internal/cmd/pipelines/runs/...
go test ./...
make lint
make docs
Reference Existing Patterns
internal/cmd/pr/view/view.go — primary 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.tpl — primary template-file reference (45 lines; same field-bullet style).
internal/cmd/pipelines/build/show/show.go (sibling under the build show sub-issue filed in the same wave) — closest sibling; mirror the entire file structure. The only differences are: (a) the Use: string (show [ORGANIZATION/]PROJECT RUN_ID vs BUILD_ID); (b) the table column PIPELINE vs DEFINITION; (c) the parseRunID vs parseBuildID helper name.
Sub-issue of #214. Sibling of #212 (
list). Hardened spec — do not re-derive decisions. Mirrorsinternal/cmd/pr/view/view.goand uses Go text templates viainternal/template.Template(the same engine used byazdo pr view).> Why this is structurally identical to
build show(#TBD build show): The Azure DevOps REST API does not differentiate "builds" from "runs" — they are the sameBuildresource. Therunssubgroup exposes the same data throughazdo pipelines runs showto match the Azure CLI's user-facing split. Internally bothbuild showandruns showcallbuild.Client.GetBuild. The default table columns and template differ slightly (modern Pipelines API surface), but the implementation structure is identical. The body below is co-written withbuild showand explicitly references it.Command Description
Display the details of a single Azure Pipelines run by integer ID. The command fetches the matching
Buildvia the Builds REST 7.1 endpoint (same call asbuild show) and renders it as a Go text template, with default columns tailored to the modern Pipelines API surface.Locked Decisions (do not re-derive)
build.Client.GetBuild(not raw HTTP). Same SDK call asbuild show. Mock shared atinternal/mocks/build_client_mock.go:387-402.runs showandbuild showcallGetBuild.azdo pipelines runs show Fabrikam 12345). The--idflag is not offered.az pipelines runs showergonomics. The ID is a single integer — clean as a positional.util.FlagErrorfbefore the SDK call.internal/template/template.gowith an//go:embed show.tplfile. Mirrorinternal/cmd/pr/view/view.goandview.tplstructure:bold,hyperlink,s,timeago,timefmt,markdown,pluck,join,truncate,stripprefix,tablerow,tablerender.pr/view.view,status. Primary name isshow.cmd.Use: "show [ORGANIZATION/]PROJECT RUN_ID",cmd.Aliases: []string{"view", "status"}.pr/viewaliasing pattern but withshowas the primary.RUN_ID(instead ofBUILD_ID) reflects the user-facing language of the modern Pipelines API.*build.Buildtoopts.exporter.Write. No view struct.--rawflag dumps the full SDK build withspew.Dumpto stderr for debugging.pr/view --raw.internal/cmd/pipelines/runs/show. Reuse SDK call from the vendoredbuildpackage.GetBuildis already generated atinternal/mocks/build_client_mock.go:387-402(shared withbuild show). Do not regenerate.build showis intentional and minimal. The only divergence is the table column labels (PIPELINEvsDEFINITION) and theRUN_IDpositional name in theUse:string. Theshow.tpl, JSON field list, and SDK call are byte-identical. If future divergence grows, extract a sharedinternal/cmd/pipelines/build/showhelper.build list(#211) andruns list(#212) — both already shareGetBuildsSDK call.Command Signature
view,statuscobra.ExactArgs(2)—util.ExactArgs(2, "project and run id required").args[0]→ project scope (viautil.ParseProjectScope);args[1]→ integer RUN_ID (parsed inRunE).RUN_IDis the second positional soazdo pipelines runs show 12345is rejected.Flags
--raw(bool)spew.Dumpof full SDK build to stderr--json/--jq/--templateutil.AddJSONFlagsJSON Output Contract
Pass the raw
*build.Buildreturned by the SDK (Decision 6). Identical tobuild show.Template Output Contract (
show.tpl)The default template renders (see
internal/cmd/pr/view/view.tplfor the established pattern; byte-identical tobuild show'sshow.tpl):The template uses
definition:(notpipeline:) because the SDK model field isDefinition. The label is consistent with the raw SDK output.Command Wiring
internal/cmd/pipelines/runs/showshow.go—NewCmd(ctx util.CmdContext) *cobra.Command+showOptions+runShowshow.tpl— Go text template (byte-identical tobuild/show/show.tplinitially; consider extracting to a sharedinternal/cmd/pipelines/shared/show.tplonly if divergence appears)show_test.go— table-driven gomock tests (mirrorbuild/show/show_test.go; only theUse:string assertions and table-column labels differ)internal/cmd/pipelines/runs/runs.goto addshowcmd "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs/show"andcmd.AddCommand(showcmd.NewCmd(ctx)). Update theExampleblock.pipelines→runs→show.API Surface
Reuse the already-vendored client. No new SDK clients required.
build.Client.GetBuild→ Builds - Get (REST 7.1) (same call asbuild show)build.GetBuildArgsstruct:{Project *string, BuildId *int}.internal/template.Templatewithbold,hyperlink,s,timeago,timefmt,markdown,pluck,join,truncate,stripprefix,tablerow,tablerender.Mock for
GetBuildis already generated atinternal/mocks/build_client_mock.go:387-402(shared withbuild show). No mock regeneration needed.Implementation Approach (TDD, reuse-first, minimal)
Phase 1 — RED (tests first). Mirror
setupFakeDepsfrominternal/cmd/boards/workitem/list/list_test.go:765-844. Addshow_test.gowith the following table-driven / behaviour tests, all usingt.Parallel()andgomock:TestNewCmd_RegistersAsShowLeaf— assertscmd.Name() == "show",cmd.Aliasescontainsviewandstatus,cmd.Usestarts withshow [ORGANIZATION/]PROJECT RUN_ID.TestNewCmd_RequiresTwoArgs— runs with[]string{"Fabrikam"}; assertscobra.ExactArgs(2)triggers "project and run id required".TestRunShow_RunIDMustBeInteger— sets RUN_ID"abc"; assertsutil.FlagErrorfreturned.TestRunShow_RunIDMustBePositive— sets RUN_ID0and-1; assertsutil.FlagErrorfreturned.TestRunShow_BasicCall— sets[Fabrikam, 12345]; stubsbuild.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 theurl:line uses ANSI hyperlink escape sequence.TestRunShow_TemplateOutput_DurationFormatted— mocks returnStartTime: 2024-01-01T12:00:00Z,FinishTime: 2024-01-01T12:02:13Z; assertsduration: 2m 13srendered.TestRunShow_TemplateOutput_NoDuration_NotStarted— mocks returnFinishTime: nil; assertsduration:line is omitted.TestRunShow_TemplateOutput_Tags— mocks returnTags: []string{"release", "nightly"}; asserts both tags rendered.TestRunShow_TemplateOutput_NoTags— mocks return no tags; assertstags:line is omitted.TestRunShow_TemplateOutput_DefinitionAndQueue_Nested— mocks returnDefinition: &BuildDefinition{Name, Id}andQueue: &Pool{Name, Id}; asserts both rendered.TestRunShow_TemplateOutput_ResultOnlyWhenCompleted— mocks returnStatus: "inProgress",Result: ""; assertsresult:line is omitted.TestRunShow_TemplateOutput_ResultShownWhenCompleted— mocks returnStatus: "completed",Result: "succeeded"; assertsresult: succeededrendered.TestRunShow_JSONOutput— sets--json; mocks return build; asserts JSON containsid,buildNumber,status,result,url,sourceBranch,sourceVersion,definition,queue,requestedBy,tags.TestRunShow_RawFlag— sets--raw; assertsspew.Dumpwas invoked.TestRunShow_ProjectScopeParsing— table-driven:myorg/Fabrikam,Fabrikam, invalid ("org/proj/extra"), empty.TestRunShow_InvalidProjectScope— assertsutil.FlagErrorWrapreturned.TestRunShow_ClientFactoryError— stubs factory to return error; asserts wrapped error.TestRunShow_SDKError— stubs SDK to return error; asserts wrapped error.TestRunShow_OrganizationFromConfigDefault— when scopeArg isFabrikam(no org), assertsclientFact.Build(ctx, defaultOrg)is called with the configured default.Phase 2 — GREEN (minimal implementation). Strict reuse rules:
parseRunID(raw string) (int, error)(~5 lines) for the integer-parse + positive check, andformatDuration(start, finish *time.Time) string(~10 lines) for the human-readable duration. Consider sharingformatDurationwithbuild/showviainternal/cmd/pipelines/build/show/sharedif it stabilizes (do not pre-extract).util.ParseProjectScope,util.AddJSONFlags,util.FlagErrorf/FlagErrorWrap,util.ExactArgs,types.GetValue,types.ToPtr,ios.StartProgressIndicator/StopProgressIndicator,iostreams.Testas-is.internal/template.New(...).WithFuncs(...).Parse(show.tpl).ExecuteData(data)— exact same pattern asinternal/cmd/pr/view/view.go:483-549.ios.StartProgressIndicator()+defer ios.StopProgressIndicator(); callios.StopProgressIndicator()immediately before template execution.opts.exporter.Write(ios, res)passing the raw SDK*Build; template viatemplate.New(...).ExecuteData(templateData{Build: res}).Target delta:
show.go≤ ~120 LOC,show.tpl≤ ~50 LOC,show_test.go≤ ~400 LOC (22 tests), parentruns.go+3 LOC,docs/pipelines_runs_show.mdregenerated viamake docs. No changes tolist.goor other runs siblings.Tooling and Verification Checklist
gofmt/gofumpton touched filesgo test ./internal/cmd/pipelines/runs/...go test ./...make lintmake docsReference Existing Patterns
internal/cmd/pr/view/view.go— primary template-engine reference (Primary:view.go:483-549;viewOptionsstruct atview.go:23-31; template embed atview.go:33-34).internal/cmd/pr/view/view.tpl— primary template-file reference (45 lines; same field-bullet style).internal/cmd/pipelines/build/show/show.go(sibling under thebuild showsub-issue filed in the same wave) — closest sibling; mirror the entire file structure. The only differences are: (a) theUse:string (show [ORGANIZATION/]PROJECT RUN_IDvsBUILD_ID); (b) the table columnPIPELINEvsDEFINITION; (c) theparseRunIDvsparseBuildIDhelper name.internal/cmd/boards/workitem/show/show.go(sibling under feat: Implementazdo boards work-item showcommand #238) — copy structure for the show flow, raw-SDK JSON output, progress lifecycle.internal/cmd/pipelines/runs/list/list.go(sibling under feat: Implementazdo pipelines runs listcommand #212) — closest sibling; reuse JSON/table split pattern, default columns for--format table.internal/cmd/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture; copy structure.internal/mocks/build_client_mock.go:387-402— mock forGetBuild(already generated, do not regenerate).internal/azdo/factory.go:61-67—ClientFactory().Build(...)accessor (reuse).internal/template/template.go— template engine + funcs (reuse, do not reimplement).References