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 #138. Sibling of #136 (work-item list), #203 (work-item create), #238 (work-item show).
Command Description
Delete a single Azure Boards work item by ID. Without --destroy, the work item is moved to the Recycle Bin and can be restored via the Azure DevOps web UI. With --destroy, the work item is permanently removed and cannot be recovered. Mirrors az boards work-item delete.
The REST surface is Work Items - Delete (REST 7.1):
Response is a WorkItemDelete object (id, code, deletedBy, deletedDate, message, name, project, type, url, resource).
Locked Decisions
#
Decision
Rationale
1
Org-scoped target.Use: "delete [ORGANIZATION/]ID". The work item ID is unique within the organization; a project segment is not required.
Matches az boards work-item delete (no project in positional) and internal/cmd/boards/workitem/show (org-scoped).
2
Aliases: []string{"d", "del", "rm"}.
Per AGENTS.md destructive command convention.
3
cobra.ExactArgs(1).
One positional target: the work item ID.
4
Parse the single positional with util.ParseTargetWithDefaultOrganization(ctx, args[0]). scope.Targets[0] is the ID string.
Existing helper handles 1- and 2-segment forms.
5
Project is an optional --project flag, not part of the positional. Default empty. When set, passed to DeleteWorkItemArgs.Project; when empty, the SDK receives nil and the REST URL omits the /project/ segment.
Matches the Python's delete_work_item which always passes project (possibly empty). Keeps the positional ergonomic (delete 1234 --yes).
6
Destructive: requires confirmation unless --yes is provided. Prompt text uses the same wording as the Python: "Are you sure you want to delete this work item?". When --destroy is set, the prompt text changes to "Are you sure you want to permanently destroy this work item? This cannot be undone.".
Mirrors the AzDO Extension's delete_work_item confirmation. On cancellation, return util.ErrCancel.
7
--destroy is an optional bool flag (default false). When set, DeleteWorkItemArgs.Destroy = &true and the REST query param is ?destroy=true.
Matches Python --destroy. Permanent deletion is the explicit-opt-in escalation.
8
Plain output: print Deleted work item <ID> to stdout. When --destroy is set, print Permanently deleted work item <ID>.
Mirrors the Python's print('Deleted work item {}'.format(id)).
9
JSON output: pass the raw *workitemtracking.WorkItemDelete returned by DeleteWorkItem directly to opts.exporter.Write. No view struct.
The SDK type is the API surface. Symmetric with #203 (create).
10
No table output. Delete returns a single status (the WorkItemDelete envelope), not a work item. The plain text message is the right human-readable surface; --json covers structured needs.
No pre-check that the work item exists. The REST 404 is surfaced via util.FlagErrorWrap.
Saves a round-trip. REST error is already clear.
14
ID must parse as a positive integer. If strconv.Atoi(scope.Targets[0]) fails or returns <= 0, return util.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0]) and do not call the SDK.
Defends against typo'd IDs without a wasted round-trip.
15
No go mod tidy / go mod vendor / scripts/generate_mocks.sh work.
Command Signature
vardeleteCmd=&cobra.Command{
Use: "delete [ORGANIZATION/]ID",
Aliases: []string{"d", "del", "rm"},
Short: "Delete a work item.",
Long: heredoc.Doc(` Delete a work item by ID. By default the work item is moved to the Recycle Bin and can be restored via the Azure DevOps web UI. Use --destroy to permanently remove the work item; this cannot be undone. `),
Args: cobra.ExactArgs(1),
RunE: func(cmd*cobra.Command, args []string) error {
returnrunDelete(cmd.Context(), opts, args[0])
},
}
Flags
Flag
Type
Default
Required
Description
--yes
bool
false
no
Skip the confirmation prompt.
--destroy
bool
false
no
Permanently delete the work item (bypasses Recycle Bin).
--project
string
""
no
Project name or ID. Routes the REST call through /project/_apis/wit/workitems/{id}. Omit to use the org-level URL.
--organization
string
$AZDO_ORGANIZATION or default config
no
Azure DevOps organization. Omit to use the default.
Also add util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "code", "deletedBy", "deletedDate", "message", "name", "project", "type", "url", "resource"}) (every JSON-tagged field of *workitemtracking.WorkItemDelete).
ID Validation
After positional parsing, before the confirmation prompt:
id, err:=strconv.Atoi(scope.Targets[0])
iferr!=nil||id<=0 {
returnutil.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0])
}
Confirmation Prompt
When --yes is not set:
if!opts.yes {
if!ios.CanPrompt() {
returnutil.FlagErrorf("--yes required when not running interactively")
}
ios.StopProgressIndicator()
prompter, err:=cmdCtx.Prompter()
iferr!=nil {
returnerr
}
message:="Are you sure you want to delete this work item?"ifopts.destroy {
message="Are you sure you want to permanently destroy this work item? This cannot be undone."
}
confirmed, err:=prompter.Confirm(message, false)
iferr!=nil { returnerr }
if!confirmed {
zap.L().Debug("work item deletion canceled by user", zap.Int("workItemId", id))
returnutil.ErrCancel
}
ios.StartProgressIndicator()
}
JSON Output Contract
Pass the raw *workitemtracking.WorkItemDelete returned by DeleteWorkItem to opts.exporter.Write. No view struct.
args:= workitemtracking.DeleteWorkItemArgs{
Id: &id, // int parsed from scope.Targets[0]Destroy: &opts.destroy,
}
ifopts.project!="" {
args.Project=&opts.project
}
res, err:=wit.DeleteWorkItem(ctx, args)
iferr!=nil {
returnfmt.Errorf("failed to delete work item %d: %w", id, err)
}
The vendored SDK enforces args.Id != nil — will return ArgumentNilError if missing. No extra validation needed in our wrapper beyond the positive-integer check (Decision 14).
Reference Existing Patterns
internal/cmd/pipelines/variablegroup/delete/delete.go:30-170 — primary reference. Mirrors the Use/Aliases, the confirmation prompt lifecycle, the ios.StopProgressIndicator/ios.StartProgressIndicator dance, the util.ErrCancel return path, and the JSON output split.
Azure DevOps REST API: Work Items - Delete (DELETE, route id 1e4b52db-afe4-4d8b-b1b5-95aa0e13a3b0, API version 7.1-preview.3). Optional destroy query param.
Sub-issue of #138. Sibling of #136 (
work-item list), #203 (work-item create), #238 (work-item show).Command Description
Delete a single Azure Boards work item by ID. Without
--destroy, the work item is moved to the Recycle Bin and can be restored via the Azure DevOps web UI. With--destroy, the work item is permanently removed and cannot be recovered. Mirrorsaz boards work-item delete.The REST surface is Work Items - Delete (REST 7.1):
Response is a
WorkItemDeleteobject (id,code,deletedBy,deletedDate,message,name,project,type,url,resource).Locked Decisions
Use: "delete [ORGANIZATION/]ID". The work item ID is unique within the organization; a project segment is not required.az boards work-item delete(no project in positional) andinternal/cmd/boards/workitem/show(org-scoped).Aliases: []string{"d", "del", "rm"}.cobra.ExactArgs(1).util.ParseTargetWithDefaultOrganization(ctx, args[0]).scope.Targets[0]is the ID string.--projectflag, not part of the positional. Default empty. When set, passed toDeleteWorkItemArgs.Project; when empty, the SDK receivesniland the REST URL omits the/project/segment.delete_work_itemwhich always passesproject(possibly empty). Keeps the positional ergonomic (delete 1234 --yes).--yesis provided. Prompt text uses the same wording as the Python:"Are you sure you want to delete this work item?". When--destroyis set, the prompt text changes to"Are you sure you want to permanently destroy this work item? This cannot be undone.".delete_work_itemconfirmation. On cancellation, returnutil.ErrCancel.--destroyis an optional bool flag (defaultfalse). When set,DeleteWorkItemArgs.Destroy = &trueand the REST query param is?destroy=true.--destroy. Permanent deletion is the explicit-opt-in escalation.Deleted work item <ID>to stdout. When--destroyis set, printPermanently deleted work item <ID>.print('Deleted work item {}'.format(id)).*workitemtracking.WorkItemDeletereturned byDeleteWorkItemdirectly toopts.exporter.Write. No view struct.#203 (create).WorkItemDeleteenvelope), not a work item. The plain text message is the right human-readable surface;--jsoncovers structured needs.workitemtracking.Client.DeleteWorkItem.vendor/.../v7/workitemtracking/client.go:865(impl) and:61(interface).internal/mocks/workitemtracking_client_mock.go:281-294already mocksDeleteWorkItem.404is surfaced viautil.FlagErrorWrap.strconv.Atoi(scope.Targets[0])fails or returns<= 0, returnutil.FlagErrorf("work item ID must be a positive integer; got %q", scope.Targets[0])and do not call the SDK.go mod tidy/go mod vendor/scripts/generate_mocks.shwork.Command Signature
Flags
--yesboolfalse--destroyboolfalse--projectstring""/project/_apis/wit/workitems/{id}. Omit to use the org-level URL.--organizationstring$AZDO_ORGANIZATIONor default configAlso add
util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "code", "deletedBy", "deletedDate", "message", "name", "project", "type", "url", "resource"})(every JSON-tagged field of*workitemtracking.WorkItemDelete).ID Validation
After positional parsing, before the confirmation prompt:
Confirmation Prompt
When
--yesis not set:JSON Output Contract
Pass the raw
*workitemtracking.WorkItemDeletereturned byDeleteWorkItemtoopts.exporter.Write. No view struct.JSON fields exposed (matching SDK struct tags):
id,code,deletedBy,deletedDate,message,name,project,type,url,resource.Table Output Contract
N/A. On success, print one line to stdout:
When
--destroyis set:Command Wiring
internal/cmd/boards/workitem/delete/delete.gowithpackage delete, factoryfunc NewCmd(ctx util.CmdContext) *cobra.Command.import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/delete"tointernal/cmd/boards/workitem/workitem.go.workitem.gowithcmd.AddCommand(delete.NewCmd(ctx))and extend the group'sExampleblock.API Surface
Vendored SDK call (from
vendor/.../v7/workitemtracking/client.go:865):The vendored SDK enforces
args.Id != nil— will returnArgumentNilErrorif missing. No extra validation needed in our wrapper beyond the positive-integer check (Decision 14).Reference Existing Patterns
internal/cmd/pipelines/variablegroup/delete/delete.go:30-170— primary reference. Mirrors theUse/Aliases, the confirmation prompt lifecycle, theios.StopProgressIndicator/ios.StartProgressIndicatordance, theutil.ErrCancelreturn path, and the JSON output split.internal/cmd/pipelines/folder/delete/delete.go(issue feat: Implementazdo pipelines folder deletecommand #264) — sibling for the simpler destructive shape (no project-references or--all).internal/cmd/boards/workitem/show— sibling for the org-scopedParseTargetWithDefaultOrganizationusage and the--projectflag pattern.internal/cmd/boards/workitem/create(feat: Implementazdo boards work-item createcommand #203) — sibling for the JSON Patch output conventions (Decision 9: raw SDK type toopts.exporter.Write).TDD Test Plan
TestNewCmd_deleteUse == "delete [ORGANIZATION/]ID",Aliases == ["d", "del", "rm"],Args == cobra.ExactArgs(1).Test_runDelete_success_withYes--yes, mockDeleteWorkItemreturns&workitemtracking.WorkItemDelete{Id: ptr(1234), DeletedDate: ptr("2026-06-05T...")}.Deleted work item 1234to stdout.Test_runDelete_success_destroy--yes --destroy, mock returns same.Permanently deleted work item 1234.args.Destroy == &trueis passed to the SDK.Test_runDelete_success_withProject--project=Fabrikam --yes, mock returns same.args.Project == ptr("Fabrikam")is passed to the SDK.Test_runDelete_success_noProject--yes, no--project.args.Project == nilis passed to the SDK.Test_runDelete_success_confirmedtrue.Deleted work item 1234.Test_runDelete_cancelledfalse.util.ErrCancel. The SDK is not called.Test_runDelete_notInteractiveios.CanPrompt() == false, no--yes.util.FlagErrorf("--yes required when not running interactively"). SDK is not called.Test_runDelete_invalidID"abc".util.FlagErrorfwith"work item ID must be a positive integer; got \"abc\"". SDK is not called.Test_runDelete_zeroID"0".util.FlagErrorfwith the same message. SDK is not called.Test_runDelete_negativeID"-5".util.FlagErrorf. SDK is not called.Test_runDelete_orgScopeOnly"1234"(no org). MockedcmdCtx.Config()returns a default org.args.Id == ptr(1234), scope.Organization is the default.Test_runDelete_explicitOrg"myorg/1234".args.Id == ptr(1234), scope.Organization is"myorg".Test_runDelete_APIErrorerrors.New("boom").--yes.Test_runDelete_success_JSON--jsonflag set, mock returns*WorkItemDelete.*WorkItemDelete(assertres.Id == ptr(1234)).Test_runDelete_destroyPromptText--destroy, prompter stubbed to returntrue. Capture the prompt message."permanently destroy"and"cannot be undone".Test_runDelete_missingIDazdo boards work-item deletewith no args.References
vendor/.../v7/workitemtracking/client.go:865(DeleteWorkItem),:898(DeleteWorkItemArgs).vendor/.../v7/workitemtracking/models.go:1096(WorkItemDelete).internal/mocks/workitemtracking_client_mock.go:281-294(DeleteWorkItem).internal/azdo/factory.go:149—ClientFactory().WorkItemTracking(ctx, organization).1e4b52db-afe4-4d8b-b1b5-95aa0e13a3b0, API version7.1-preview.3). Optionaldestroyquery param.azdo boards work-itemcommand group #138.azdo boards work-item listcommand #136 (list), feat: Implementazdo boards work-item createcommand #203 (create), feat: Implementazdo boards work-item showcommand #238 (show).internal/cmd/pipelines/variablegroup/delete/delete.go(destructive confirmation, JSON split, success message).