{"record":{"id":"3a408acfbc8b6eb7","repo":"gastownhall/beads","slug":"repo-q-contains-an-empty-path-component","errorCode":null,"errorMessage":"repo %q contains an empty path component","messagePattern":"repo %q contains an empty path component","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/gate.go","lineNumber":890,"sourceCode":"\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 {\n\t\t\tif (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||\n\t\t\t\t(char >= '0' && char <= '9') || char == '-' || char == '_' || char == '.' {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn \"\", fmt.Errorf(\"repo %q contains invalid character %q\", repo, char)\n\t\t}\n\t}\n\n\treturn repo, nil\n}\n\n// isGitHubGateType returns true for gate types whose condition is checked\n// against a GitHub repository (gh:run, gh:pr, and any future gh:* type).\nfunc isGitHubGateType(gateType string) bool {\n\treturn strings.HasPrefix(gateType, \"gh:\")\n}","sourceCodeStart":872,"sourceCodeEnd":908,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/gate.go#L872-L908","documentation":"Each slash-separated component of metadata.repo must be non-empty; an empty component means the repo path has a dangling or doubled slash (e.g. \"/owner/repo\", \"owner//repo\", \"owner/repo/\") and cannot name a valid GitHub repository.","triggerScenarios":"metadata.repo strings with leading, trailing, or doubled slashes split into an empty part, hitting the `if part == \"\"` branch in githubRepoFromIssue during gate checks.","commonSituations":"Trailing slash pasted from a URL; typo \"owner//repo\" after deleting a hostname; template substitution that left an empty segment (e.g. \"$host/$owner/repo\" with $host unset).","solutions":["Remove leading/trailing/duplicate slashes so the value is exactly \"owner/repo\" or \"host/owner/repo\".","Fix template variables that were empty at render time (e.g. ensure host is set or dropped).","Re-check what produced the value: URL join logic or string concatenation that injects empty segments.","Validate the metadata after editing and re-run the gate check."],"exampleFix":"// before\n{\"repo\": \"gastownhall//beads\"}\n// after\n{\"repo\": \"gastownhall/beads\"}","handlingStrategy":"validation","validationCode":"func repoHasEmptyComponent(repo string) bool {\n    for _, p := range strings.Split(repo, \"/\") {\n        if p == \"\" { return true }\n    }\n    return false\n} // reject or strings.TrimSpace/Trim(repo, \"/\") before storing","typeGuard":"func isCleanRepoPath(s string) bool {\n    parts := strings.Split(s, \"/\")\n    for _, p := range parts { if p == \"\" { return false } }\n    return true\n}","tryCatchPattern":null,"preventionTips":["Trim leading/trailing slashes from user input before storing.","Guard string concatenation/templates that build repo paths (check for empty segments).","Normalize \"$host/$owner/$repo\" style templates: drop empty variables entirely."],"tags":["validation","github","configuration"],"backgroundTag":"invalid-repo-format","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}