gastownhall/beads · error
expected assignee must name a holder; there is no release of
Error message
expected assignee must name a holder; there is no release of an unheld issue
What it means
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.
Source
Thrown at internal/workapi/release.go:37
// (issueops.Releaser.Release); that body is
// internal/storage/issueops.ReleaseIssueInTx, which all three legs reach.
// ValidateReleaseRequest applies the request rules every Releaser
// implementation shares.
func ValidateReleaseRequest(in issueops.ReleaseRequest) error {
if strings.TrimSpace(in.Actor) == "" {
return fmt.Errorf("%w: release requires an actor to attribute it to", issueops.ErrValidation)
}
if strings.TrimSpace(in.IssueID) == "" {
return fmt.Errorf("%w: release requires an issue id", issueops.ErrValidation)
}
if in.ExpectedAssignee != nil {
// A non-nil pointer to "" is NOT "expected unassigned" here, unlike
// UpdateRequest.ExpectedAssignee: releasing a row nobody holds is not a
// release, and the raw seam beneath this role refuses the empty
// expectation in as many words.
if strings.TrimSpace(*in.ExpectedAssignee) == "" {
return fmt.Errorf("%w: expected assignee must name a holder; there is no release of an unheld issue",
issueops.ErrValidation)
}
// The two are answers to the same question and they disagree, which is
// the rule UpdateRequest states for ForceAssigneeTransfer beside its
// own ExpectedAssignee.
if in.Force {
return fmt.Errorf("%w: force releases whoever holds the issue and expected-assignee releases only a named holder; a request cannot ask for both",
issueops.ErrValidation)
}
}
return nil
}
View on GitHub (pinned to 71377f2769)
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
Example fix
// before
var expected string // ends up ""
req := issueops.ReleaseRequest{Actor: "alice", IssueID: "bd-1", ExpectedAssignee: &expected}
// after
var expected *string
if holder != "" {
expected = &holder
}
req := issueops.ReleaseRequest{Actor: "alice", IssueID: "bd-1", ExpectedAssignee: expected} Defensive patterns
Strategy: validation
Validate before calling
if req.ExpectedAssignee != nil && strings.TrimSpace(*req.ExpectedAssignee) == "" {
return fmt.Errorf("expectedAssignee must name a holder; use nil for no expectation")
} Type guard
func hasValidExpectedAssignee(req issueops.ReleaseRequest) bool {
return req.ExpectedAssignee == nil || strings.TrimSpace(*req.ExpectedAssignee) != ""
} Try / catch
if err := releaser.Release(ctx, req); err != nil {
if errors.Is(err, issueops.ErrValidation) {
return fmt.Errorf("invalid expected-assignee: %w", err)
}
return err
} Prevention
- 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
When it happens
Trigger: Passing issueops.ReleaseRequest{Actor: "a", IssueID: "bd-1", ExpectedAssignee: ptr("")} or ptr(" ") — a non-nil pointer to a blank string — to any Releaser implementation.
Common situations: 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.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- release requires an actor to attribute it to
- release requires an issue id
- force releases whoever holds the issue and expected-assignee
- no store is open for this workspace
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/caa820fa5f5aa1e7.
Report an issue: GitHub.