Skip to content

feat: Implement azdo pipelines pool show command #244

Description

@tmeckel

Sub-issue of #239. 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).

> Org-scoped, not project-scoped. The pool subgroup (umbrella #239) takes a positional ORGANIZATION/POOL scope pattern (no project segment). Pools live at the organization level. The pool show command reflects that: it fetches a pool by integer ID or name within the org.

Command Description

Display the details of a single Azure DevOps agent pool by integer ID or name. The command resolves the target (positive integer is used directly; a string is resolved via GetAgentPools), fetches the matching TaskAgentPool via the Agent Pools REST 7.1 endpoint, and renders it as a Go text template. The pool's AutoProvision, AutoUpdate, and (when present) TargetAssignment are rendered with their respective child values.

GET https://dev.azure.com/{organization}/_apis/distributedtask/pools/{poolId}?api-version=7.1

Locked Decisions (do not re-derive)

# Decision Rationale
1 Use the vendored SDK taskagent.Client.GetAgentPool (not raw HTTP). Mock already generated at internal/mocks/taskagent_client_mock.go:411-426. Consistent with the other pool/queue subgroup siblings; the SDK is what the umbrella wires in #239.
2 The pool is identified by a positional POOL argument ([ORGANIZATION/]POOL). The target segment is resolved via a new shared.ResolvePool helper at internal/cmd/pipelines/pool/show/shared/resolve.go that mirrors the variablegroup resolution pattern: if it parses as a positive integer, use it directly; otherwise call taskagent.Client.GetAgentPools(poolName=...) and pick the first match. Folds the previous --id flag into the positional. Mirrors the variablegroup/delete precedent. Pools are org-scoped.
2.5 Parse the positional using util.ParseTargetWithDefaultOrganization from internal/cmd/util/scope.go:173. The function returns a *Target with Organization and Target fields, accepting 1- or 2-segment inputs. Org-scope pattern; project segment is not used. Mirrors internal/cmd/security/group/membership/list/ precedent.
3 util.ExactArgs(1, "pool 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/]POOL", cmd.Aliases: []string{"view", "status"}. Mirrors the pr/view aliasing pattern but with show as the primary.
6 JSON output passes the raw SDK *taskagent.TaskAgentPool 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 pool with spew.Dump to stderr for debugging. Mirrors pr/view --raw.
9 No new SDK client, no new helper beyond shared.ResolvePool, no new package beyond internal/cmd/pipelines/pool/show. Reuse SDK call from the vendored taskagent package. Mandate: minimal code.
10 Mock for GetAgentPool is already generated at internal/mocks/taskagent_client_mock.go:411-426. Do not regenerate. Verified.
11 taskagent.Client.GetAgentPools is used for name resolution; mock also generated. Mirrors the variablegroup/delete name-resolution pattern.

Command Signature

azdo pipelines pool show [ORGANIZATION/]POOL
  [--raw]
  [--json ...]
  • Aliases: view, status
  • Positional parsing: args[0] → target (via util.ParseTargetWithDefaultOrganization).
  • The Target field of the parsed *Target is resolved via shared.ResolvePool(ctx, clientFact, args[0]).

Flags

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

JSON Output Contract

util.AddJSONFlags(cmd, &opts.exporter, []string{
    "id", "name", "type", "scope", "size", "isHosted", "isLegacy",
    "autoProvision", "autoUpdate", "uri", "createdDate", "createdBy", "targetAssignment",
    "properties", "owner", "options", "_links",
})

Pass the raw *taskagent.TaskAgentPool 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:            
name:          
type:          (e.g., automation, deployment, custom)
scope:         (e.g., projectCollection, deployment)
size:          
is hosted:     
is legacy:     
auto provision: 
auto update:   ; ; ...  (if non-empty)
uri:           
created on:    
created by:    ()  (if present)
owner:         ()  (if present)
target assignment:  ()  (if present, e.g., "Hosted")

Command Wiring

  • Package path: internal/cmd/pipelines/pool/show
  • Files:
    • show.goNewCmd(ctx util.CmdContext) *cobra.Command + showOptions + runShow
    • show.tpl — Go text template
    • shared/resolve.goResolvePool(ctx, clientFact, raw) (int, error) (positive-int fast path + GetAgentPools first-match lookup)
    • show_test.go — table-driven gomock tests
  • Update internal/cmd/pipelines/pool/pool.go to add showcmd "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/pool/show" and cmd.AddCommand(showcmd.NewCmd(ctx)). Update the Example block.
  • Existing higher-level parents must already remain wired: pipelinespoolshow.

API Surface

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

  • taskagent.Client.GetAgentPoolAgent Pools - Get (REST 7.1)
  • taskagent.GetAgentPoolArgs struct: {PoolId *int}.
  • taskagent.Client.GetAgentPools (and GetAgentPoolsArgs with PoolName *string) — for name → ID resolution.
  • Template engine: internal/template.Template with bold, hyperlink, s, timeago, timefmt, markdown, pluck, join, truncate, stripprefix, tablerow, tablerender.

Mock for GetAgentPool is already generated at internal/mocks/taskagent_client_mock.go:411-426. 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/]POOL.
  • TestNewCmd_RequiresOneArg — runs cmd.SetArgs([]string{}) + cmd.Execute(); asserts cobra ExactArgs error.
  • TestRunShow_ResolveByPositiveInteger — sets myorg/7; asserts shared.ResolvePool returns 7 without calling GetAgentPools.
  • TestRunShow_ResolveByName — sets myorg/Default; stubs taskagent.EXPECT().GetAgentPools(...); asserts *args.PoolId == 7 (resolved ID).
  • TestRunShow_BasicCall — sets [myorg] 7; stubs taskagent.EXPECT().GetAgentPool(gomock.Any(), gomock.Any()); asserts *args.PoolId == 7.
  • TestRunShow_OrgFromConfigDefault — sets [] 7; asserts clientFact.TaskAgent(ctx, defaultOrg) is called with the configured default organization.
  • TestRunShow_OrgFromPositional — sets myorg/7; asserts clientFact.TaskAgent(ctx, "myorg") is called with the positional organization.
  • TestRunShow_TemplateOutput_BasicFields — mocks return *TaskAgentPool{Id, Name, Type, Scope, Size, IsHosted, Url}; asserts rendered output contains all field labels and values.
  • TestRunShow_TemplateOutput_Hyperlink — asserts the url: line uses ANSI hyperlink escape sequence.
  • TestRunShow_TemplateOutput_AutoUpdateAsList — mocks return AutoUpdate: true; asserts auto update: true rendered (or the bool is rendered as true/false).
  • TestRunShow_TemplateOutput_CreatedBy_Nested — mocks return CreatedBy: &IdentityRef{DisplayName, UniqueName}; asserts created by: Alice (alice@contoso.com) rendered.
  • TestRunShow_TemplateOutput_NoCreatedBy — mocks return CreatedBy: nil; asserts created by: line is omitted.
  • TestRunShow_TemplateOutput_TargetAssignment — mocks return TargetAssignment: "Hosted"; asserts target assignment: Hosted rendered.
  • TestRunShow_TemplateOutput_NoTargetAssignment — mocks return TargetAssignment: ""; asserts target assignment: line is omitted.
  • TestRunShow_JSONOutput — sets --json; mocks return pool; asserts JSON contains id, name, type, scope, size, isHosted, isLegacy, autoProvision, autoUpdate, uri, createdDate, createdBy.
  • TestRunShow_RawFlag — sets --raw; asserts spew.Dump was invoked.
  • TestRunShow_ClientFactoryError — stubs factory to return error; asserts wrapped error.
  • TestRunShow_SDKError — stubs SDK to return error; asserts wrapped error.

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

  • No new helpers beyond shared.ResolvePool(ctx, clientFact, raw) (int, error) (~30 lines): positive-int fast path + taskagent.Client.GetAgentPools first-match lookup; error on zero/ambiguous matches.
  • Reuse util.ParseTargetWithDefaultOrganization, util.AddJSONFlags, util.FlagErrorf/FlagErrorWrap, 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 *TaskAgentPool; template via template.New(...).ExecuteData(templateData{Pool: res}).
  • Debug log at the point of the SDK call: organization, poolId.

Target delta: show.go ≤ ~130 LOC, show.tpl ≤ ~50 LOC, shared/resolve.go ≤ ~30 LOC, show_test.go ≤ ~400 LOC (21 tests), parent pool.go +3 LOC, docs/pipelines_pool_show.md regenerated via make docs. No changes to other pool siblings (none yet).

Tooling and Verification Checklist

  • Run gofmt / gofumpt on touched files
  • go test ./internal/cmd/pipelines/pool/...
  • 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/boards/workitem/list/list_test.go:765-844setupFakeDeps / stub* fixture; copy structure.
  • internal/cmd/pipelines/variablegroup/delete/delete.goprimary target-resolution precedent (Decision 2 / 2.5): uses Use: "delete [ORGANIZATION/]PROJECT/GROUP", util.ExactArgs(1, "..."), and a shared.ResolveVariableGroup helper. Our pool/show differs only in the parser (ParseTargetWithDefaultOrganization because pools are org-scoped) and the resolution API.
  • internal/cmd/util/scope.go:173util.ParseTargetWithDefaultOrganization (org-scoped parser).
  • internal/mocks/taskagent_client_mock.go:411-426 — mock for GetAgentPool (already generated, do not regenerate).
  • internal/azdo/factory.go:133-139ClientFactory().TaskAgent(...) accessor (reuse).
  • internal/template/template.go — template engine + funcs (reuse, do not reimplement).

References

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions