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 #116. 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).
> No SDK client for the pipelines resource. The vendored Azure DevOps Go SDK does not expose a dedicated pipelines package — only pipelinepermissions, pipelinesapproval, pipelineschecks, and pipelinestaskcheck. The az pipelines show command surfaces pipeline definitions (YAML/UI-defined reusable pipeline definitions) and operates against the Pipelines REST API. We will use the low-level Client.CreateRequestMessage / Send plumbing exposed by internal/azdo/connection.go:25-35 (the Client interface wraps azuredevops.Connection). This is the same pattern the SDK's own ClientImpl uses internally for any endpoint that doesn't have a dedicated Go method.
Command Description
Display the details of a single Azure Pipelines definition by integer ID or name. The user supplies the [ORGANIZATION/]PROJECT/PIPELINE scope; the command resolves the target (a positive integer is used directly; a string is resolved via GetDefinitions), fetches the matching BuildDefinition via the Definitions REST 7.1 endpoint, and renders it as a Go text template. The definition's Process, Repository, AuthoredBy, and Queue are rendered with their respective child types when present.
GET https://dev.azure.com/{organization}/{project}/_apis/pipelines/definitions/{definitionId}?api-version=7.1
> Why "pipelines show" and not "pipelines definition show"? The Azure CLI's az pipelines show is a top-level command (az pipelines show --id X), not a definition subgroup. It surfaces the same BuildDefinition resource that az pipelines build definition show would, but the user-facing shape is a top-level leaf. We mirror that.
Locked Decisions (do not re-derive)
#
Decision
Rationale
1
No vendored SDK client for the pipelines resource exists. Use the low-level Client.CreateRequestMessage + Client.Send plumbing from internal/azdo/connection.go:25-35 (the same plumbing the SDK uses internally) to call the REST endpoint. The raw JSON response is unmarshaled into a *build.BuildDefinition model from the vendored build package (the response shape matches the BuildDefinition struct in vendor/.../build/models.go).
The Azure DevOps Go SDK does not expose a dedicated pipelines package. The response is structurally identical to BuildDefinition.
2
The pipeline is identified by a positional PIPELINE argument ([ORGANIZATION/]PROJECT/PIPELINE). The target segment is resolved via a new shared.ResolvePipelineDefinition helper at internal/cmd/pipelines/show/shared/resolve.go that mirrors Python's get_definition_id_from_name: if it parses as a positive integer, use it directly; otherwise call build.Client.GetDefinitions and pick the first match. Folds the previous --id flag into the positional.
Consistent with sibling leaves (#255, #257, #258). One positional for either ID or name.
2.5
Parse the positional using util.ParseProjectTargetWithDefaultOrganization from internal/cmd/util/scope.go:183. The function returns a *Target with Organization, Project, Target fields, accepting 2- or 3-segment inputs.
Consistent with internal/cmd/pipelines/variablegroup/{create,delete,show,update}/ precedent.
3
util.ExactArgs(1, "pipeline target is required").
Standard cobra pattern; the 1st positional is the full target.
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/PIPELINE", cmd.Aliases: []string{"view", "status"}.
Mirrors the pr/view aliasing pattern but with show as the primary.
6
JSON output passes the unmarshaled *build.BuildDefinition to opts.exporter.Write. No view struct.
Symmetric with other show siblings.
7
No confirmation prompt. Show is read-only.
Show is non-destructive.
8
--raw flag dumps the full SDK definition with spew.Dump to stderr for debugging.
Mirrors pr/view --raw.
9
No new SDK client, no new helper beyond shared.ResolvePipelineDefinition, no new package beyond internal/cmd/pipelines/show. The HTTP plumbing uses the existing Client interface from internal/azdo/connection.go:25-35.
Mandate: minimal code.
10
The unmarshaled response is *build.BuildDefinition — we reuse the vendored build model struct rather than introducing a hand-written DTO. This means we do not need to regenerate any mocks: there is no SDK method to mock. Tests use httptest.NewServer to stub the HTTP endpoint instead.
build.BuildDefinition is the canonical struct for this resource; the Azure DevOps REST API does not require a different shape.
Command Signature
azdo pipelines show [ORGANIZATION/]PROJECT/PIPELINE
[--raw]
[--json ...]
show_test.go — table-driven tests using httptest.NewServer to stub the HTTP endpoint (no gomock — no SDK method to mock).
The leaf is wired directly under the existing internal/cmd/pipelines/pipelines.go (the umbrella), via cmd.AddCommand(show.NewCmd(ctx)). No new subgroup package is required.
Existing higher-level parents must already remain wired: pipelines → show.
API Surface
No new SDK clients required. Use the low-level Client interface from internal/azdo/connection.go:25-35.
REST endpoint: GET https://dev.azure.com/{organization}/{project}/_apis/pipelines/definitions/{definitionId}?api-version=7.1 → Definitions - Get (REST 7.1)
Response model: *build.BuildDefinition (vendored in vendor/.../build/models.go).
Resource location ID: discovered from Connection.GetResourceAreas at startup; for pipelines (resource area 5d6898bb-45ec-463f-95f9-54d49c71752e) the location ID is //apis/ResourceAreas/5d6898bb-45ec-463f-95f9-54d49c71752e (verify in vendor/.../core/client.go or internal/azdo/extensions/ if not found).
build.Client.GetDefinitions is used for name resolution: args.Project + args.Name; pick first *BuildDefinitionReference from the response Value slice; error on zero or ambiguous matches.
> Mock strategy: Because there is no SDK method to mock, tests use httptest.NewServer to intercept the HTTP request and return a canned JSON body. The test must construct a Client whose Send method calls the test server. Reuse the pattern from internal/cmd/extensions/ if it exists; otherwise use httptest.NewServer + a Client adapter that rewrites the base URL. The GetDefinitions call used during name resolution IS mocked via build.Client gomock (mock at internal/mocks/build_client_mock.go:837).
TestRunShow_TemplateOutput_BasicFields — httptest server returns full BuildDefinition JSON; asserts rendered output contains all field labels and values.
TestRunShow_TemplateOutput_Hyperlink — asserts the url: line uses ANSI hyperlink escape sequence.
TestRunShow_TemplateOutput_DescriptionMarkdown — httptest returns Description: "<p>Hello</p>"; asserts the rendered output contains the markdown-converted text.
TestRunShow_TemplateOutput_NoDescription — httptest returns no description; asserts the description: block shows None given (or is omitted — see sibling boards show for canonical behavior).
TestRunShow_TemplateOutput_ProcessAndRepository_Nested — httptest returns Process: {Type: "yaml"} and Repository: {Name, Id}; asserts both rendered.
TestRunShow_OrganizationFromConfigDefault — when scopeArg is Fabrikam/42 (no org), asserts the HTTP request includes the configured default organization URL.
Phase 2 — GREEN (minimal implementation). Strict reuse rules:
No new helpers beyond the inline shared.ResolvePipelineDefinition(ctx, clientFact, raw) (int, error) (~30 lines): positive-int fast path + build.Client.GetDefinitions first-match lookup; error on zero/ambiguous matches.
Output split: JSON via opts.exporter.Write(ios, res) passing the unmarshaled *build.BuildDefinition; template via template.New(...).ExecuteData(templateData{Definition: res}).
Debug log at the point of the HTTP call: organization, project, pipelineId.
Wiring: in internal/cmd/pipelines/pipelines.go, add show "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/show" to imports and cmd.AddCommand(show.NewCmd(ctx)) to the existing NewCmd.
Target delta: show.go ≤ ~150 LOC (slightly larger than the SDK-cmd siblings due to manual HTTP plumbing), show.tpl ≤ ~50 LOC, shared/resolve.go ≤ ~30 LOC, show_test.go ≤ ~450 LOC (21 tests), internal/cmd/pipelines/pipelines.go +3 LOC, docs/pipelines_show.md regenerated via make docs. No changes to existing subgroup packages (build, runs, variablegroup, pool, queue).
Tooling and Verification Checklist
Run gofmt / gofumpt on touched files
go test ./internal/cmd/pipelines/...
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/extensions/ — primary HTTP-plumbing reference (if it exists; otherwise use the pattern in internal/azdo/extensions/ for raw HTTP calls via the Client interface).
Sub-issue of #116. Hardened spec — do not re-derive decisions. Mirrors
internal/cmd/pr/view/view.goand uses Go text templates viainternal/template.Template(the same engine used byazdo pr view).> No SDK client for the
pipelinesresource. The vendored Azure DevOps Go SDK does not expose a dedicatedpipelinespackage — onlypipelinepermissions,pipelinesapproval,pipelineschecks, andpipelinestaskcheck. Theaz pipelines showcommand surfaces pipeline definitions (YAML/UI-defined reusable pipeline definitions) and operates against the Pipelines REST API. We will use the low-levelClient.CreateRequestMessage/Sendplumbing exposed byinternal/azdo/connection.go:25-35(theClientinterface wrapsazuredevops.Connection). This is the same pattern the SDK's ownClientImpluses internally for any endpoint that doesn't have a dedicated Go method.Command Description
Display the details of a single Azure Pipelines definition by integer ID or name. The user supplies the
[ORGANIZATION/]PROJECT/PIPELINEscope; the command resolves the target (a positive integer is used directly; a string is resolved viaGetDefinitions), fetches the matchingBuildDefinitionvia the Definitions REST 7.1 endpoint, and renders it as a Go text template. The definition'sProcess,Repository,AuthoredBy, andQueueare rendered with their respective child types when present.> Why "pipelines show" and not "pipelines definition show"? The Azure CLI's
az pipelines showis a top-level command (az pipelines show --id X), not adefinitionsubgroup. It surfaces the sameBuildDefinitionresource thataz pipelines build definition showwould, but the user-facing shape is a top-level leaf. We mirror that.Locked Decisions (do not re-derive)
pipelinesresource exists. Use the low-levelClient.CreateRequestMessage+Client.Sendplumbing frominternal/azdo/connection.go:25-35(the same plumbing the SDK uses internally) to call the REST endpoint. The raw JSON response is unmarshaled into a*build.BuildDefinitionmodel from the vendoredbuildpackage (the response shape matches theBuildDefinitionstruct invendor/.../build/models.go).pipelinespackage. The response is structurally identical toBuildDefinition.PIPELINEargument ([ORGANIZATION/]PROJECT/PIPELINE). The target segment is resolved via a newshared.ResolvePipelineDefinitionhelper atinternal/cmd/pipelines/show/shared/resolve.gothat mirrors Python'sget_definition_id_from_name: if it parses as a positive integer, use it directly; otherwise callbuild.Client.GetDefinitionsand pick the first match. Folds the previous--idflag into the positional.util.ParseProjectTargetWithDefaultOrganizationfrominternal/cmd/util/scope.go:183. The function returns a*TargetwithOrganization,Project,Targetfields, accepting 2- or 3-segment inputs.internal/cmd/pipelines/variablegroup/{create,delete,show,update}/precedent.util.ExactArgs(1, "pipeline target is required").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/PIPELINE",cmd.Aliases: []string{"view", "status"}.pr/viewaliasing pattern but withshowas the primary.*build.BuildDefinitiontoopts.exporter.Write. No view struct.--rawflag dumps the full SDK definition withspew.Dumpto stderr for debugging.pr/view --raw.shared.ResolvePipelineDefinition, no new package beyondinternal/cmd/pipelines/show. The HTTP plumbing uses the existingClientinterface frominternal/azdo/connection.go:25-35.*build.BuildDefinition— we reuse the vendoredbuildmodel struct rather than introducing a hand-written DTO. This means we do not need to regenerate any mocks: there is no SDK method to mock. Tests usehttptest.NewServerto stub the HTTP endpoint instead.build.BuildDefinitionis the canonical struct for this resource; the Azure DevOps REST API does not require a different shape.Command Signature
view,statusargs[0]→ target (viautil.ParseProjectTargetWithDefaultOrganization).Targetfield of the parsed*Targetis resolved viashared.ResolvePipelineDefinition(ctx, clientFact, args[0]).Flags
--raw(bool)spew.Dumpof full SDK definition to stderr--json/--jq/--templateutil.AddJSONFlagsJSON Output Contract
Pass the unmarshaled
*build.BuildDefinition(Decision 6, Decision 10).Template Output Contract (
show.tpl)The default template renders (see
internal/cmd/pr/view/view.tplfor the established pattern):Command Wiring
internal/cmd/pipelines/showshow.go—NewCmd(ctx util.CmdContext) *cobra.Command+showOptions+runShowshow.tpl— Go text templateshared/resolve.go—ResolvePipelineDefinition(ctx, clientFact, raw) (int, error)(positive-int fast path +GetDefinitionsfirst-match lookup)show_test.go— table-driven tests usinghttptest.NewServerto stub the HTTP endpoint (no gomock — no SDK method to mock).internal/cmd/pipelines/pipelines.go(the umbrella), viacmd.AddCommand(show.NewCmd(ctx)). No new subgroup package is required.pipelines→show.API Surface
No new SDK clients required. Use the low-level
Clientinterface frominternal/azdo/connection.go:25-35.GET https://dev.azure.com/{organization}/{project}/_apis/pipelines/definitions/{definitionId}?api-version=7.1→ Definitions - Get (REST 7.1)*build.BuildDefinition(vendored invendor/.../build/models.go).client.CreateRequestMessage(ctx, "GET", route, "7.1", nil, "", "application/json", nil)→client.Send(ctx, ...)→client.UnmarshalBody(resp, &definition).7.1.Connection.GetResourceAreasat startup; forpipelines(resource area5d6898bb-45ec-463f-95f9-54d49c71752e) the location ID is//apis/ResourceAreas/5d6898bb-45ec-463f-95f9-54d49c71752e(verify invendor/.../core/client.goorinternal/azdo/extensions/if not found).build.Client.GetDefinitionsis used for name resolution:args.Project+args.Name; pick first*BuildDefinitionReferencefrom the responseValueslice; error on zero or ambiguous matches.Template engine:
internal/template.Templatewithbold,hyperlink,s,timeago,timefmt,markdown,pluck,join,truncate,stripprefix,tablerow,tablerender.> Mock strategy: Because there is no SDK method to mock, tests use
httptest.NewServerto intercept the HTTP request and return a canned JSON body. The test must construct aClientwhoseSendmethod calls the test server. Reuse the pattern frominternal/cmd/extensions/if it exists; otherwise usehttptest.NewServer+ aClientadapter that rewrites the base URL. TheGetDefinitionscall used during name resolution IS mocked viabuild.Clientgomock (mock atinternal/mocks/build_client_mock.go:837).Implementation Approach (TDD, reuse-first, minimal)
Phase 1 — RED (tests first). Add
show_test.gowith the following table-driven / behaviour tests, all usingt.Parallel()andhttptest:TestNewCmd_RegistersAsShowLeaf— assertscmd.Name() == "show",cmd.Aliasescontainsviewandstatus,cmd.Usestarts withshow [ORGANIZATION/]PROJECT/PIPELINE.TestNewCmd_RequiresOneArg— runscmd.SetArgs([]string{})+cmd.Execute(); asserts cobraExactArgserror.TestRunShow_ResolveByPositiveInteger— setsFabrikam/42; assertsshared.ResolvePipelineDefinitionreturns42without callingGetDefinitions.TestRunShow_ResolveByName— setsFabrikam/My Pipeline; stubsbuild.EXPECT().GetDefinitions(...); asserts HTTP request URL path contains_apis/pipelines/definitions/{resolvedId}.TestRunShow_NameNotFound— stubsGetDefinitionsreturning empty; assertsutil.FlagErrorf(or wrapped error).TestRunShow_NameAmbiguous— stubsGetDefinitionsreturning 2 matches; asserts error.TestRunShow_TemplateOutput_BasicFields— httptest server returns fullBuildDefinitionJSON; asserts rendered output contains all field labels and values.TestRunShow_TemplateOutput_Hyperlink— asserts theurl:line uses ANSI hyperlink escape sequence.TestRunShow_TemplateOutput_DescriptionMarkdown— httptest returnsDescription: "<p>Hello</p>"; asserts the rendered output contains the markdown-converted text.TestRunShow_TemplateOutput_NoDescription— httptest returns nodescription; asserts thedescription:block showsNone given(or is omitted — see sibling boards show for canonical behavior).TestRunShow_TemplateOutput_ProcessAndRepository_Nested— httptest returnsProcess: {Type: "yaml"}andRepository: {Name, Id}; asserts both rendered.TestRunShow_JSONOutput— sets--json; httptest returns definition; asserts JSON containsid,name,revision,description,path,type,url,process,repository,queue,authoredBy,createdDate,quality.TestRunShow_RawFlag— sets--raw; assertsspew.Dumpwas invoked.TestRunShow_ProjectScopeParsing— table-driven:myorg/Fabrikam/42,Fabrikam/42, invalid ("org/proj/extra/x"), empty.TestRunShow_InvalidProjectScope— assertsutil.FlagErrorWrapreturned.TestRunShow_ClientFactoryError— stubs factory to return error; asserts wrapped error.TestRunShow_HTTPError— httptest returns 500; asserts wrapped error.TestRunShow_HTTPNotFound— httptest returns 404; asserts wrapped error (with 404 status surfaced).TestRunShow_OrganizationFromConfigDefault— when scopeArg isFabrikam/42(no org), asserts the HTTP request includes the configured default organization URL.Phase 2 — GREEN (minimal implementation). Strict reuse rules:
shared.ResolvePipelineDefinition(ctx, clientFact, raw) (int, error)(~30 lines): positive-int fast path +build.Client.GetDefinitionsfirst-match lookup; error on zero/ambiguous matches.util.ParseProjectTargetWithDefaultOrganization,util.AddJSONFlags,util.FlagErrorf/FlagErrorWrap,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 unmarshaled*build.BuildDefinition; template viatemplate.New(...).ExecuteData(templateData{Definition: res}).internal/cmd/pipelines/pipelines.go, addshow "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/show"to imports andcmd.AddCommand(show.NewCmd(ctx))to the existingNewCmd.Target delta:
show.go≤ ~150 LOC (slightly larger than the SDK-cmd siblings due to manual HTTP plumbing),show.tpl≤ ~50 LOC,shared/resolve.go≤ ~30 LOC,show_test.go≤ ~450 LOC (21 tests),internal/cmd/pipelines/pipelines.go+3 LOC,docs/pipelines_show.mdregenerated viamake docs. No changes to existing subgroup packages (build,runs,variablegroup,pool,queue).Tooling and Verification Checklist
gofmt/gofumpton touched filesgo test ./internal/cmd/pipelines/...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/extensions/— primary HTTP-plumbing reference (if it exists; otherwise use the pattern ininternal/azdo/extensions/for raw HTTP calls via theClientinterface).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/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture; copy structure (but usehttptestinstead of gomock).internal/azdo/connection.go:25-35—Clientinterface (the low-level HTTP plumbing).internal/template/template.go— template engine + funcs (reuse, do not reimplement).internal/cmd/pipelines/variablegroup/delete/delete.go— primary target-resolution precedent (Decision 2 / 2.5): usesUse: "delete [ORGANIZATION/]PROJECT/GROUP",util.ExactArgs(1, "..."),util.ParseProjectTargetWithDefaultOrganization, and ashared.ResolveVariableGrouphelper.internal/cmd/util/scope.go:183—util.ParseProjectTargetWithDefaultOrganization(the parser to call onargs[0]).internal/mocks/build_client_mock.go:837— mock forGetDefinitions(used for name → ID resolution).References