gastownhall/beads · error

provenance: ref-kind git-sha requires a 40-character lowerca

Error message

provenance: ref-kind git-sha requires a 40-character lowercase hex ref

What it means

When a provenance event's RefKind is "git-sha", ValidateProvenanceEvent additionally requires the Ref to match gitSHARE, a 40-character lowercase hex regex (a full git commit SHA). Anything shorter, uppercase, or non-hex is rejected. This guarantees provenance records reference a canonical commit identity.

Source

Thrown at internal/storage/issueops/provenance.go:72

	if _, ok := knownProvKinds[ev.Kind]; !ok {
		return fmt.Errorf("provenance: unknown kind %q", ev.Kind)
	}
	if strings.TrimSpace(ev.Source) == "" {
		return fmt.Errorf("provenance: source is required")
	}
	if strings.EqualFold(strings.TrimSpace(ev.Source), ReservedProvSource) {
		return fmt.Errorf("provenance: source %q is reserved for ingest backfill and cannot be recorded directly", ReservedProvSource)
	}
	if ev.RefKind != nil {
		if _, ok := knownProvRefKinds[*ev.RefKind]; !ok {
			return fmt.Errorf("provenance: unknown ref-kind %q", *ev.RefKind)
		}
		if ev.Ref == nil || *ev.Ref == "" {
			return fmt.Errorf("provenance: ref-kind %q requires a ref", *ev.RefKind)
		}
		if *ev.RefKind == "git-sha" {
			if !gitSHARE.MatchString(*ev.Ref) {
				return fmt.Errorf("provenance: ref-kind git-sha requires a 40-character lowercase hex ref")
			}
		}
	}
	// A ref-less event is keyed by occurred_at for its stable id; without either,
	// two distinct events would collapse to the same content-addressed id. Guard
	// at the store boundary so every caller (CLI or library) is covered.
	if (ev.Ref == nil || *ev.Ref == "") && ev.OccurredAt == nil {
		return fmt.Errorf("provenance: event with no ref requires occurred_at (--at) for a stable id")
	}
	return nil
}

// ProvenanceEventID computes the deterministic, idempotent id for a provenance
// event from source:issue:kind:(ref or occurred_at). A producer firing twice
// with the same facts yields the same id, so the INSERT IGNORE in
// RecordProvenanceEventInTx is a harmless no-op the second time. The
// discriminator is the ref when present, otherwise the fixed-width occurred_at,
// which is why a ref-less event requires --at (so the id is caller-owned, never

View on GitHub (pinned to 71377f2769)

Solutions

  1. Use the full commit SHA: `git rev-parse HEAD` (no --short) and lowercase it before assigning to ev.Ref
  2. Validate the ref against ^[0-9a-f]{40}$ in the caller before building the event
  3. If you only have a branch/tag, use the appropriate RefKind instead of "git-sha"

Example fix

// before
ev := ProvenanceEvent{RefKind: strPtr("git-sha"), Ref: &shortSHA}
// after
fullSHA := exec.Command("git", "rev-parse", "HEAD").Out()
fullSHA = strings.ToLower(strings.TrimSpace(fullSHA))
ev := ProvenanceEvent{RefKind: strPtr("git-sha"), Ref: &fullSHA}
Defensive patterns

Strategy: validation

Validate before calling

var gitSHARE = regexp.MustCompile(`^[0-9a-f]{40}$`)
func isFullSHA(s string) bool { return gitSHARE.MatchString(s) }

Type guard

func asGitSHA(ref string) (*string, bool) {
  if len(ref) == 40 && regexp.MustCompile(`^[0-9a-f]{40}$`).MatchString(ref) {
    return &ref, true
  }
  return nil, false
}

Prevention

When it happens

Trigger: Calling RecordProvenanceEventInTx with RefKind "git-sha" and a Ref that is a short SHA (e.g. 7 chars from `git rev-parse --short`), an uppercase SHA, a branch name, or a tag instead of a full commit hash.

Common situations: Piping `git rev-parse --short HEAD` output into the event; copying SHAs from UI tools that truncate; normalizing SHAs with strings.ToUpper; passing a ref name where a SHA is expected.

Related errors


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