gastownhall/beads · error

close: actor and issue ID must not be empty

Error message

close: actor and issue ID must not be empty

What it means

validateCloseRequest requires a non-empty Actor and IssueID on every CloseRequest and wraps this rejection in publicops.ErrValidation. Closing an issue must record who closed it and which issue was closed.

Solutions

  1. Populate request.Actor with the closing identity
  2. Populate request.IssueID with the target issue key
  3. Filter/validate the ID list before looping Close calls
  4. Check errors.Is(err, publicops.ErrValidation) to distinguish input bugs from storage failures

Example fix

// before
for _, id := range ids { uc.Close(ctx, publicops.CloseRequest{Actor: actor, IssueID: id}) }
// after
for _, id := range ids {
    if id == "" { continue }
    uc.Close(ctx, publicops.CloseRequest{Actor: actor, IssueID: id})
}
Defensive patterns

Strategy: validation

Validate before calling

func validClose(req publicops.CloseRequest) error {
    if req.Actor == "" { return fmt.Errorf("actor required") }
    if req.IssueID == "" { return fmt.Errorf("issue ID required") }
    return nil
}

Type guard

func closeRequestComplete(req publicops.CloseRequest) bool {
    return req.Actor != "" && req.IssueID != ""
}

Try / catch

if err := uc.Close(ctx, req); err != nil {
    if errors.Is(err, publicops.ErrValidation) {
        // fix Actor/IssueID before retrying
        return err
    }
    return err
}

Prevention

When it happens

Trigger: Calling Close with a publicops.CloseRequest where request.Actor == "" or request.IssueID == "".

Common situations: Scripted bulk-close loops where an ID list contains an empty string, a close automation with no configured actor identity, or parsing issue IDs from input that produced empty fields.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at internal/storage/uow/issue_operations.go:540

	return fmt.Errorf("%w: %w", publicops.ErrValidation, err)
}

func validateUpdateRequest(request publicops.UpdateRequest) error {
	if request.Actor == "" || request.IssueID == "" {
		return validationError(fmt.Errorf("update: actor and issue ID must not be empty"))
	}
	if err := storageissueops.ValidateUpdateRequest(request); err != nil {
		return validationError(err)
	}
	if err := validateMetadataPatch(request.Patch.Metadata); err != nil {
		return validationError(err)
	}
	return nil
}

func validateCloseRequest(request publicops.CloseRequest) error {
	if request.Actor == "" || request.IssueID == "" {
		return validationError(fmt.Errorf("close: actor and issue ID must not be empty"))
	}
	return nil
}

func validateMetadataPatch(metadata publicops.MetadataPatch) error {
	if metadata.Replace.Set && (metadata.Merge.Set || len(metadata.Set) > 0 || len(metadata.Unset) > 0) {
		return fmt.Errorf("metadata replacement cannot combine with incremental edits")
	}
	if metadata.Replace.Set && len(metadata.Replace.Value) > 0 && !json.Valid(metadata.Replace.Value) {
		return fmt.Errorf("metadata replacement is not valid JSON")
	}
	if metadata.Merge.Set {
		var object map[string]json.RawMessage
		if len(metadata.Merge.Value) == 0 || json.Unmarshal(metadata.Merge.Value, &object) != nil || object == nil {
			return fmt.Errorf("metadata merge must be a JSON object")
		}
	}
	keys := make([]string, 0, len(metadata.Set))

View on GitHub (pinned to 71377f2769)