{"record":{"id":"aaa65d9aa4b32ecb","repo":"gastownhall/beads","slug":"repo-q-contains-invalid-character-q","errorCode":null,"errorMessage":"repo %q contains invalid character %q","messagePattern":"repo %q contains invalid character %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/gate.go","lineNumber":897,"sourceCode":"\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}\n\n// repoMetadataForGate computes the metadata to store on a new ad-hoc gate,\n// inheriting a validated GitHub repo selector from the blocked issue.\n//\n// This is restricted to gh:* gate types (SF4): \"repo\" is legal, unrelated\n// metadata on any issue (the metadata contract allows arbitrary JSON), so\n// running GitHub-repo validation for human/timer gates would fail ordinary","sourceCodeStart":879,"sourceCodeEnd":915,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/gate.go#L879-L915","documentation":"GitHub repository/owner names are limited to alphanumerics, hyphens, underscores, and dots. githubRepoFromIssue enforces this per component and reports the exact offending character and the full repo string, preventing invalid values from reaching `gh run list --repo`.","triggerScenarios":"metadata.repo contains characters outside [A-Za-z0-9._-], e.g. spaces, \"owner~repo\", a URL-encoded segment like \"%20\", or non-ASCII characters; hit during any gate check that resolves the repo.","commonSituations":"Pasting \"owner / repo\" with spaces; including URL escapes or query strings; Unicode lookalikes from copy-paste; shell interpolation leaking separators like \":\" or \"@\".","solutions":["Remove/replace invalid characters so each component matches [A-Za-z0-9._-], e.g. \"gastownhall/beads\".","Strip URL encoding or query fragments before storing: use the decoded path without scheme/parameters.","Quote values properly in shell scripts to avoid stray characters being captured into metadata.","Re-run `bd` gate checks after fixing to confirm the repo resolves."],"exampleFix":"// before\n{\"repo\": \"gastownhall / beads\"}\n// after\n{\"repo\": \"gastownhall/beads\"}","handlingStrategy":"validation","validationCode":"func validRepoChars(repo string) bool {\n    for _, part := range strings.Split(repo, \"/\") {\n        for _, c := range part {\n            ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'\n            if !ok { return false }\n        }\n    }\n    return true\n}","typeGuard":"var repoComponentRE = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)\nfunc charsAllowed(s string) bool {\n    for _, p := range strings.Split(s, \"/\") {\n        if !repoComponentRE.MatchString(p) { return false }\n    }\n    return true\n}","tryCatchPattern":null,"preventionTips":["Validate repo strings with a regex at the point of input, not at gate-check time.","Reject pasted URLs early; extract just the owner/repo path.","Use url.PathUnescape only if input may be URL-encoded, then re-validate characters."],"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"}