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 (iteration project list), #207 (iteration project delete), and #209 (iteration project update). Hardened spec — do not re-derive decisions. Mirrors #204 (area project create) with iteration-specific additions for dates and attributes.
Command Description
Create a new iteration (sprint) under a project's iteration tree. The body is {name: "...", attributes: {startDate, finishDate, ...}} posted to the Classification Nodes REST 7.1 endpoint.
POST https://dev.azure.com/{organization}/{project}/_apis/wit/classificationnodes/Iterations/{parentPath}?api-version=7.1Content-Type: application/json
For v1 the structureGroup is hardcoded to Iterations (lowercase literal "iterations"). The parentPath segment is optional; omitting it creates a node at the project root (directly under <Project>/Iteration). The body always carries name, and optionally attributes (carrying startDate, finishDate, or arbitrary key=value pairs). No id — move semantics deferred to #209 (update).
Path normalization via shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path) — same helper as #144. Strips leading <Project> and Iteration segments; URL-escapes remaining; returns ("", nil) for empty input.
Symmetric round-trip; one helper, one source of truth. (Note: scope name is "Iteration" singular, matching the list command's convention.)
5
StructureGroup is hardcoded to &workitemtracking.TreeStructureGroupValues.Iterations (literal "iterations"). No --structure-group flag.
Scope discipline; area lives in its own command.
6
No move-by-id semantics in v1. Always send Name in PostedNode; never Id.
--start-date and --finish-date are first-class flags (string), parsed strictly as time.RFC3339 (or YYYY-MM-DD → midnight UTC). Stored in PostedNode.Attributes as RFC 3339 strings to match the REST sample exactly.
Iteration dates are the canonical reason this command exists.
8
finishDate < startDate is rejected with util.FlagErrorf.
Catches a class of user errors before the network call.
9
--attributes key=value is a repeatable flag for arbitrary attributes. Merged into PostedNode.Attributesafter start/finish dates; --start-date / --finish-date win on key conflict.
Escape hatch; covers fields like custom Budget or Goal.
10
No date operators (>=, today, etc.) in create. Strict RFC 3339 or YYYY-MM-DD only.
List has filter semantics; create must not. Avoid re-implementing the list's parseDateConstraint.
11
JSON output passes the raw SDK *WorkItemClassificationNode to opts.exporter.Write. No view struct.
Table output: columns ID, NAME, PATH, START DATE, FINISH DATE, HAS CHILDREN (single row).
Mirrors the create surface; date columns surface the freshly-set attributes.
13
No confirmation prompt. Create is reversible via iteration project delete (#207).
Matches az boards iteration project update.
14
No pre-existence check. Defer to REST 4xx.
Saves one round-trip; error is already clear.
15
No new SDK client, no new helper, no new package. Only the new create package + 3 LOC in project.go to wire it. Date parsing inlined (≈8 lines) — do not promote parseFlexibleDate from list.go; it handles today/operators that create must reject.
Mandate: minimal code.
16
Mock for CreateOrUpdateClassificationNode is already generated at internal/mocks/workitemtracking_client_mock.go:106-118. Do not regenerate.
Verified; same mock serves #204, #209, and this issue.
Pass the raw *workitemtracking.WorkItemClassificationNode returned by the SDK (Decision 11). Date strings live inside attributes.
Table default columns: ID, NAME, PATH, START DATE, FINISH DATE, HAS CHILDREN (Decision 12). When neither date flag is set, the date columns render empty (not omitted).
Use the golang-spf13-cobra, golang-cli, golang-testing, and golang-stretchr-testify skills as the source of truth for structure, flag wiring, args validators, table-driven tests, and mock verification.
Phase 1 — RED (tests first). Mirror setupFakeDeps from internal/cmd/boards/workitem/list/list_test.go:765-844. Add create_test.go with the following table-driven / behaviour tests, all using t.Parallel() and gomock (require for preconditions, assert for verifications):
TestNewCmd_RegistersAsCreateLeaf — asserts cmd.Name() == "create", cmd.Aliases contains c and cr, cmd.Use starts with create [ORGANIZATION/]PROJECT.
TestRunCreate_BothDates_RFC3339 — sets --start-date 2025-01-06T00:00:00Z --finish-date 2025-01-19T00:00:00Z; asserts both present in attributes as strings.
TestRunCreate_DateFlags_InvalidFormat — sets --start-date "yesterday"; asserts util.FlagErrorf with invalid --start-date.
TestRunCreate_DateFlags_FinishBeforeStart — sets --start-date 2025-01-19 --finish-date 2025-01-06; asserts util.FlagErrorf with finish-date must be on or after start-date.
TestRunCreate_AttributesFlag_StartDateWins — sets both --start-date 2025-01-06 and --attributes startDate=2024-12-01; asserts the start-date flag value is used.
TestRunCreate_AttributesFlag_InvalidFormat — sets --attributes "=value" (no key) and --attributes "novalue" (no =); asserts util.FlagErrorf.
TestRunCreate_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 buildAttributes / parseStrictDate / formatAttributeDate in §3. Do not promote parseFlexibleDate from list.go — its relaxed semantics (operators, today) are wrong for create.
Reuse shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path) for parent-path normalization; the helper already URL-escapes segments.
Reuse shared.NormalizeClassificationPath for the response table Path column (REST returns backslashes; convert to forward slashes).
SDK call only: wit.CreateOrUpdateClassificationNode(ctx.Context(), args) with args.PostedNode{Name, Attributes}, args.Project, args.StructureGroup = &workitemtracking.TreeStructureGroupValues.Iterations, and conditionallyargs.Path.
Progress indicator: ios.StartProgressIndicator() + defer ios.StopProgressIndicator(); call ios.StopProgressIndicator() immediately before the table render (mirrors internal/cmd/repo/create/create.go).
Output split: JSON via opts.exporter.Write(ios, res) passing the raw SDK *WorkItemClassificationNode; table via ctx.Printer("table") with AddColumns/AddField/EndRow/Render.
Debug log at the point of the SDK call: organization, project, name, parentPath, attributeCount.
Target delta: create.go ≤ ~170 LOC, create_test.go ≤ ~450 LOC (22 tests), parent project.go +3 LOC, docs/boards_iteration_project_create.md regenerated via make docs. No changes to list.go, delete.go, update.go, or iteration.go.
Tooling and Verification Checklist
gofmt / gofumpt on touched files
go test ./internal/cmd/boards/iteration/...
go test ./...
make lint
make docs
Reference Existing Patterns
internal/cmd/boards/iteration/project/list/list.go — use this as the structural template (already uses the SDK; same BuildClassificationPath(project, true, "Iteration", ...) and TreeStructureGroupValues.Iterations).
Sub-issue of #142. Sibling of #144 (
iteration project list), #207 (iteration project delete), and #209 (iteration project update). Hardened spec — do not re-derive decisions. Mirrors #204 (area project create) with iteration-specific additions for dates and attributes.Command Description
Create a new iteration (sprint) under a project's iteration tree. The body is
{name: "...", attributes: {startDate, finishDate, ...}}posted to the Classification Nodes REST 7.1 endpoint.For v1 the structureGroup is hardcoded to
Iterations(lowercase literal"iterations"). TheparentPathsegment is optional; omitting it creates a node at the project root (directly under<Project>/Iteration). The body always carriesname, and optionallyattributes(carryingstartDate,finishDate, or arbitrarykey=valuepairs). Noid— move semantics deferred to #209 (update).Reference sample (REST 7.1 docs):
{ "name": "Final Iteration", "attributes": { "startDate": "2014-10-27T00:00:00Z", "finishDate": "2014-10-31T00:00:00Z" } }Locked Decisions (do not re-derive)
workitemtracking.Client.CreateOrUpdateClassificationNode(not raw HTTP). Mock is already generated.iteration project listalready uses.--nameis the only required flag. Empty/whitespace rejected withutil.FlagErrorf.--pathis optional. It specifies the parent iteration path under<Project>/Iteration. Empty → new node at project root.--pathin #144 and #204.shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path)— same helper as #144. Strips leading<Project>andIterationsegments; URL-escapes remaining; returns("", nil)for empty input."Iteration"singular, matching the list command's convention.)StructureGroupis hardcoded to&workitemtracking.TreeStructureGroupValues.Iterations(literal"iterations"). No--structure-groupflag.NameinPostedNode; neverId.--start-dateand--finish-dateare first-class flags (string), parsed strictly astime.RFC3339(orYYYY-MM-DD→ midnight UTC). Stored inPostedNode.Attributesas RFC 3339 strings to match the REST sample exactly.finishDate < startDateis rejected withutil.FlagErrorf.--attributes key=valueis a repeatable flag for arbitrary attributes. Merged intoPostedNode.Attributesafter start/finish dates;--start-date/--finish-datewin on key conflict.BudgetorGoal.>=,today, etc.) in create. Strict RFC 3339 orYYYY-MM-DDonly.parseDateConstraint.*WorkItemClassificationNodetoopts.exporter.Write. No view struct.ID, NAME, PATH, START DATE, FINISH DATE, HAS CHILDREN(single row).iteration project delete(#207).az boards iteration project update.createpackage + 3 LOC inproject.goto wire it. Date parsing inlined (≈8 lines) — do not promoteparseFlexibleDatefrom list.go; it handlestoday/operators that create must reject.CreateOrUpdateClassificationNodeis already generated atinternal/mocks/workitemtracking_client_mock.go:106-118. Do not regenerate.Command Signature
c,crutil.ParseProjectScope(ctx, scopeArg)(defined ininternal/cmd/util/scope.go:78-108); errors wrapped withutil.FlagErrorWrap.Flags (mapped to SDK/REST)
--name(required)WorkItemClassificationNode.Name--path(optional)CreateOrUpdateClassificationNodeArgs.Path(URL segment, parent)--start-date(optional)PostedNode.Attributes["startDate"](RFC 3339 string)YYYY-MM-DDaccepted → midnight UTC--finish-date(optional)PostedNode.Attributes["finishDate"](RFC 3339 string)>= startDateif both set--attributes(repeatablekey=value)PostedNode.Attributes[key]--start-date/--finish-datewin on conflictJSON Output Contract
Pass the raw
*workitemtracking.WorkItemClassificationNodereturned by the SDK (Decision 11). Date strings live insideattributes.Table default columns:
ID, NAME, PATH, START DATE, FINISH DATE, HAS CHILDREN(Decision 12). When neither date flag is set, the date columns render empty (not omitted).Command Wiring
internal/cmd/boards/iteration/project/createcreate.go—NewCmd(ctx util.CmdContext) *cobra.Command+createOptions+runCreatecreate_test.go— table-driven gomock testsinternal/cmd/boards/iteration/project/project.go:boards→iteration→project→create.Code Skeleton (canonical, copy verbatim)
§1.
createOptionsstruct§2.
NewCmdshape (no surprises)§3.
runCreateskeleton§4. Test fixture (copy from
workitem/list/list_test.go:765-844)API Surface
Reuse the already-vendored client. No new SDK clients required.
workitemtracking.Client.CreateOrUpdateClassificationNode→ Classification Nodes - Create Or Update (REST 7.1)workitemtracking.TreeStructureGroupValues.Iterations="iterations"(lowercase; atvendor/.../workitemtracking/models.go:967)Mock for
CreateOrUpdateClassificationNodeis already generated atinternal/mocks/workitemtracking_client_mock.go:106-118. No mock regeneration needed.Implementation Approach (TDD, reuse-first, minimal)
Use the
golang-spf13-cobra,golang-cli,golang-testing, andgolang-stretchr-testifyskills as the source of truth for structure, flag wiring, args validators, table-driven tests, and mock verification.Phase 1 — RED (tests first). Mirror
setupFakeDepsfrominternal/cmd/boards/workitem/list/list_test.go:765-844. Addcreate_test.gowith the following table-driven / behaviour tests, all usingt.Parallel()andgomock(requirefor preconditions,assertfor verifications):TestNewCmd_RegistersAsCreateLeaf— assertscmd.Name() == "create",cmd.Aliasescontainscandcr,cmd.Usestarts withcreate [ORGANIZATION/]PROJECT.TestNewCmd_NameFlagRequired— runscmd.SetArgs([]string{"Fabrikam"})+cmd.Execute(); asserts cobraMarkFlagRequirederror mentioningname.TestRunCreate_EmptyNameFlag— sets--name " "; assertsutil.FlagErrorfreturned with--namein message.TestRunCreate_RootLevelCreate— sets only--name "Sprint 1"; stubswit.EXPECT().CreateOrUpdateClassificationNode(gomock.Any(), gomock.Any()); asserts capturedargs.Path == nil,*args.StructureGroup == TreeStructureGroupValues.Iterations(literal"iterations"),*args.Project == "Fabrikam",*args.PostedNode.Name == "Sprint 1",args.PostedNode.Attributes == nil.TestRunCreate_NestedPathCreate— sets--name "Sprint 2" --path "Release 2025"; assertsargs.Path != niland equals"Release%202025"(URL-escaped).TestRunCreate_PathNormalizationStripsProjectAndIteration— sets--path "Fabrikam/Iteration/Release 2025/Sprint 1"; assertsargs.Path == "Release%202025/Sprint%201".TestRunCreate_PathURLEscaping— sets--path "My Sprint/Sub Sprint"; assertsargs.Path == "My%20Sprint/Sub%20Sprint".TestRunCreate_StructureGroupIsIterations— asserts*args.StructureGroup == "iterations".TestRunCreate_PostedNodeHasName— assertsargs.PostedNode != nil,*args.PostedNode.Name == "X",args.PostedNode.Id == nil(no move semantics).TestRunCreate_StartDateOnly— sets--start-date 2025-01-06; asserts(*args.PostedNode.Attributes)["startDate"] == "2025-01-06T00:00:00Z"andfinishDatekey absent.TestRunCreate_FinishDateOnly— sets--finish-date 2025-01-19T00:00:00Z; assertsattributes["finishDate"]present,startDatekey absent.TestRunCreate_BothDates_RFC3339— sets--start-date 2025-01-06T00:00:00Z --finish-date 2025-01-19T00:00:00Z; asserts both present inattributesas strings.TestRunCreate_DateFlags_InvalidFormat— sets--start-date "yesterday"; assertsutil.FlagErrorfwithinvalid --start-date.TestRunCreate_DateFlags_FinishBeforeStart— sets--start-date 2025-01-19 --finish-date 2025-01-06; assertsutil.FlagErrorfwithfinish-date must be on or after start-date.TestRunCreate_AttributesFlag_Merged— sets--attributes goal=Ship --attributes team=Alpha; assertsattributes["goal"] == "Ship"andattributes["team"] == "Alpha".TestRunCreate_AttributesFlag_StartDateWins— sets both--start-date 2025-01-06and--attributes startDate=2024-12-01; asserts the start-date flag value is used.TestRunCreate_AttributesFlag_InvalidFormat— sets--attributes "=value"(no key) and--attributes "novalue"(no=); assertsutil.FlagErrorf.TestRunCreate_ProjectScopeParsing— table-driven:[ORG/]PROJECT,PROJECT, invalid ("org/proj/extra"), empty.TestRunCreate_InvalidProjectScope— assertsutil.FlagErrorWrapreturned.TestRunCreate_ClientFactoryError— stubs factory to return error; asserts wrapped error.TestRunCreate_SDKError— stubs SDK to return error; asserts wrapped error.TestRunCreate_TableOutput_AllColumns— mocks return*WorkItemClassificationNode{Id, Name, Path, HasChildren, Attributes{startDate,finishDate}}; parses stdout; asserts row matches all 6 columns.TestRunCreate_JSONOutput— sets--json; mocks return node; asserts JSON containsid,name,path,_links,hasChildren,url,identifier,structureType,attributes.TestRunCreate_OrganizationFromConfigDefault— when scopeArg isPROJECT(no org), assertsclientFact.WorkItemTracking(ctx, defaultOrg)is called with the configured default.Phase 2 — GREEN (minimal implementation). Strict reuse rules:
buildAttributes/parseStrictDate/formatAttributeDatein §3. Do not promoteparseFlexibleDatefrom list.go — its relaxed semantics (operators,today) are wrong for create.util.ParseProjectScope,util.AddJSONFlags,util.FlagErrorf/FlagErrorWrap,util.ExactArgs,types.GetValue,types.ToPtr,ctx.Printer("table"),ios.StartProgressIndicator/StopProgressIndicator,iostreams.Testas-is.shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path)for parent-path normalization; the helper already URL-escapes segments.shared.NormalizeClassificationPathfor the response tablePathcolumn (REST returns backslashes; convert to forward slashes).wit.CreateOrUpdateClassificationNode(ctx.Context(), args)withargs.PostedNode{Name, Attributes},args.Project,args.StructureGroup = &workitemtracking.TreeStructureGroupValues.Iterations, and conditionallyargs.Path.ios.StartProgressIndicator()+defer ios.StopProgressIndicator(); callios.StopProgressIndicator()immediately before the table render (mirrorsinternal/cmd/repo/create/create.go).opts.exporter.Write(ios, res)passing the raw SDK*WorkItemClassificationNode; table viactx.Printer("table")withAddColumns/AddField/EndRow/Render.Target delta:
create.go≤ ~170 LOC,create_test.go≤ ~450 LOC (22 tests), parentproject.go+3 LOC,docs/boards_iteration_project_create.mdregenerated viamake docs. No changes tolist.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/boards/iteration/project/list/list.go— use this as the structural template (already uses the SDK; sameBuildClassificationPath(project, true, "Iteration", ...)andTreeStructureGroupValues.Iterations).internal/cmd/boards/iteration/project/create/create.go(this issue) — primary deliverable.internal/cmd/boards/iteration/project/delete/delete.go(feat: Implementazdo boards iteration project deletecommand #207) — destructive-command sibling; mirror the SDK call shape and progress lifecycle.internal/cmd/boards/iteration/project/update/update.go(feat: Implementazdo boards iteration project updatecommand #209) — update sibling; mirror the date/attribute parsing style.internal/cmd/boards/area/project/create/create.go(feat: Implementazdo boards area project createcommand #204) — closest mirror; copy progress lifecycle, JSON/table split, raw-SDK JSON output.internal/cmd/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture; copy structure (Decision fixture §4).internal/cmd/boards/shared/path.go—BuildClassificationPath+NormalizeClassificationPath(reuse, do not reimplement).internal/mocks/workitemtracking_client_mock.go:106-118— mock forCreateOrUpdateClassificationNode(already generated, do not regenerate).internal/azdo/factory.go—ClientFactory().WorkItemTracking(...)accessor (reuse).References