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 #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.
--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)
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.
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.
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 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.go — primary 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.tpl — primary 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).
Sub-issue of #142. Sibling of #144 (
list), #205 (create), #207 (delete), #209 (update). Hardened spec — do not re-derive decisions. Mirrorsinternal/cmd/pr/view/view.goand uses Go text templates viainternal/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
WorkItemClassificationNodevia the Classification Nodes REST 7.1 endpoint and renders it as a Go text template (the same engine used byazdo pr view). Areas share the same shape but are out of scope for this issue — seeazdo boards area project showfor that sibling.Locked Decisions (do not re-derive)
workitemtracking.Client.GetClassificationNode(not raw HTTP). Mock already generated atinternal/mocks/workitemtracking_client_mock.go.listandcreatesiblings; the SDK is whatiteration project listalready uses.--path, not a positional. The path is the leaf node (e.g.Release 2025/Sprint 1), not the parent. URL-escaped byshared.BuildClassificationPathand passed toGetClassificationNodeArgs.Path.--pathflag in #144, #205, #209. Positional paths with embedded/and spaces are awkward.--pathis required. Empty / whitespace rejected withutil.FlagErrorf.listcommand's job.--depthdefaults to0(just the node, no children). Valid range0..10; mapped toGetClassificationNodeArgs.Depth.showis for inspecting a single node. Default 0 keeps the response small; users opt into child metadata.internal/template/template.gowith an//go:embed show.tplfile. Mirrorinternal/cmd/pr/view/view.goandview.tplstructure:bold,hyperlink,s,timeago,timefmt,markdown. Add template funcs as needed.pr/view.view,status. Primary name isshow.cmd.Use: "show [ORGANIZATION/]PROJECT",cmd.Aliases: []string{"view", "status"}.pr/viewaliasing pattern but withshowas the primary.*WorkItemClassificationNodetoopts.exporter.Write. No view struct.--rawflag dumps the full SDK node withspew.Dumpto stderr for debugging.pr/view --raw.--include-children(defaultfalse) flag controls whether the template iterates overChildren. Whenfalse, the template omits the children block. Whentrue, renders child name + identifier + hasChildren one per line.Attributesrendered as akey: valuelist under aattributes:heading in the template. Dates formatted withtimefmt "2006-01-02".startDate/finishDate; custom attributes are rare.internal/cmd/boards/iteration/project/show. Reuseshared.BuildClassificationPathandshared.NormalizeClassificationPathfrom #144/#205.GetClassificationNodeis already generated atinternal/mocks/workitemtracking_client_mock.go. Do not regenerate.Command Signature
view,statusutil.ParseProjectScope(ctx, scopeArg). Errors wrapped withutil.FlagErrorWrap.--pathis required;--depthdefaults to0.Flags
--path(required)GetClassificationNodeArgs.Pathshared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path)--depth(optional, default 0)GetClassificationNodeArgs.Depth--include-children(bool, default false)Childrenblock--raw(bool)spew.Dumpof full SDK node to stderr--json/--jq/--templateutil.AddJSONFlagsJSON Output Contract
Pass the raw
*workitemtracking.WorkItemClassificationNodereturned by the SDK (Decision 7).Template Output Contract (
show.tpl)The default template renders (see
internal/cmd/pr/view/view.tplfor the established pattern):Description is omitted (classification nodes have no description field).
Command Wiring
internal/cmd/boards/iteration/project/showshow.go—NewCmd(ctx util.CmdContext) *cobra.Command+showOptions+runShowshow.tpl— Go text templateshow_test.go— table-driven gomock testsinternal/cmd/boards/iteration/project/project.goto addshowcmd "github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration/project/show"andcmd.AddCommand(showcmd.NewCmd(ctx)). Update theExampleblock.boards→iteration→project→show.API Surface
Reuse the already-vendored client. No new SDK clients required.
workitemtracking.Client.GetClassificationNode→ Classification Nodes - Get Classification Node (REST 7.1)workitemtracking.TreeStructureGroupValues.Iterations="iterations"internal/template.Templatewithbold,hyperlink,s,timeago,timefmt,markdown,pluck,join,truncate,stripprefix,tablerow,tablerenderMock for
GetClassificationNodeis already generated atinternal/mocks/workitemtracking_client_mock.go:120-140. 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(requirefor preconditions,assertfor verifications):TestNewCmd_RegistersAsShowLeaf— assertscmd.Name() == "show",cmd.Aliasescontainsviewandstatus,cmd.Usestarts withshow [ORGANIZATION/]PROJECT.TestNewCmd_PathFlagRequired— runscmd.SetArgs([]string{"Fabrikam"})+cmd.Execute(); asserts cobraMarkFlagRequirederror mentioningpath.TestRunShow_PathRequired_Empty— sets--path ""; assertsutil.FlagErrorfreturned.TestRunShow_PathRequired_Whitespace— sets--path " "; assertsutil.FlagErrorfreturned.TestRunShow_RootLevelCall— sets--path "Sprint 1"; stubswit.EXPECT().GetClassificationNode(gomock.Any(), gomock.Any()); assertsargs.Path == "Sprint%201",*args.StructureGroup == "iterations",*args.Project == "Fabrikam",*args.Depth == 0.TestRunShow_PathNormalizationStripsProjectAndIteration— sets--path "Fabrikam/Iteration/Sprint 1"; assertsargs.Path == "Sprint%201".TestRunShow_PathURLEscaping— sets--path "My Sprint/Sub Sprint"; assertsargs.Path == "My%20Sprint/Sub%20Sprint".TestRunShow_DepthFlag— sets--depth 2; asserts*args.Depth == 2.TestRunShow_DepthBounds— sets--depth 11; assertsutil.FlagErrorfwith 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 theurl:line uses ANSI hyperlink escape sequence (i.e.\x1b]8;;is present).TestRunShow_TemplateOutput_AttributesBlock— mocks returnAttributes{startDate: "2024-01-01T00:00:00Z", finishDate: "2024-01-15T00:00:00Z"}; asserts both appear formatted asYYYY-MM-DD.TestRunShow_TemplateOutput_NoAttributes— mocks returnnilattributes; asserts theattributes:block is omitted.TestRunShow_TemplateOutput_ChildrenIncluded— sets--include-children; mocks return node withChildren; asserts children block rendered.TestRunShow_TemplateOutput_ChildrenOmitted_Default— default flags; mocks return node withChildren; asserts children block omitted.TestRunShow_JSONOutput— sets--json; mocks return node; asserts JSON containsid,identifier,name,path,_links,hasChildren,url,structureType,attributes.TestRunShow_RawFlag— sets--raw; assertsspew.Dumpwas 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— 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 isPROJECT(no org), assertsclientFact.WorkItemTracking(ctx, defaultOrg)is called with the configured default.Phase 2 — GREEN (minimal implementation). Strict reuse rules:
formatAttributeDate(~10 lines) if atimefmttemplate function isn't expressive enough.util.ParseProjectScope,util.AddJSONFlags,util.FlagErrorf/FlagErrorWrap,util.ExactArgs,types.GetValue,types.ToPtr,ios.StartProgressIndicator/StopProgressIndicator,iostreams.Test,shared.BuildClassificationPath,shared.NormalizeClassificationPathas-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 or any user-visible print.opts.exporter.Write(ios, res)passing the raw SDK*WorkItemClassificationNode; template viatemplate.New(...).ExecuteData(templateData{Node: res}).Target delta:
show.go≤ ~120 LOC,show.tpl≤ ~50 LOC,show_test.go≤ ~400 LOC (24 tests), parentproject.go+3 LOC,docs/boards_iteration_project_show.mdregenerated viamake docs. No changes tolist.go,create.go,delete.go,update.go, oriteration.go.Tooling and Verification Checklist
gofmt/gofumpton touched filesgo test ./internal/cmd/boards/iteration/...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; JSON view struct atview.go:42-89).internal/cmd/pr/view/view.tpl— primary template-file reference (45 lines; same field-bullet style).internal/cmd/boards/iteration/project/list/list.go— sameBuildClassificationPath(project, true, "Iteration", ...)andTreeStructureGroupValues.Iterations(reuse).internal/cmd/boards/iteration/project/create/create.go(feat: Implementazdo boards iteration project createcommand #205) — closest sibling; mirror JSON/table split, raw-SDK JSON output, progress lifecycle.internal/cmd/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture; copy structure.internal/cmd/boards/shared/path.go—BuildClassificationPath+NormalizeClassificationPath(reuse, do not reimplement).internal/mocks/workitemtracking_client_mock.go— mock forGetClassificationNode(already generated, do not regenerate).internal/azdo/factory.go—ClientFactory().WorkItemTracking(...)accessor (reuse).internal/template/template.go— template engine + funcs (reuse, do not reimplement).References