{"record":{"id":"b6aa7af6becfdd12","repo":"gastownhall/beads","slug":"metadata-repo-must-not-be-null","errorCode":null,"errorMessage":"metadata.repo must not be null","messagePattern":"metadata\\.repo must not be null","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/gate.go","lineNumber":874,"sourceCode":"\tif issue == nil || len(issue.Metadata) == 0 || string(issue.Metadata) == \"null\" {\n\t\treturn \"\", nil\n\t}\n\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(issue.Metadata, &raw); err != nil {\n\t\treturn \"\", fmt.Errorf(\"metadata must be a JSON object: %w\", err)\n\t}\n\trepoRaw, hasRepo := raw[\"repo\"]\n\tif !hasRepo {\n\t\treturn \"\", nil\n\t}\n\n\tvar repoValue interface{}\n\tif err := json.Unmarshal(repoRaw, &repoValue); err != nil {\n\t\treturn \"\", fmt.Errorf(\"metadata.repo: %w\", err)\n\t}\n\tif repoValue == nil {\n\t\treturn \"\", fmt.Errorf(\"metadata.repo must not be null\")\n\t}\n\trepo, ok := repoValue.(string)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"metadata.repo must be a string, got %T\", repoValue)\n\t}\n\tif repo == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tparts := strings.Split(repo, \"/\")\n\tif len(parts) != 2 && len(parts) != 3 {\n\t\treturn \"\", fmt.Errorf(\"repo %q must use OWNER/REPO or HOST/OWNER/REPO\", repo)\n\t}\n\tfor _, part := range parts {\n\t\tif part == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"repo %q contains an empty path component\", repo)\n\t\t}\n\t\tfor _, char := range part {","sourceCodeStart":856,"sourceCodeEnd":892,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/gate.go#L856-L892","documentation":"After unmarshaling, githubRepoFromIssue checks the decoded `repo` value; if it decodes to JSON null (rather than being absent), the function rejects it with \"metadata.repo must not be null\". A null repo is indistinguishable from corruption, so it fails loudly instead of silently skipping gate checks.","triggerScenarios":"The issue metadata stores `\"repo\": null` explicitly. json.Unmarshal succeeds and yields repoValue == nil, hitting this branch during repoMetadataForGate or any GH run/PR gate check.","commonSituations":"Automation that clears fields by setting them to null instead of deleting the key; partial JSON merges that zeroed the value; hand-edited issue exports.","solutions":["Delete the `repo` key entirely rather than setting it to null — absence means \"no repo configured\" and returns empty string gracefully.","Set a valid value: `\"repo\": \"owner/repo\"` or `\"host/owner/repo\"` in the issue metadata.","Update whatever writer (script or tool) nulls the field so it removes the key instead."],"exampleFix":"// before\n{\"repo\": null}\n// after\n{} // or {\"repo\": \"gastownhall/beads\"}","handlingStrategy":"validation","validationCode":"var v interface{}\nif err := json.Unmarshal(md[\"repo\"], &v); err == nil && v == nil {\n    delete(md, \"repo\") // treat null as \"no repo\" instead of leaving null\n}","typeGuard":"func repoMetadataIsPresent(md map[string][]byte) bool {\n    raw, ok := md[\"repo\"]\n    if !ok { return false }\n    var v interface{}\n    if err := json.Unmarshal(raw, &v); err != nil { return false }\n    return v != nil\n}","tryCatchPattern":null,"preventionTips":["Never set metadata fields to null; delete the key instead.","Audit scripts that clear issue fields for null writes.","Add a pre-submit check rejecting `\"repo\": null` in metadata."],"tags":["json","metadata","validation"],"backgroundTag":"null-metadata-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}