gastownhall/beads · error · issueops.ErrValidation

%w: compare-and-set requires an issue id

Error message

%w: compare-and-set requires an issue id

What it means

PlanCompareAndSetKey returns this wrapped issueops.ErrValidation error when in.IssueID is empty. The compare-and-set operation targets a specific issue's metadata key, so an issue id is mandatory for the plan. It is a pure input-validation error raised before any storage access.

Source

Thrown at internal/storage/metadata_cas.go:64

	// request removes the key.
	Value *json.RawMessage
}

// PlanCompareAndSetKey validates a compare-and-set request and canonicalizes
// its values. It is the whole of the role's request validation: every
// implementation calls it before touching a substrate, so a refused request
// costs no database work anywhere.
//
// It COPIES both raw values rather than aliasing the caller's, because the
// canonical form is written into the plan and the request belongs to the caller
// for the whole call.
func PlanCompareAndSetKey(in issueops.CompareAndSetKeyRequest) (CompareAndSetKeyPlan, error) {
	if in.Actor == "" {
		return CompareAndSetKeyPlan{}, fmt.Errorf(
			"%w: compare-and-set requires an actor to attribute the swap to", issueops.ErrValidation)
	}
	if in.IssueID == "" {
		return CompareAndSetKeyPlan{}, fmt.Errorf(
			"%w: compare-and-set requires an issue id", issueops.ErrValidation)
	}
	if err := ValidateMetadataKey(in.Key); err != nil {
		return CompareAndSetKeyPlan{}, fmt.Errorf("%w: %v", issueops.ErrValidation, err)
	}
	plan := CompareAndSetKeyPlan{Actor: in.Actor, IssueID: in.IssueID, Key: in.Key}
	var err error
	if plan.Expected, err = CanonicalMetadataPointer(in.Expected); err != nil {
		return CompareAndSetKeyPlan{}, fmt.Errorf("%w: expected value for metadata key %q: %v",
			issueops.ErrValidation, in.Key, err)
	}
	if plan.Value, err = CanonicalMetadataPointer(in.Value); err != nil {
		return CompareAndSetKeyPlan{}, fmt.Errorf("%w: new value for metadata key %q: %v",
			issueops.ErrValidation, in.Key, err)
	}
	return plan, nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set in.IssueID to the target issue's id before calling (e.g. "bd-42").
  2. Guard the caller: if issueID == "" return early with a clearer upstream message instead of reaching the planner.
  3. In shell scripts, quote and check: : "${ID:?issue id required}".
  4. Match errors.Is(err, issueops.ErrValidation) to render a user-friendly 'issue id required' error.

Example fix

// before
plan, err := PlanCompareAndSetKey(issueops.CompareAndSetKeyRequest{Actor: "alice", Key: "status"})
// after
plan, err := PlanCompareAndSetKey(issueops.CompareAndSetKeyRequest{Actor: "alice", IssueID: "bd-42", Key: "status"})
Defensive patterns

Strategy: validation

Validate before calling

if req.IssueID == "" {
    return errors.New("compare-and-set requires an issue id")
} // run before PlanCompareAndSetKey

Try / catch

plan, err := PlanCompareAndSetKey(req)
if err != nil {
    if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "requires an issue id") {
        return fmt.Errorf("no issue selected: resolve an issue id first")
    }
    return err
}

Prevention

When it happens

Trigger: Calling PlanCompareAndSetKey with CompareAndSetKeyRequest.IssueID == "" — e.g. the caller resolved no issue from a search, or a variable holding the issue id was never assigned.

Common situations: A lookup step returned zero results and the empty string was passed through; scripts using an unset shell variable ($ID expands to empty); refactors that renamed the field and left it unset.

Related errors


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