gastownhall/beads · error

release %q: the releaser reported success without an issue

Error message

release %q: the releaser reported success without an issue

What it means

checkedReleaser wraps the Release role and enforces the same contract as checkedCommenter: success must include the released Issue. If an inner implementation returns nil error with a nil Issue, the wrapper converts it to this error, preventing the HTTP layer from answering success with an empty body (which would imply a 409/404 semantics mismatch).

Source

Thrown at internal/httpapi/roles.go:281

// checkedReleaser is the releaser the release handler is handed.
//
// It exists for checkedClaimer's reason exactly: handleRelease writes
// *result.Issue and reads its RowVersion, so a role that reported success
// without the row would panic on a live server.
type checkedReleaser struct{ inner issueops.Releaser }

// Release refuses a result that reports success without the row the response
// body is built from.
//
// The generic 500, for checkedClaimer's reason and one of its own: there is no
// wire code that fits and there must not be. A 409 would say the row refused
// the release when the role said it did not, and a 404 would say the issue does
// not exist when nothing here knows that. It is a broken implementation.
func (c checkedReleaser) Release(ctx context.Context, req issueops.ReleaseRequest) (issueops.ReleaseResult, error) {
	result, err := c.inner.Release(ctx, req)
	if err == nil && result.Issue == nil {
		return issueops.ReleaseResult{}, fmt.Errorf("release %q: the releaser reported success without an issue", req.IssueID)
	}
	return result, err
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix the inner releaser to return the updated Issue on nil error.
  2. Verify the storage layer actually persists the release and can read the issue back.
  3. Add a contract test that Release must return a non-nil Issue on success.
  4. Check test doubles/mocks used in the HTTP handler tests for empty success results.

Example fix

// before
func (s *store) Release(ctx context.Context, req issueops.ReleaseRequest) (issueops.ReleaseResult, error) {
    return issueops.ReleaseResult{}, s.releaseIssue(ctx, req) // no issue returned
}
// after
func (s *store) Release(ctx context.Context, req issueops.ReleaseRequest) (issueops.ReleaseResult, error) {
    if err := s.releaseIssue(ctx, req); err != nil {
        return issueops.ReleaseResult{}, err
    }
    issue, err := s.getIssue(ctx, req.IssueID)
    if err != nil {
        return issueops.ReleaseResult{}, err
    }
    return issueops.ReleaseResult{Issue: issue}, nil
}
Defensive patterns

Strategy: type-guard

Validate before calling

// contract test for any releaser implementation
result, err := impl.Release(ctx, req)
if err == nil && result.Issue == nil {
    t.Fatal("implementation violates contract: nil Issue with nil error")
}

Type guard

// narrow the result before use
func validRelease(r issueops.ReleaseResult, err error) bool {
    return err == nil && r.Issue != nil
}

Try / catch

result, err := httpAPI.Release(ctx, req)
if err != nil {
    // contract violations from checkedReleaser land here
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

Prevention

When it happens

Trigger: Any HTTP release request where the registered inner releaser returns (ReleaseResult{}, nil) — claiming the release succeeded while omitting the resulting issue.

Common situations: A custom release backend that updates the assignee but forgets to load/return the issue; a storage driver bug that skips the read-back step after the update; an in-memory test double that returns zero-valued results.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/907a7f921b13a413. Report an issue: GitHub.