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.

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 argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — 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.

Appendix: 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)