{"record":{"id":"2c9f3bb3c313ce0a","repo":"gastownhall/beads","slug":"failed-to-add-work-item-link-w","errorCode":null,"errorMessage":"failed to add work item link: %w","messagePattern":"failed to add work item link: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/ado/client.go","lineNumber":545,"sourceCode":"// The comment parameter sets the relation comment attribute; pass \"\" for no comment.\nfunc (c *Client) AddWorkItemLink(ctx context.Context, sourceID int, targetURL, linkType, comment string) error {\n\tops := []PatchOperation{\n\t\t{\n\t\t\tOp:   \"add\",\n\t\t\tPath: \"/relations/-\",\n\t\t\tValue: map[string]interface{}{\n\t\t\t\t\"rel\": linkType,\n\t\t\t\t\"url\": targetURL,\n\t\t\t\t\"attributes\": map[string]interface{}{\n\t\t\t\t\t\"comment\": comment,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\turlStr := addAPIVersion(fmt.Sprintf(\"%s/wit/workitems/%d\", c.apiBase(), sourceID))\n\t_, err := c.doRequest(ctx, http.MethodPatch, urlStr, \"application/json-patch+json\", ops)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add work item link: %w\", err)\n\t}\n\treturn nil\n}\n\n// RemoveWorkItemLink removes a relation link by index from the given work item.\nfunc (c *Client) RemoveWorkItemLink(ctx context.Context, sourceID, relationIndex int) error {\n\tops := []PatchOperation{\n\t\t{\n\t\t\tOp:   \"remove\",\n\t\t\tPath: fmt.Sprintf(\"/relations/%d\", relationIndex),\n\t\t},\n\t}\n\turlStr := addAPIVersion(fmt.Sprintf(\"%s/wit/workitems/%d\", c.apiBase(), sourceID))\n\t_, err := c.doRequest(ctx, http.MethodPatch, urlStr, \"application/json-patch+json\", ops)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to remove work item link: %w\", err)\n\t}\n\treturn nil","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/ado/client.go#L527-L563","documentation":"This error wraps any failure returned by the underlying HTTP layer when AddWorkItemLink PATCHes a JSON-Patch 'add /relations/-' operation to the Azure DevOps work item identified by sourceID. It is a wrapped error (%w), so the original cause — auth failure, 404, network error, invalid relation payload — is preserved and reachable via errors.Is/As. The library throws it to give a single, recognizable message at the 'add link' call boundary.","triggerScenarios":"Calling Client.AddWorkItemLink(ctx, sourceID, ...) when doRequest fails: invalid/expired PAT (401), nonexistent sourceID (404), malformed relation URL/target ID rejected by ADO (400), network/DNS failure, or server 5xx after doRequest retries are exhausted.","commonSituations":"Linking a work item to another whose ID was mistyped; a PAT that lacks Work Items write scope; org/project URL misconfiguration in the client; ADO returning 400 because the relation 'url' field is not a fully-qualified work item URL.","solutions":["Inspect the wrapped cause with errors.As / %v on the returned error to get ADO's status code and message","Verify the PAT is valid and has 'Work Items (Read & Write)' scope for the organization","Confirm sourceID is an existing work item ID in the configured project/org (GET the work item first)","Check the relation payload: the 'url' attribute must be the full ADO API URL of the target work item","Verify the client's org/project base URL configuration (apiBase) points at the right organization"],"exampleFix":"// before\nif err := client.AddWorkItemLink(ctx, 42, rel); err != nil {\n  return err\n}\n// after\nif err := client.AddWorkItemLink(ctx, 42, rel); err != nil {\n  var httpErr *ado.HTTPError\n  if errors.As(err, &httpErr) && httpErr.StatusCode == 404 {\n    return fmt.Errorf(\"work item 42 not found: %w\", err)\n  }\n  return err\n}","handlingStrategy":"try-catch","validationCode":"// preflight: ensure the source work item exists and target URL is well-formed\nwi, err := client.GetWorkItem(ctx, sourceID)\nif err != nil {\n  return fmt.Errorf(\"source work item %d not accessible: %w\", sourceID, err)\n}\nu, err := url.Parse(rel.URL)\nif err != nil || !strings.Contains(u.Path, \"/_apis/wit/workItems/\") {\n  return fmt.Errorf(\"invalid relation url %q\", rel.URL)\n}","typeGuard":"func asHTTPError(err error) (statusCode int, ok bool) {\n  var he interface{ StatusCode() int }\n  if errors.As(err, &he) {\n    return he.StatusCode(), true\n  }\n  return 0, false\n}","tryCatchPattern":"if err := client.AddWorkItemLink(ctx, sourceID, rel); err != nil {\n  var transient interface{ Temporary() bool }\n  if errors.As(err, &transient) && transient.Temporary() {\n    // retry with backoff\n  }\n  return fmt.Errorf(\"add link to %d: %w\", sourceID, err)\n}","preventionTips":["GET the work item before linking to confirm the source ID exists","Always pass the fully-qualified ADO API URL of the target work item in the relation","Keep the PAT scoped to Work Items Read & Write and rotate before expiry","Treat 429/5xx causes as retryable with exponential backoff"],"tags":["azure-devops","http","work-items","error-wrapping"],"backgroundTag":"http-request-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}