gastownhall/beads · error

release requires an issue id

Error message

release requires an issue id

What it means

ValidateReleaseRequest refuses a ReleaseRequest whose IssueID is empty (or whitespace-only). A release names the issue whose assignee is being released; with no issue id the request is meaningless, so it is rejected up front before any database work. All Releaser implementations share this rule.

Source

Thrown at internal/workapi/release.go:29

// means before anything is read.
//
// Every implementation runs it, so `bd unclaim` has one definition of a
// malformed request rather than one per backend, and a refused request costs no
// database work anywhere.
//
// What is NOT here is the release. Classifying the refusals needs the row, and
// the row and the release must see one snapshot
// (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)
		}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set ReleaseRequest.IssueID to the id of the issue to release (e.g. "bd-42")
  2. Verify the issue id was actually parsed/populated from user input before building the request
  3. Handle errors.Is(err, issueops.ErrValidation) with an 'issue id required' message

Example fix

// before
req := issueops.ReleaseRequest{Actor: "alice"}
// after
req := issueops.ReleaseRequest{Actor: "alice", IssueID: issueID}
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(req.IssueID) == "" {
    return fmt.Errorf("cannot release: issue id is required")
}

Type guard

func hasIssueID(req issueops.ReleaseRequest) bool {
    return strings.TrimSpace(req.IssueID) != ""
}

Try / catch

if err := releaser.Release(ctx, req); err != nil {
    if errors.Is(err, issueops.ErrValidation) {
        return fmt.Errorf("invalid release request: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling a Releaser implementation with issueops.ReleaseRequest{IssueID: ""} (or whitespace) even when Actor and ExpectedAssignee are correctly set.

Common situations: A CLI handler built the request before parsing the positional issue argument; a loop over issues passed an empty string from an unset variable; a caller constructed the struct with named fields and forgot IssueID.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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