{"record":{"id":"46619dc77bd44905","repo":"gastownhall/beads","slug":"invalid-repo-metadata-w","errorCode":null,"errorMessage":"invalid repo metadata: %w","messagePattern":"invalid repo metadata: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/gate_discover.go","lineNumber":272,"sourceCode":"// repo's runs of a same-named workflow, which would otherwise persist the\n// wrong await_id permanently (the persisted ID pins the gate).\n//\n// queryRuns receives a workflowHint - the gate's AwaitID workflow name hint,\n// non-empty only for a foreign (cross-repo) query - so it can narrow the\n// `gh run list` call with --workflow. Without that narrowing, a busy foreign\n// repo's unfiltered recent-run list (capped by --limit) might never surface\n// the specific workflow a gate is waiting on. The current repo's query is\n// never narrowed this way (workflowHint is always \"\" for it), matching\n// pre-existing `bd gate discover` behavior for local gates.\nfunc matchGatesToRuns(gates []*types.Issue, maxAge time.Duration, queryRuns func(repo, workflowHint string) ([]GHWorkflowRun, error)) []gateDiscoveryMatch {\n\trunsByKey := make(map[string][]GHWorkflowRun)\n\tqueryErrByKey := make(map[string]error)\n\tresults := make([]gateDiscoveryMatch, 0, len(gates))\n\n\tfor _, gate := range gates {\n\t\trepo, repoErr := githubRepoFromIssue(gate)\n\t\tif repoErr != nil {\n\t\t\tresults = append(results, gateDiscoveryMatch{gate: gate, err: fmt.Errorf(\"invalid repo metadata: %w\", repoErr)})\n\t\t\tcontinue\n\t\t}\n\n\t\tforeign := repo != \"\"\n\t\thint := getWorkflowNameHint(gate)\n\n\t\t// Cross-repo discovery requires a workflow hint. With local-commit/\n\t\t// local-branch heuristics neutralized for a foreign repo (see\n\t\t// matchGateToRun), a hintless gate could only ever score on time\n\t\t// proximity alone and risk pinning the wrong run in another\n\t\t// repository permanently. Skip the query entirely rather than spend\n\t\t// a GitHub API call on a gate that can never match.\n\t\tif foreign && hint == \"\" {\n\t\t\tresults = append(results, gateDiscoveryMatch{gate: gate})\n\t\t\tcontinue\n\t\t}\n\n\t\tqueryHint := \"\"","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/gate_discover.go#L254-L290","documentation":"`bd gate discover` validates each gate's metadata.repo via githubRepoFromIssue before using it as a GitHub repo selector. When the metadata contains a malformed repo value (explicit \"repo\":null, a non-string repo value, or a repo string not matching [HOST/]OWNER/REPO), the function returns an error and matchGatesToRuns wraps it as \"invalid repo metadata: %w\" instead of silently falling back to the current repository, since a silent fallback could point a cross-repo check at the wrong repo.","triggerScenarios":"Running `bd gate discover` when a gate issue's metadata.repo is invalid: the key exists but holds JSON null, a non-string value (number/bool/object), or a string that fails [HOST/]OWNER/REPO validation (e.g. \"owneronly\", \"a/b/c/d\", empty string).","commonSituations":"Hand-edited or script-written issue metadata with a typo'd repo field; an old gate created before validation was added; metadata written by another tool using a different repo format (URL instead of OWNER/REPO); JSON null stored explicitly instead of omitting the key.","solutions":["Run `bd show <gate-id>` and inspect the gate's metadata.repo value","Fix the metadata with `bd update <gate-id> --metadata '{\"repo\":\"OWNER/REPO\"}'` (valid OWNER/REPO or HOST/OWNER/REPO string)","If the gate should target the current repo, remove the repo key from metadata entirely rather than setting it to null","Re-run `bd gate discover`; the remaining gates will be processed"],"exampleFix":"// before (invalid metadata on gate)\n{\"repo\": null}\n// after\n{\"repo\": \"gastownhall/beads\"}","handlingStrategy":"validation","validationCode":"func validRepoMetadata(md json.RawMessage) bool {\n  if len(md) == 0 || string(md) == \"null\" { return true } // current repo\n  var m map[string]json.RawMessage\n  if json.Unmarshal(md, &m) != nil { return false }\n  raw, ok := m[\"repo\"]\n  if !ok { return true }\n  var s string\n  if json.Unmarshal(raw, &s) != nil { return false } // null or non-string\n  parts := strings.Split(s, \"/\")\n  return len(parts) == 2 || len(parts) == 3 // OWNER/REPO or HOST/OWNER/REPO\n}","typeGuard":"func gateRepoIsWellFormed(g *types.Issue) bool {\n  _, err := githubRepoFromIssue(g)\n  return err == nil\n}","tryCatchPattern":null,"preventionTips":["Never set metadata.repo to JSON null; omit the key to mean 'current repo'","Only write repo values as OWNER/REPO or HOST/OWNER/REPO strings, not URLs","Create gates with `bd gate create` so repo metadata is validated at write time","Audit existing gates with a script that calls bd list --json and re-validates metadata.repo before running discovery"],"tags":["validation","metadata","github"],"backgroundTag":"invalid-repo-metadata","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}