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 new boards work-item relation umbrella. Hardened spec — do not re-derive decisions. Sibling of #272 (list-type), #273 (remove), #274 (show).
Command Description
Attach a relation (link) from one Azure Boards work item to one or more target work items, or to one or more arbitrary artifact URLs. Mirrors az boards work-item relation add. The command builds a JSON-Patch document containing add operations on /relations/- and sends it to the server.
The REST surface is Work Items - Update (REST 7.1):
The Python reference implementation is at azure-devops/azext_devops/dev/boards/relations.py:21-58 (add_relation function). The function:
Validates that exactly one of --target-id / --target-url is supplied.
Looks up the referenceName of the relation type via GetRelationTypes (case-insensitive friendly name → referenceName).
For each target ID, fetches the target work item via GetWorkItem(id) to obtain its url.
For each target URL, uses the URL directly.
Sends a PATCH with one add op per relation: {op: add, path: /relations/-, value: {rel, url}}.
Fetches the updated work item with expand='All' to populate the response with friendly relation names.
Locked Decisions
#
Decision
Rationale
1
Org-scoped target.Use: "add [ORGANIZATION/]ID". The source work item ID is unique within the organization.
Matches az boards work-item relation add (no project positional). Symmetric with #269 (delete) and #270 (update).
2
Aliases: []string{"a"}.
Per AGENTS.md update command convention.
3
cobra.ExactArgs(1).
One positional target: the source 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. Symmetric with #269 and #270.
5
No --project flag. The PATCH URL is org-scoped (PATCH /_apis/wit/workitems/{id}); the Python never passes a project to client.update_work_item. Pass nil for UpdateWorkItemArgs.Project.
Matches az boards work-item relation add exactly.
6
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. Symmetric with #269 Decision 14 and #270 Decision 6.
7
Exactly one of --target-id / --target-url is required. Reject if both are empty (return util.FlagErrorf with the exact message "--target-id or --target-url must be provided") and reject if both are set (return util.FlagErrorf with "--target-id and --target-url are mutually exclusive; supply only one").
Mirrors Python's if target_id is None and target_url is None: raise CLIError('--target-id or --target-url must be provided').
8
--target-id is repeatable; multiple invocations concatenate. Each ID must parse as a positive integer. Empty entries are skipped.
Mirrors Python's target_id.split(',') semantics with Go-native flag repetition. Decision 8 supersedes the comma-separated form — comma-separated values are also accepted (split on , after collecting) to match az ergonomics, but the primary form is repeated flag invocation.
9
--target-url is repeatable with the same semantics.
Mirrors Python's target_url.split(',').
10
--relation-type is a single friendly name (e.g., parent, child, related, duplicate, successor, predecessor, artifact). Case-insensitive match against the list returned by GetRelationTypes.
Mirrors Python's get_system_relation_name.
11
Invalid relation type surfaces a CLI error with the exact Python message: --relation-type is not valid. Use "azdo boards work-item relation list-type" command to list possible relation types in your project.
No --expand flag. All responses use expand='All' internally. The user does not need to choose.
Mirrors Python's hard-coded expand='All'.
13
No confirmation prompt. The operation is reversible via remove.
Mirrors Python (the commands.py:97 only adds a confirmation to remove, not add).
14
For each target ID, call GetWorkItem(id) to obtain its url and use that URL in the patch op. If the call fails, return the wrapped SDK error. This mirrors the Python's query_by_wiql validation but uses GetWorkItem per ID (cheaper for small counts; deterministic for tests).
Matches Python's intent: reject invalid target IDs before sending a PATCH.
15
Validate all target IDs succeed before sending the PATCH. If any GetWorkItem for a target ID fails, abort with the wrapped error from the first failed call.
Mirrors Python's "all or nothing" validation (len(target_work_items) != len(target_work_item_ids): raise CLIError).
16
JSON output passes the raw SDK *WorkItem (the GetWorkItem(id, expand=All) response after the PATCH succeeds) to opts.exporter.Write. Do not introduce a view struct.
Mirrors #203, #269, #270 conventions. The user can filter to relations via --jq.
17
Default output: table with columns TYPE, URL (mirroring Python's transform_work_item_relations columns Relation Type, URL). The table is the work item's relations array after friendly-name resolution.
Friendly-name resolution: before rendering, replace each Rel (referenceName) with its Name using the list returned by GetRelationTypes. This is done in the shared helper internal/cmd/boards/workitem/relation/shared/relation.go (introduced by the umbrella).
Patch doc order is: one op per target, in the order the user supplied them. Tests assert order.
Mirrors Python's append-in-input-order.
Command Signature
varaddCmd=&cobra.Command{
Use: "add [ORGANIZATION/]ID",
Aliases: []string{"a"},
Short: "Add a relation(s) to a work item.",
Long: heredoc.Doc(` Attach one or more relations to an existing work item. The relation type must be one of the friendly names returned by 'list-type'. Targets can be other work items (by ID) or arbitrary artifact URLs. `),
Args: cobra.ExactArgs(1),
RunE: func(cmd*cobra.Command, args []string) error {
returnrunAdd(cmd.Context(), opts, args[0])
},
}
Flags
Flag
Maps to
Notes
--id
not a flag — positional
Source work item ID (positional [ORGANIZATION/]ID).
--relation-type
relationType (friendly name)
Required. Case-insensitive.
--target-id (repeatable)
targetId slice
Required if --target-url is not set.
--target-url (repeatable)
targetUrl slice
Required if --target-id is not set.
--organization
*Path.Organization
Default org from config.
Also add util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "rev", "fields", "url", "_links", "relations", "commentVersionRef"}) (every JSON-tagged field of the returned *workitemtracking.WorkItem).
Code Skeleton (canonical, copy verbatim)
§1. addOptions struct
typeaddOptionsstruct {
targetArgstringrelationTypestring// --relation-typetargetIDs []string// --target-id (repeatable; comma-separated also accepted)targetURLs []string// --target-url (repeatable; comma-separated also accepted)exporter util.Exporter
}
§2. runAdd skeleton
funcrunAdd(cmdCtx util.CmdContext, opts*addOptions, targetArgstring) error {
ios, err:=cmdCtx.IOStreams()
iferr!=nil { returnerr }
ios.StartProgressIndicator()
deferios.StopProgressIndicator()
scope, err:=util.ParseTargetWithDefaultOrganization(cmdCtx, targetArg)
iferr!=nil { returnutil.FlagErrorWrap(err) }
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])
}
// Collect and expand target IDs / URLs.targetIDs, err:=util.SplitAndTrimCSV(opts.targetIDs)
iferr!=nil { returnutil.FlagErrorWrap(err) }
targetURLs, err:=util.SplitAndTrimCSV(opts.targetURLs)
iferr!=nil { returnutil.FlagErrorWrap(err) }
iflen(targetIDs) ==0&&len(targetURLs) ==0 {
returnutil.FlagErrorf("--target-id or --target-url must be provided")
}
iflen(targetIDs) >0&&len(targetURLs) >0 {
returnutil.FlagErrorf("--target-id and --target-url are mutually exclusive; supply only one")
}
for_, tid:=rangetargetIDs {
n, err:=strconv.Atoi(tid)
iferr!=nil||n<=0 {
returnutil.FlagErrorf("target work item ID must be a positive integer; got %q", tid)
}
}
wit, err:=cmdCtx.ClientFactory().WorkItemTracking(cmdCtx.Context(), scope.Organization)
iferr!=nil { returnerr }
relRefName, err:=shared.ResolveRelationType(cmdCtx.Context(), wit, opts.relationType)
iferr!=nil { returnutil.FlagErrorWrap(err) }
// Resolve target IDs to URLs.targetURLsResolved:= []string{}
for_, tid:=rangetargetIDs {
n, _:=strconv.Atoi(tid)
target, err:=wit.GetWorkItem(cmdCtx.Context(), workitemtracking.GetWorkItemArgs{Id: &n})
iferr!=nil {
returnfmt.Errorf("failed to resolve target work item %d: %w", n, err)
}
iftarget.Url==nil||*target.Url=="" {
returnfmt.Errorf("target work item %d has no URL; cannot create relation", n)
}
targetURLsResolved=append(targetURLsResolved, *target.Url)
}
targetURLsResolved=append(targetURLsResolved, targetURLs...)
add:=webapi.OperationValues.Adddoc:= []webapi.JsonPatchOperation{}
for_, u:=rangetargetURLsResolved {
p:="/relations/-"doc=append(doc, webapi.JsonPatchOperation{
Op: &add,
Path: &p,
Value: map[string]any{"rel": relRefName, "url": u},
})
}
res, err:=wit.UpdateWorkItem(cmdCtx.Context(), workitemtracking.UpdateWorkItemArgs{
Document: &doc,
Id: &id,
})
iferr!=nil { returnerr }
// Re-fetch with expand=All to populate relations.expand:=workitemtracking.WorkItemExpandValues.Allpopulated, err:=wit.GetWorkItem(cmdCtx.Context(), workitemtracking.GetWorkItemArgs{
Id: &id,
Expand: &expand,
})
iferr!=nil { returnerr }
ifopts.exporter!=nil {
returnopts.exporter.Write(ios, populated)
}
tp, err:=cmdCtx.Printer("list")
iferr!=nil { returnerr }
tp.AddColumns("TYPE", "URL")
ifpopulated.Relations!=nil {
for_, rel:=range*populated.Relations {
tp.AddField(types.GetValue(rel.Rel, ""))
tp.AddField(types.GetValue(rel.Url, ""))
tp.EndRow()
}
}
returntp.Render()
}
util.SplitAndTrimCSV is a new shared helper at internal/cmd/util/csv.go that splits a []string on , and trims whitespace from each element; returns an error if any element is empty after trimming. If this helper does not yet exist in the umbrella's #138's implementation, the add leaf must create it in internal/cmd/boards/workitem/relation/shared/csv.go.
JSON Output Contract
Pass the raw *workitemtracking.WorkItem returned by the post-PATCH GetWorkItem (with expand=All) to opts.exporter.Write. No view struct.
Use ctx.Printer("list") with 2 columns: TYPE, URL. The TYPE cell is the relation type's friendly name (not referenceName) — friendly-name resolution is performed by the shared helper.
Command Wiring
Create internal/cmd/boards/workitem/relation/add/add.go with package add, factory func NewCmd(ctx util.CmdContext) *cobra.Command.
Add import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/relation/add" to internal/cmd/boards/workitem/relation/relation.go.
Register in relation.go with cmd.AddCommand(add.NewCmd(ctx)).
Reuse internal/cmd/boards/workitem/relation/shared/relation.go (introduced by the umbrella) — shared.ResolveRelationType, shared.PopulateFriendlyNames.
// 1. Resolve friendly relation type name to referenceNamerelTypes, err:=wit.GetRelationTypes(ctx, workitemtracking.GetRelationTypesArgs{})
// 2. Resolve each target ID to its URLtarget, err:=wit.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{Id: &n})
// 3. PATCH the source work itemres, err:=wit.UpdateWorkItem(ctx, workitemtracking.UpdateWorkItemArgs{
Document: &doc,
Id: &id,
})
// 4. Re-fetch with expand=Allexpand:=workitemtracking.WorkItemExpandValues.Allpopulated, err:=wit.GetWorkItem(ctx, workitemtracking.GetWorkItemArgs{
Id: &id,
Expand: &expand,
})
The vendored SDK enforces args.Document != nil and args.Id != nil on UpdateWorkItem — will return ArgumentNilError if missing. No extra validation needed beyond the positive-integer check (Decision 6) and the mutual-exclusion check (Decision 7).
Reference Existing Patterns
internal/cmd/boards/workitem/show — sibling for the org-scoped ParseTargetWithDefaultOrganization usage.
Two add ops using the URLs directly; no GetWorkItem calls for the targets.
Test_runAdd_invalidRelationType
--relation-type bogus.
Returns util.FlagErrorf with the exact message containing "--relation-type is not valid. Use \"azdo boards work-item relation list-type\"". SDK is not called for UpdateWorkItem.
Test_runAdd_invalidSourceID
Positional is "abc".
Returns util.FlagErrorf with "work item ID must be a positive integer; got \"abc\"". SDK is not called.
Test_runAdd_zeroSourceID
Positional is "0".
Returns util.FlagErrorf. SDK is not called.
Test_runAdd_negativeSourceID
Positional is "-5".
Returns util.FlagErrorf. SDK is not called.
Test_runAdd_noTargets
No --target-id and no --target-url.
Returns util.FlagErrorf with "--target-id or --target-url must be provided". SDK is not called.
Test_runAdd_bothTargets
Both --target-id and --target-url set.
Returns util.FlagErrorf with "--target-id and --target-url are mutually exclusive". SDK is not called.
Test_runAdd_invalidTargetID
--target-id -3 or abc.
Returns util.FlagErrorf with "target work item ID must be a positive integer". SDK is not called.
Test_runAdd_targetIDNotFound
Mock GetWorkItem for the target returns 404.
Returns wrapped error containing the target ID; PATCH is not sent.
Test_runAdd_orgScopeOnly
Positional is "1234".
args.Id == ptr(1234), scope.Organization is the default.
Test_runAdd_explicitOrg
Positional is "myorg/1234".
args.Id == ptr(1234), scope.Organization is "myorg".
Test_runAdd_APIError
Mock UpdateWorkItem returns errors.New("boom").
Returns wrapped error including the source ID.
Test_runAdd_success_JSON
--json flag set.
Exporter receives the raw *WorkItem from the post-PATCH GetWorkItem.
Test_runAdd_tableOutput
Default output, mock returns a work item with 2 relations.
Table has 2 rows with the 2 columns populated; TYPE is the friendly name.
Test_runAdd_emptyRelations
Work item has relations = nil.
Table has 0 rows; no error.
Test_runAdd_caseInsensitiveRelationType
--relation-type PARENT (uppercase).
Resolves to System.LinkTypes.Hierarchy-Reverse exactly as --relation-type parent does.
Test_runAdd_patchesInInputOrder
Targets supplied as 4,2,3 (single flag, comma-separated).
Sub-issue of new
boards work-item relationumbrella. Hardened spec — do not re-derive decisions. Sibling of #272 (list-type), #273 (remove), #274 (show).Command Description
Attach a relation (link) from one Azure Boards work item to one or more target work items, or to one or more arbitrary artifact URLs. Mirrors
az boards work-item relation add. The command builds a JSON-Patch document containingaddoperations on/relations/-and sends it to the server.The REST surface is Work Items - Update (REST 7.1):
The Python reference implementation is at
azure-devops/azext_devops/dev/boards/relations.py:21-58(add_relationfunction). The function:--target-id/--target-urlis supplied.referenceNameof the relation type viaGetRelationTypes(case-insensitive friendly name →referenceName).GetWorkItem(id)to obtain itsurl.addop per relation:{op: add, path: /relations/-, value: {rel, url}}.expand='All'to populate the response with friendly relation names.Locked Decisions
Use: "add [ORGANIZATION/]ID". The source work item ID is unique within the organization.az boards work-item relation add(no project positional). Symmetric with #269 (delete) and #270 (update).Aliases: []string{"a"}.cobra.ExactArgs(1).util.ParseTargetWithDefaultOrganization(ctx, args[0]).scope.Targets[0]is the ID string.--projectflag. The PATCH URL is org-scoped (PATCH /_apis/wit/workitems/{id}); the Python never passes a project toclient.update_work_item. PassnilforUpdateWorkItemArgs.Project.az boards work-item relation addexactly.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.--target-id/--target-urlis required. Reject if both are empty (returnutil.FlagErrorfwith the exact message"--target-id or --target-url must be provided") and reject if both are set (returnutil.FlagErrorfwith"--target-id and --target-url are mutually exclusive; supply only one").if target_id is None and target_url is None: raise CLIError('--target-id or --target-url must be provided').--target-idis repeatable; multiple invocations concatenate. Each ID must parse as a positive integer. Empty entries are skipped.target_id.split(',')semantics with Go-native flag repetition. Decision 8 supersedes the comma-separated form — comma-separated values are also accepted (split on,after collecting) to matchazergonomics, but the primary form is repeated flag invocation.--target-urlis repeatable with the same semantics.target_url.split(',').--relation-typeis a single friendly name (e.g.,parent,child,related,duplicate,successor,predecessor,artifact). Case-insensitive match against the list returned byGetRelationTypes.get_system_relation_name.--relation-type is not valid. Use "azdo boards work-item relation list-type" command to list possible relation types in your project.get_system_relation_name(relations.py:131-135).--expandflag. All responses useexpand='All'internally. The user does not need to choose.expand='All'.remove.commands.py:97only adds a confirmation toremove, notadd).GetWorkItem(id)to obtain itsurland use that URL in the patch op. If the call fails, return the wrapped SDK error. This mirrors the Python'squery_by_wiqlvalidation but usesGetWorkItemper ID (cheaper for small counts; deterministic for tests).GetWorkItemfor a target ID fails, abort with the wrapped error from the first failed call.len(target_work_items) != len(target_work_item_ids): raise CLIError).*WorkItem(theGetWorkItem(id, expand=All)response after the PATCH succeeds) toopts.exporter.Write. Do not introduce a view struct.relationsvia--jq.TYPE, URL(mirroring Python'stransform_work_item_relationscolumnsRelation Type, URL). The table is the work item'srelationsarray after friendly-name resolution.transform_work_item_relations(_format.py:14-24).Rel(referenceName) with itsNameusing the list returned byGetRelationTypes. This is done in the shared helperinternal/cmd/boards/workitem/relation/shared/relation.go(introduced by the umbrella).fill_friendly_name_for_relations_in_work_item(relations.py:107-115).relations(no relations before or after) renders as an empty table — no error.if result['relations'] is None: return [].workitemtracking.Client.{UpdateWorkItem, GetWorkItem, GetRelationTypes}.vendor/.../v7/workitemtracking/client.go:205, 137, 113.internal/mocks/workitemtracking_client_mock.go:849 (GetWorkItem), 669 (GetRelationTypes), 1358 (UpdateWorkItem)already exist.go mod tidy/go mod vendor/scripts/generate_mocks.shwork.--organizationflag; organization comes fromParseTargetWithDefaultOrganizationor default config.azergonomics.--detectflag.azused this for autodetecting the organization from a.azuredevopsconfig; we resolve viaParseTargetWithDefaultOrganization.azdoconventions; not inazdoscope.JsonPatchOperationvalue is wrapped beyond{rel, url}. Noattributes, nonullfor missing fields._create_patch_operation(relations.py:138-146).Command Signature
Flags
--id[ORGANIZATION/]ID).--relation-typerelationType(friendly name)--target-id(repeatable)targetIdslice--target-urlis not set.--target-url(repeatable)targetUrlslice--target-idis not set.--organization*Path.OrganizationAlso add
util.AddJSONFlags(cmd, &opts.exporter, []string{"id", "rev", "fields", "url", "_links", "relations", "commentVersionRef"})(every JSON-tagged field of the returned*workitemtracking.WorkItem).Code Skeleton (canonical, copy verbatim)
§1.
addOptionsstruct§2.
runAddskeletonutil.SplitAndTrimCSVis a new shared helper atinternal/cmd/util/csv.gothat splits a[]stringon,and trims whitespace from each element; returns an error if any element is empty after trimming. If this helper does not yet exist in the umbrella's #138's implementation, theaddleaf must create it ininternal/cmd/boards/workitem/relation/shared/csv.go.JSON Output Contract
Pass the raw
*workitemtracking.WorkItemreturned by the post-PATCHGetWorkItem(withexpand=All) toopts.exporter.Write. No view struct.JSON fields exposed:
id,rev,fields,url,_links,relations,commentVersionRef.Table Output Contract
Use
ctx.Printer("list")with 2 columns:TYPE, URL. TheTYPEcell is the relation type's friendly name (notreferenceName) — friendly-name resolution is performed by the shared helper.Command Wiring
internal/cmd/boards/workitem/relation/add/add.gowithpackage add, factoryfunc NewCmd(ctx util.CmdContext) *cobra.Command.import "github.com/tmeckel/azdo-cli/internal/cmd/boards/workitem/relation/add"tointernal/cmd/boards/workitem/relation/relation.go.relation.gowithcmd.AddCommand(add.NewCmd(ctx)).internal/cmd/boards/workitem/relation/shared/relation.go(introduced by the umbrella) —shared.ResolveRelationType,shared.PopulateFriendlyNames.API Surface
Vendored SDK calls (from
vendor/.../v7/workitemtracking/client.go):The vendored SDK enforces
args.Document != nilandargs.Id != nilonUpdateWorkItem— will returnArgumentNilErrorif missing. No extra validation needed beyond the positive-integer check (Decision 6) and the mutual-exclusion check (Decision 7).Reference Existing Patterns
internal/cmd/boards/workitem/show— sibling for the org-scopedParseTargetWithDefaultOrganizationusage.internal/cmd/boards/workitem/shared/description.go(introduced by feat: Implementazdo boards work-item createcommand #203) — reference for the shared helper pattern.internal/cmd/security/group/update/update.go:103-124— reference for the 4-line patch-doc append pattern.internal/cmd/util/flag_error.go—util.FlagErrorffor the CLI error formatting (used in Decision 6 and Decision 7).Reference implementations (for UX parity only —
azdois the implementation target):azure-devops/azext_devops/dev/boards/relations.py:21-58-add_relationfunction. Confirms: org-scoped,--target-idand--target-urlmutually exclusive, comma-separated targets,expand='All'after write, friendly-name fill.azure-devops/azext_devops/dev/boards/relations.py:131-135-get_system_relation_namefunction. Confirms: case-insensitive match onname, error message wording.azure-devops/azext_devops/dev/boards/_format.py:14-24-transform_work_item_relationsconfirms table columnsRelation Type, URL.expand: relationsparameter.TDD Test Plan
TestNewCmd_addUse == "add [ORGANIZATION/]ID",Aliases == ["a"],Args == cobra.ExactArgs(1).Test_runAdd_minimal--relation-type parent --target-id 2. Mock returns aWorkItemwith one relation.addop on/relations/-withvalue.rel = "System.LinkTypes.Hierarchy-Reverse"andvalue.url = "https://dev.azure.com/.../workitems/2".Test_runAdd_multipleTargetIDs--target-id 2 --target-id 3.addops in order.Test_runAdd_multipleTargetsSingleFlag--target-id 2,3,4(comma-separated).addops in order.Test_runAdd_targetURLs--relation-type artifact --target-url https://.../1 --target-url https://.../2.addops using the URLs directly; noGetWorkItemcalls for the targets.Test_runAdd_invalidRelationType--relation-type bogus.util.FlagErrorfwith the exact message containing"--relation-type is not valid. Use \"azdo boards work-item relation list-type\"". SDK is not called forUpdateWorkItem.Test_runAdd_invalidSourceID"abc".util.FlagErrorfwith"work item ID must be a positive integer; got \"abc\"". SDK is not called.Test_runAdd_zeroSourceID"0".util.FlagErrorf. SDK is not called.Test_runAdd_negativeSourceID"-5".util.FlagErrorf. SDK is not called.Test_runAdd_noTargets--target-idand no--target-url.util.FlagErrorfwith"--target-id or --target-url must be provided". SDK is not called.Test_runAdd_bothTargets--target-idand--target-urlset.util.FlagErrorfwith"--target-id and --target-url are mutually exclusive". SDK is not called.Test_runAdd_invalidTargetID--target-id -3orabc.util.FlagErrorfwith"target work item ID must be a positive integer". SDK is not called.Test_runAdd_targetIDNotFoundGetWorkItemfor the target returns 404.Test_runAdd_orgScopeOnly"1234".args.Id == ptr(1234), scope.Organization is the default.Test_runAdd_explicitOrg"myorg/1234".args.Id == ptr(1234), scope.Organization is"myorg".Test_runAdd_APIErrorUpdateWorkItemreturnserrors.New("boom").Test_runAdd_success_JSON--jsonflag set.*WorkItemfrom the post-PATCHGetWorkItem.Test_runAdd_tableOutputTYPEis the friendly name.Test_runAdd_emptyRelationsrelations = nil.Test_runAdd_caseInsensitiveRelationType--relation-type PARENT(uppercase).System.LinkTypes.Hierarchy-Reverseexactly as--relation-type parentdoes.Test_runAdd_patchesInInputOrder4,2,3(single flag, comma-separated).[4, 2, 3].References
vendor/.../v7/workitemtracking/client.go:113 (GetRelationTypes), :137 (GetWorkItem), :205 (UpdateWorkItem).vendor/.../v7/workitemtracking/models.go:1425 (WorkItemRelation), :1435 (WorkItemRelationType).internal/mocks/workitemtracking_client_mock.go:669 (GetRelationTypes), 849 (GetWorkItem), 1358 (UpdateWorkItem).internal/azdo/factory.go:149—ClientFactory().WorkItemTracking(ctx, organization).7.1-preview.3).boards work-item relationsubgroup. Sibling: Implementazdo boards work-item relation addcommand #272, Implementazdo boards work-item relation list-typecommand #273, Implementazdo boards work-item relation removecommand #274.internal/cmd/boards/workitem/create(feat: Implementazdo boards work-item createcommand #203),internal/cmd/boards/workitem/update(feat: Implementazdo boards work-item updatecommand #270).