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 #205 (iteration project create) and #144 (iteration project list). Hardened spec — do not re-derive decisions. Mirrors #206 (area project delete) with the structure-group and scope-name swaps only.
Command Description
Delete an iteration (sprint) from a project's iteration tree. The endpoint is the Classification Nodes REST 7.1 DELETE:
The optional ?$reclassifyId={id} query param moves work items from the deleted node to the specified target node before deletion. The SDK returns no body (204 No Content).
For v1 the structureGroup is hardcoded to Iterations (lowercase literal "iterations"). The scope name passed to BuildClassificationPath is "Iteration" (singular), matching the list/create convention.
Locked Decisions (do not re-derive)
#
Decision
Rationale
1
Use the vendored SDK workitemtracking.Client.DeleteClassificationNode (not raw HTTP). Mock is already generated.
Consistent with #205, #209; raw HTTP is the outlier (see list.go).
2
--path is the only required flag. It specifies the path of the node to delete. The path is the node's own path (not parent). <Project>/<Iteration> segments are stripped via BuildClassificationPath. Empty/whitespace rejected with util.FlagErrorf.
The SDK's DeleteClassificationNodeArgs.Path is the only delete-target mechanism; the SDK exposes no NodeId route.
3
Optional --reclassify-id . Maps to DeleteClassificationNodeArgs.ReclassifyId. When set, work items are moved to that node before deletion.
Mirrors the REST $reclassifyId query param.
4
No --reclassify-path flag in v1. SDK only supports ReclassifyId (int), not a path.
SDK constraint.
5
No --id flag. The SDK's DeleteClassificationNodeArgs has no NodeId field; only path-based deletion is supported.
SDK constraint.
6
No --force flag. Use --yes to skip confirmation (existing pattern in pipelines/variablegroup/delete, serviceendpoint/delete, security/permission/delete).
Standard destructive convention.
7
--yes, -y via BoolVarP. Non-TTY + no --yes returns util.FlagErrorf("--yes required when not running interactively") — mirrors internal/cmd/pipelines/variablegroup/delete/delete.go:125.
Match existing pattern.
8
Confirmation prompt via prompter.Confirm(...). User cancel returns util.ErrCancel. Stop the progress indicator before the prompt and restart after — mirrors internal/cmd/pipelines/variablegroup/delete/delete.go:127,141.
Match existing pattern.
9
Aliases: d, del, rm per AGENTS.md destructive convention.
Standard.
10
Path normalization via shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path) (note: "Iteration" is the singular scope name matching list/create). URL-escapes segments; returns the path portion.
One helper, one source of truth.
11
StructureGroup hardcoded to &workitemtracking.TreeStructureGroupValues.Iterations (literal "iterations"). No --structure-group flag.
Scope discipline; area lives in its own command (#206).
12
JSON output: deleteResult{Deleted bool, Path string, ReclassifyID *int} via util.AddJSONFlags. ReclassifyID is *int with omitempty so it only appears when the flag was set.
Default output: single line Deleted iteration: <path> via fmt.Fprintf(ios.Out, ...). When --reclassify-id is set, append \nReclassified work items to: <id>.
Default output: single line Deleted iteration: <path> (Decision 13). When --reclassify-id is set, append \nReclassified work items to: <id>.
funcNewCmd(ctx util.CmdContext) *cobra.Command {
opts:=&deleteOptions{}
cmd:=&cobra.Command{
Use: "delete [ORGANIZATION/]PROJECT --path ",
Short: "Delete an iteration from a project.",
Long: heredoc.Doc(` Delete an iteration (sprint) from a project. The command prompts for confirmation unless --yes is supplied. Use --reclassify-id to move any work items to another node before deletion; the Azure DevOps REST API rejects deletes while a node is still in use unless work items are reclassified first. `),
Example: heredoc.Doc(` # Delete a top-level iteration azdo boards iteration project delete Fabrikam --path "Sprint 1" --yes # Delete a nested iteration with a confirmation prompt azdo boards iteration project delete Fabrikam --path "Release 2025/Sprint 1" # Reclassify work items to node 42 before deletion azdo boards iteration project delete Fabrikam --path "Sprint 1" \ --reclassify-id 42 --yes # Emit JSON azdo boards iteration project delete Fabrikam --path "Sprint 1" --reclassify-id 42 --json `),
Aliases: []string{"d", "del", "rm"},
Args: util.ExactArgs(1, "project argument required"),
RunE: func(cmd*cobra.Command, args []string) error {
opts.scopeArg=args[0]
returnrunDelete(ctx, opts)
},
}
cmd.Flags().StringVar(&opts.path, "path", "", "Path of the iteration to delete (under /Iteration, leading /Iteration stripped).")
cmd.Flags().IntVar(&opts.reclassifyID, "reclassify-id", 0, "ID of the target node to which work items should be moved before deletion.")
cmd.Flags().BoolVarP(&opts.yes, "yes", "y", false, "Skip the confirmation prompt.")
_=cmd.MarkFlagRequired("path")
util.AddJSONFlags(cmd, &opts.exporter, []string{"deleted", "path", "reclassifyId"})
returncmd
}
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 and the prompter mock wiring from internal/cmd/security/permission/delete/delete_test.go. Add delete_test.go with the following table-driven / behaviour tests, all using t.Parallel() and gomock (require for preconditions, assert for verifications):
TestNewCmd_RegistersAsDeleteLeaf — asserts cmd.Name() == "delete", cmd.Aliases contains d, del, rm, cmd.Use starts with delete [ORGANIZATION/]PROJECT.
TestRunDelete_ClientFactoryError — stubs factory to return error; asserts wrapped error.
TestRunDelete_SDKError — stubs SDK to return error; asserts wrapped error.
TestRunDelete_YesFlag_SkipsPrompt (TTY, --yes) — asserts prompter.Confirm is not called, SDK is called.
TestRunDelete_ConfirmationPrompt_Yes (TTY, no --yes, user confirms) — asserts prompter.Confirm called with deletion prompt; SDK is called.
TestRunDelete_ConfirmationPrompt_No (TTY, no --yes, user declines) — asserts prompter.Confirm returns false; SDK is not called; returns util.ErrCancel.
TestRunDelete_NonTTY_NoYes_ReturnsError (non-TTY, no --yes) — asserts util.FlagErrorf with --yes required; SDK is not called.
TestRunDelete_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. Inline everything in runDelete (already short).
Reuse shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path) for path normalization; the helper already URL-escapes segments.
SDK call only: wit.DeleteClassificationNode(ctx.Context(), args) with args.Project, args.StructureGroup = &workitemtracking.TreeStructureGroupValues.Iterations, args.Path, and conditionallyargs.ReclassifyId.
Progress indicator: ios.StartProgressIndicator() + defer ios.StopProgressIndicator(); call ios.StopProgressIndicator() before the confirmation prompt and ios.StartProgressIndicator() after a confirmed prompt (mirrors internal/cmd/pipelines/variablegroup/delete/delete.go:127,141).
Confirmation prompt: gated on !opts.yes and !ios.CanPrompt(); on cancel, return util.ErrCancel.
Output split: JSON via opts.exporter.Write(ios, deleteResult{...}) (constructed in runDelete, NOT from the SDK return — the SDK returns only error); default via fmt.Fprintf(ios.Out, ...).
Debug log at the point of the SDK call: organization, project, path, reclassifyId; plus a canceled event when the user declines.
Target delta: delete.go ≤ ~140 LOC, delete_test.go ≤ ~500 LOC (22 tests), parent project.go +3 LOC, docs/boards_iteration_project_delete.md regenerated via make docs. No changes to list.go, create.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
#206 (area delete) — the primary mirror; same SDK call, same confirmation pattern, same JSON shape.
internal/cmd/boards/iteration/project/create/create.go (sibling feat: Implement azdo boards iteration project create command #205) — structural template: same path normalization, same client access, same JSON flag registration. Read it to ensure stylistic consistency on iteration wording.
Sub-issue of #142. Sibling of #205 (
iteration project create) and #144 (iteration project list). Hardened spec — do not re-derive decisions. Mirrors #206 (area project delete) with the structure-group and scope-name swaps only.Command Description
Delete an iteration (sprint) from a project's iteration tree. The endpoint is the Classification Nodes REST 7.1 DELETE:
The optional
?$reclassifyId={id}query param moves work items from the deleted node to the specified target node before deletion. The SDK returns no body (204 No Content).For v1 the structureGroup is hardcoded to
Iterations(lowercase literal"iterations"). The scope name passed toBuildClassificationPathis"Iteration"(singular), matching the list/create convention.Locked Decisions (do not re-derive)
workitemtracking.Client.DeleteClassificationNode(not raw HTTP). Mock is already generated.list.go).--pathis the only required flag. It specifies the path of the node to delete. The path is the node's own path (not parent).<Project>/<Iteration>segments are stripped viaBuildClassificationPath. Empty/whitespace rejected withutil.FlagErrorf.DeleteClassificationNodeArgs.Pathis the only delete-target mechanism; the SDK exposes no NodeId route.--reclassify-id. Maps toDeleteClassificationNodeArgs.ReclassifyId. When set, work items are moved to that node before deletion.$reclassifyIdquery param.--reclassify-pathflag in v1. SDK only supportsReclassifyId(int), not a path.--idflag. The SDK'sDeleteClassificationNodeArgshas no NodeId field; only path-based deletion is supported.--forceflag. Use--yesto skip confirmation (existing pattern inpipelines/variablegroup/delete,serviceendpoint/delete,security/permission/delete).--yes, -yviaBoolVarP. Non-TTY + no--yesreturnsutil.FlagErrorf("--yes required when not running interactively")— mirrorsinternal/cmd/pipelines/variablegroup/delete/delete.go:125.prompter.Confirm(...). User cancel returnsutil.ErrCancel. Stop the progress indicator before the prompt and restart after — mirrorsinternal/cmd/pipelines/variablegroup/delete/delete.go:127,141.d,del,rmper AGENTS.md destructive convention.shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path)(note:"Iteration"is the singular scope name matching list/create). URL-escapes segments; returns the path portion.StructureGrouphardcoded to&workitemtracking.TreeStructureGroupValues.Iterations(literal"iterations"). No--structure-groupflag.deleteResult{Deleted bool, Path string, ReclassifyID *int}viautil.AddJSONFlags.ReclassifyIDis*intwithomitemptyso it only appears when the flag was set.internal/cmd/pipelines/variablegroup/delete/delete.go:25-28.Deleted iteration: <path>viafmt.Fprintf(ios.Out, ...). When--reclassify-idis set, append\nReclassified work items to: <id>.internal/cmd/pipelines/variablegroup/delete/delete.go:168simplicity.canceledevent when user declines.deletepackage + 3 LOC inproject.goto wire it.DeleteClassificationNodeis already generated atinternal/mocks/workitemtracking_client_mock.go:196-207. Do not regenerate.Command Signature
d,del,rmutil.ParseProjectScope(ctx, scopeArg)(defined ininternal/cmd/util/scope.go:78-108); errors wrapped withutil.FlagErrorWrap.Flags (mapped to SDK/REST)
--path(required)DeleteClassificationNodeArgs.Path(URL segment, node's own path)<Project>/<Iteration>segments stripped--reclassify-id(optional)DeleteClassificationNodeArgs.ReclassifyId(int, REST$reclassifyIdquery param)--yes, -y(optional)JSON Output Contract
View struct (only used in JSON path; the SDK returns only
error):Default output: single line
Deleted iteration: <path>(Decision 13). When--reclassify-idis set, append\nReclassified work items to: <id>.Command Wiring
internal/cmd/boards/iteration/project/deletedelete.go—NewCmd(ctx util.CmdContext) *cobra.Command+deleteOptions+runDeletedelete_test.go— table-driven gomock testsinternal/cmd/boards/iteration/project/project.go:boards→iteration→project→delete.Code Skeleton (canonical, copy verbatim)
§1.
deleteOptionsstruct§2.
NewCmdshape (no surprises)§3.
runDeleteskeleton§4. Test fixture (copy from
workitem/list/list_test.go:765-844; add prompter mock fromsecurity/permission/delete)(The fixture takes a
canPromptbool so individual tests can flip TTY state — required for the confirmation tests.)API Surface
Reuse the already-vendored client. No new SDK clients required.
workitemtracking.Client.DeleteClassificationNode→ Classification Nodes - Delete (REST 7.1)workitemtracking.TreeStructureGroupValues.Iterations="iterations"(lowercase; atvendor/.../workitemtracking/models.go:967)Mock for
DeleteClassificationNodeis already generated atinternal/mocks/workitemtracking_client_mock.go:196-207. 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-844and the prompter mock wiring frominternal/cmd/security/permission/delete/delete_test.go. Adddelete_test.gowith the following table-driven / behaviour tests, all usingt.Parallel()andgomock(requirefor preconditions,assertfor verifications):TestNewCmd_RegistersAsDeleteLeaf— assertscmd.Name() == "delete",cmd.Aliasescontainsd,del,rm,cmd.Usestarts withdelete [ORGANIZATION/]PROJECT.TestNewCmd_PathFlagRequired— runscmd.SetArgs([]string{"Fabrikam"})+cmd.Execute(); asserts cobraMarkFlagRequirederror mentioningpath.TestRunDelete_EmptyPathFlag— sets--path " "; assertsutil.FlagErrorfreturned with--path must not be empty.TestRunDelete_RootNode_Rejected— sets--path ""and--path "Fabrikam/Iteration"(root only); assertsutil.FlagErrorfbecausenodePathends up empty.TestRunDelete_DefaultScope— sets--path "Sprint 1" --yes(non-TTY); asserts capturedargs.Path == "Sprint%201",*args.StructureGroup == TreeStructureGroupValues.Iterations(literal"iterations"),*args.Project == "Fabrikam",args.ReclassifyId == nil.TestRunDelete_NestedPath— sets--path "Release 2025/Sprint 1" --yes; assertsargs.Path == "Release%202025/Sprint%201".TestRunDelete_PathNormalizationStripsProjectAndIteration— sets--path "Fabrikam/Iteration/Release 2025/Sprint 1" --yes; assertsargs.Path == "Release%202025/Sprint%201".TestRunDelete_PathURLEscaping— sets--path "My Sprint/Sub Sprint" --yes; assertsargs.Path == "My%20Sprint/Sub%20Sprint".TestRunDelete_StructureGroupIsIterations— asserts*args.StructureGroup == workitemtracking.TreeStructureGroupValues.Iterations(literal"iterations").TestRunDelete_ReclassifyId_DefaultNil— no--reclassify-id; assertsargs.ReclassifyId == nil.TestRunDelete_ReclassifyId_Set— sets--reclassify-id 42 --yes; asserts*args.ReclassifyId == 42.TestRunDelete_ProjectScopeParsing— table-driven:[ORG/]PROJECT,PROJECT, invalid ("org/proj/extra"), empty.TestRunDelete_InvalidProjectScope— assertsutil.FlagErrorWrapreturned.TestRunDelete_ClientFactoryError— stubs factory to return error; asserts wrapped error.TestRunDelete_SDKError— stubs SDK to return error; asserts wrapped error.TestRunDelete_YesFlag_SkipsPrompt(TTY,--yes) — assertsprompter.Confirmis not called, SDK is called.TestRunDelete_ConfirmationPrompt_Yes(TTY, no--yes, user confirms) — assertsprompter.Confirmcalled with deletion prompt; SDK is called.TestRunDelete_ConfirmationPrompt_No(TTY, no--yes, user declines) — assertsprompter.Confirmreturns false; SDK is not called; returnsutil.ErrCancel.TestRunDelete_NonTTY_NoYes_ReturnsError(non-TTY, no--yes) — assertsutil.FlagErrorfwith--yes required; SDK is not called.TestRunDelete_DefaultOutput— asserts stdout containsDeleted iteration: Sprint 1\n.TestRunDelete_DefaultOutput_WithReclassify— asserts stdout containsDeleted iteration: Sprint 1\nReclassified work items to: 42\n.TestRunDelete_JSONOutput— sets--json; asserts JSON contains{"deleted":true,"path":"Sprint 1"}and noreclassifyIdkey.TestRunDelete_JSONOutput_WithReclassify— sets--json --reclassify-id 42; asserts JSON contains{"deleted":true,"path":"Sprint 1","reclassifyId":42}.TestRunDelete_OrganizationFromConfigDefault— when scopeArg isPROJECT(no org), assertsclientFact.WorkItemTracking(ctx, defaultOrg)is called with the configured default.Phase 2 — GREEN (minimal implementation). Strict reuse rules:
runDelete(already short).util.ParseProjectScope,util.AddJSONFlags,util.FlagErrorf/FlagErrorWrap,util.ExactArgs,types.GetValue,types.ToPtr,util.ErrCancel,ios.StartProgressIndicator/StopProgressIndicator,iostreams.Testas-is.shared.BuildClassificationPath(scope.Project, true, "Iteration", opts.path)for path normalization; the helper already URL-escapes segments.wit.DeleteClassificationNode(ctx.Context(), args)withargs.Project,args.StructureGroup = &workitemtracking.TreeStructureGroupValues.Iterations,args.Path, and conditionallyargs.ReclassifyId.ios.StartProgressIndicator()+defer ios.StopProgressIndicator(); callios.StopProgressIndicator()before the confirmation prompt andios.StartProgressIndicator()after a confirmed prompt (mirrorsinternal/cmd/pipelines/variablegroup/delete/delete.go:127,141).!opts.yesand!ios.CanPrompt(); on cancel, returnutil.ErrCancel.opts.exporter.Write(ios, deleteResult{...})(constructed inrunDelete, NOT from the SDK return — the SDK returns onlyerror); default viafmt.Fprintf(ios.Out, ...).canceledevent when the user declines.Target delta:
delete.go≤ ~140 LOC,delete_test.go≤ ~500 LOC (22 tests), parentproject.go+3 LOC,docs/boards_iteration_project_delete.mdregenerated viamake docs. No changes tolist.go,create.go, oriteration.go.Tooling and Verification Checklist
gofmt/gofumpton touched filesgo test ./internal/cmd/boards/iteration/...go test ./...make lintmake docsReference Existing Patterns
#206(area delete) — the primary mirror; same SDK call, same confirmation pattern, same JSON shape.internal/cmd/pipelines/variablegroup/delete/delete.go—--yesconfirmation pattern (Decision 7-8, 15); JSON view struct; single-line success output.internal/cmd/security/permission/delete/delete.go—prompter.Confirmpattern.internal/cmd/boards/iteration/project/create/create.go(sibling feat: Implementazdo boards iteration project createcommand #205) — structural template: same path normalization, same client access, same JSON flag registration. Read it to ensure stylistic consistency on iteration wording.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:196-207— mock forDeleteClassificationNode(already generated, do not regenerate).internal/azdo/factory.go—ClientFactory().WorkItemTracking(...)accessor (reuse).References