{"record":{"id":"caa820fa5f5aa1e7","repo":"gastownhall/beads","slug":"expected-assignee-must-name-a-holder-there-is-no","errorCode":null,"errorMessage":"expected assignee must name a holder; there is no release of an unheld issue","messagePattern":"expected assignee must name a holder; there is no release of an unheld issue","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/release.go","lineNumber":37,"sourceCode":"// (issueops.Releaser.Release); that body is\n// internal/storage/issueops.ReleaseIssueInTx, which all three legs reach.\n\n// ValidateReleaseRequest applies the request rules every Releaser\n// implementation shares.\nfunc ValidateReleaseRequest(in issueops.ReleaseRequest) error {\n\tif strings.TrimSpace(in.Actor) == \"\" {\n\t\treturn fmt.Errorf(\"%w: release requires an actor to attribute it to\", issueops.ErrValidation)\n\t}\n\tif strings.TrimSpace(in.IssueID) == \"\" {\n\t\treturn fmt.Errorf(\"%w: release requires an issue id\", issueops.ErrValidation)\n\t}\n\tif in.ExpectedAssignee != nil {\n\t\t// A non-nil pointer to \"\" is NOT \"expected unassigned\" here, unlike\n\t\t// UpdateRequest.ExpectedAssignee: releasing a row nobody holds is not a\n\t\t// release, and the raw seam beneath this role refuses the empty\n\t\t// expectation in as many words.\n\t\tif strings.TrimSpace(*in.ExpectedAssignee) == \"\" {\n\t\t\treturn fmt.Errorf(\"%w: expected assignee must name a holder; there is no release of an unheld issue\",\n\t\t\t\tissueops.ErrValidation)\n\t\t}\n\t\t// The two are answers to the same question and they disagree, which is\n\t\t// the rule UpdateRequest states for ForceAssigneeTransfer beside its\n\t\t// own ExpectedAssignee.\n\t\tif in.Force {\n\t\t\treturn fmt.Errorf(\"%w: force releases whoever holds the issue and expected-assignee releases only a named holder; a request cannot ask for both\",\n\t\t\t\tissueops.ErrValidation)\n\t\t}\n\t}\n\treturn nil\n}\n","sourceCodeStart":19,"sourceCodeEnd":50,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/release.go#L19-L50","documentation":"ValidateReleaseRequest refuses an ExpectedAssignee that is a non-nil pointer to an empty/whitespace string. Unlike UpdateRequest, an empty expectation does NOT mean 'expected unassigned' — releasing an issue nobody holds is not a release. The underlying storage seam refuses the empty expectation, so the validator rejects it early with this message.","triggerScenarios":"Passing issueops.ReleaseRequest{Actor: \"a\", IssueID: \"bd-1\", ExpectedAssignee: ptr(\"\")} or ptr(\"   \") — a non-nil pointer to a blank string — to any Releaser implementation.","commonSituations":"A caller reused code written against UpdateRequest.ExpectedAssignee semantics where \"\" meant unassigned; a variable holding the expected holder was empty but the code took its address anyway instead of leaving the pointer nil.","solutions":["Either set ExpectedAssignee to the non-empty name of the expected holder, or set it to nil to express 'no expectation'","Fix call sites that take &emptyString unconditionally; build the pointer only when a holder was actually specified","Do not port UpdateRequest's empty-means-unassigned convention to release requests"],"exampleFix":"// before\nvar expected string // ends up \"\"\nreq := issueops.ReleaseRequest{Actor: \"alice\", IssueID: \"bd-1\", ExpectedAssignee: &expected}\n// after\nvar expected *string\nif holder != \"\" {\n    expected = &holder\n}\nreq := issueops.ReleaseRequest{Actor: \"alice\", IssueID: \"bd-1\", ExpectedAssignee: expected}","handlingStrategy":"validation","validationCode":"if req.ExpectedAssignee != nil && strings.TrimSpace(*req.ExpectedAssignee) == \"\" {\n    return fmt.Errorf(\"expectedAssignee must name a holder; use nil for no expectation\")\n}","typeGuard":"func hasValidExpectedAssignee(req issueops.ReleaseRequest) bool {\n    return req.ExpectedAssignee == nil || strings.TrimSpace(*req.ExpectedAssignee) != \"\"\n}","tryCatchPattern":"if err := releaser.Release(ctx, req); err != nil {\n    if errors.Is(err, issueops.ErrValidation) {\n        return fmt.Errorf(\"invalid expected-assignee: %w\", err)\n    }\n    return err\n}","preventionTips":["Only set the ExpectedAssignee pointer when a holder was actually specified; leave it nil otherwise","Do not reuse UpdateRequest's empty-means-unassigned convention for release requests","Add a unit test asserting ptr(\"\") is refused"],"tags":["go","validation","release","expected-assignee"],"backgroundTag":"invalid-argument-value","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}