Skip to content

feat: Implement azdo boards iteration project show command #236

Description

@tmeckel

Sub-issue of #142. Sibling of #144 (list), #205 (create), #207 (delete), #209 (update). Hardened spec — do not re-derive decisions. Mirrors internal/cmd/pr/view/view.go and uses Go text templates via internal/template.Template.

Command Description

Display the details of a single iteration (sprint) node in a project's iteration tree. The user supplies a fully-qualified iteration path; the command fetches the matching WorkItemClassificationNode via the Classification Nodes REST 7.1 endpoint and renders it as a Go text template (the same engine used by azdo pr view). Areas share the same shape but are out of scope for this issue — see azdo boards area project show for that sibling.

GET https://dev.azure.com/{organization}/{project}/_apis/wit/classificationnodes/Iterations/{nodePath}?$depth={depth}&api-version=7.1

Locked Decisions (do not re-derive)

# Decision Rationale
1 Use the vendored SDK workitemtracking.Client.GetClassificationNode (not raw HTTP). Mock already generated at internal/mocks/workitemtracking_client_mock.go. Consistent with the list and create siblings; the SDK is what iteration project list already uses.
2 The node is identified by --path, not a positional. The path is the leaf node (e.g. Release 2025/Sprint 1), not the parent. URL-escaped by shared.BuildClassificationPath and passed to GetClassificationNodeArgs.Path. Mirrors the --path flag in #144, #205, #209. Positional paths with embedded / and spaces are awkward.
3 --path is required. Empty / whitespace rejected with util.FlagErrorf. Without a path the SDK would return the project root, which is the list command's job.
4 --depth defaults to 0 (just the node, no children). Valid range 0..10; mapped to GetClassificationNodeArgs.Depth. show is for inspecting a single node. Default 0 keeps the response small; users opt into child metadata.
5 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. Add template funcs as needed. The user explicitly requested template rendering like pr/view.
6 Aliases: view, status. Primary name is show. cmd.Use: "show [ORGANIZATION/]PROJECT", cmd.Aliases: []string{"view", "status"}. Mirrors the pr/view aliasing pattern but with show as the primary.
7 JSON output passes the raw SDK *WorkItemClassificationNode to opts.exporter.Write. No view struct. Symmetric with #144, #205, #209.
8 No confirmation prompt. Show is read-only. Show is non-destructive.
9 --raw flag dumps the full SDK node with spew.Dump to stderr for debugging. Mirrors pr/view --raw.
10 --include-children (default false) flag controls whether the template iterates over Children. When false, the template omits the children block. When true, renders child name + identifier + hasChildren one per line. Children can be large; default to compact.
11 Attributes rendered as a key: value list under a attributes: heading in the template. Dates formatted with timefmt "2006-01-02". Iterations canonically have startDate / finishDate; custom attributes are rare.
12 No new SDK client, no new helper, no new package beyond internal/cmd/boards/iteration/project/show. Reuse shared.BuildClassificationPath and shared.NormalizeClassificationPath from #144/#205. Mandate: minimal code.
13 Mock for GetClassificationNode is already generated at internal/mocks/workitemtracking_client_mock.go. Do not regenerate. Verified.

Command Signature

azdo boards iteration project show [ORGANIZATION/]PROJECT
  • Aliases: view, status
  • Positional parsing via util.ParseProjectScope(ctx, scopeArg). Errors wrapped with util.FlagErrorWrap.
  • --path is required; --depth defaults to 0.

Flags

Flag Maps to Notes
--path (required) GetClassificationNodeArgs.Path URL-escaped via shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path)
--depth (optional, default 0) GetClassificationNodeArgs.Depth 0 = node only; 1 = + immediate children; max 10
--include-children (bool, default false) template switch When true, template renders Children block
--raw (bool) debug dump spew.Dump of full SDK node to stderr
--json / --jq / --template util.AddJSONFlags JSON export of raw SDK node

JSON Output Contract

util.AddJSONFlags(cmd, &opts.exporter, []string{
    "id", "identifier", "name", "path", "structureType",
    "hasChildren", "attributes", "url", "_links", "children",
})

Pass the raw *workitemtracking.WorkItemClassificationNode returned by the SDK (Decision 7).

Template Output Contract (show.tpl)

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

url:        
id:         
identifier: 
name:       
path:       
structure:  
has children: 

attributes:
  startDate:     (if present)
  finishDate:    (if present)
  :            (if --include-children or other attrs)

[children:]                   (only when --include-children)
  -  (, )
  ...

Description is omitted (classification nodes have no description field).

Command Wiring

  • Package path: internal/cmd/boards/iteration/project/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/boards/iteration/project/project.go to add showcmd "github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration/project/show" and cmd.AddCommand(showcmd.NewCmd(ctx)). Update the Example block.
  • Existing higher-level parents must already remain wired: boardsiterationprojectshow.

API Surface

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

  • workitemtracking.Client.GetClassificationNodeClassification Nodes - Get Classification Node (REST 7.1)
  • Constant: workitemtracking.TreeStructureGroupValues.Iterations = "iterations"
  • Template engine: internal/template.Template with bold, hyperlink, s, timeago, timefmt, markdown, pluck, join, truncate, stripprefix, tablerow, tablerender

Mock for GetClassificationNode is already generated at internal/mocks/workitemtracking_client_mock.go:120-140. 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.
  • TestNewCmd_PathFlagRequired — runs cmd.SetArgs([]string{"Fabrikam"}) + cmd.Execute(); asserts cobra MarkFlagRequired error mentioning path.
  • TestRunShow_PathRequired_Empty — sets --path ""; asserts util.FlagErrorf returned.
  • TestRunShow_PathRequired_Whitespace — sets --path " "; asserts util.FlagErrorf returned.
  • TestRunShow_RootLevelCall — sets --path "Sprint 1"; stubs wit.EXPECT().GetClassificationNode(gomock.Any(), gomock.Any()); asserts args.Path == "Sprint%201", *args.StructureGroup == "iterations", *args.Project == "Fabrikam", *args.Depth == 0.
  • TestRunShow_PathNormalizationStripsProjectAndIteration — sets --path "Fabrikam/Iteration/Sprint 1"; asserts args.Path == "Sprint%201".
  • TestRunShow_PathURLEscaping — sets --path "My Sprint/Sub Sprint"; asserts args.Path == "My%20Sprint/Sub%20Sprint".
  • TestRunShow_DepthFlag — sets --depth 2; asserts *args.Depth == 2.
  • TestRunShow_DepthBounds — sets --depth 11; asserts util.FlagErrorf with depth bounds message.
  • TestRunShow_StructureGroupIsIterations — asserts *args.StructureGroup == "iterations".
  • TestRunShow_TemplateOutput_BasicFields — mocks return *WorkItemClassificationNode{Id, Identifier, Name, Path, StructureType, HasChildren, Url}; asserts rendered output contains all field labels and values.
  • TestRunShow_TemplateOutput_Hyperlink — asserts the url: line uses ANSI hyperlink escape sequence (i.e. \x1b]8;; is present).
  • TestRunShow_TemplateOutput_AttributesBlock — mocks return Attributes{startDate: "2024-01-01T00:00:00Z", finishDate: "2024-01-15T00:00:00Z"}; asserts both appear formatted as YYYY-MM-DD.
  • TestRunShow_TemplateOutput_NoAttributes — mocks return nil attributes; asserts the attributes: block is omitted.
  • TestRunShow_TemplateOutput_ChildrenIncluded — sets --include-children; mocks return node with Children; asserts children block rendered.
  • TestRunShow_TemplateOutput_ChildrenOmitted_Default — default flags; mocks return node with Children; asserts children block omitted.
  • TestRunShow_JSONOutput — sets --json; mocks return node; asserts JSON contains id, identifier, name, path, _links, hasChildren, url, structureType, attributes.
  • TestRunShow_RawFlag — sets --raw; asserts spew.Dump was invoked (use a no-op wrapper for testability or just assert no error).
  • TestRunShow_ProjectScopeParsing — table-driven: [ORG/]PROJECT, PROJECT, 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 PROJECT (no org), asserts clientFact.WorkItemTracking(ctx, defaultOrg) is called with the configured default.

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

  • No new helpers beyond the inline formatAttributeDate (~10 lines) if a timefmt template function isn't expressive enough.
  • Reuse util.ParseProjectScope, util.AddJSONFlags, util.FlagErrorf/FlagErrorWrap, util.ExactArgs, types.GetValue, types.ToPtr, ios.StartProgressIndicator/StopProgressIndicator, iostreams.Test, shared.BuildClassificationPath, shared.NormalizeClassificationPath 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 or any user-visible print.
  • Output split: JSON via opts.exporter.Write(ios, res) passing the raw SDK *WorkItemClassificationNode; template via template.New(...).ExecuteData(templateData{Node: res}).
  • Debug log at the point of the SDK call: organization, project, path, depth.

Target delta: show.go ≤ ~120 LOC, show.tpl ≤ ~50 LOC, show_test.go ≤ ~400 LOC (24 tests), parent project.go +3 LOC, docs/boards_iteration_project_show.md regenerated via make docs. No changes to list.go, create.go, delete.go, update.go, or iteration.go.

Tooling and Verification Checklist

  • Run gofmt / gofumpt on touched files
  • go test ./internal/cmd/boards/iteration/...
  • 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; JSON view struct at view.go:42-89).
  • internal/cmd/pr/view/view.tplprimary template-file reference (45 lines; same field-bullet style).
  • internal/cmd/boards/iteration/project/list/list.go — same BuildClassificationPath(project, true, "Iteration", ...) and TreeStructureGroupValues.Iterations (reuse).
  • internal/cmd/boards/iteration/project/create/create.go (feat: Implement azdo boards iteration project create command #205) — closest sibling; mirror JSON/table split, raw-SDK JSON output, progress lifecycle.
  • internal/cmd/boards/workitem/list/list_test.go:765-844setupFakeDeps / stub* fixture; copy structure.
  • internal/cmd/boards/shared/path.goBuildClassificationPath + NormalizeClassificationPath (reuse, do not reimplement).
  • internal/mocks/workitemtracking_client_mock.go — mock for GetClassificationNode (already generated, do not regenerate).
  • internal/azdo/factory.goClientFactory().WorkItemTracking(...) 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