{"record":{"id":"f6a57a21443d741d","repo":"glanceapp/glance","slug":"invalid-source","errorCode":null,"errorMessage":"invalid source","messagePattern":"invalid source","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/glance/widget-releases.go","lineNumber":149,"sourceCode":"\t}\n\n\tparts := strings.SplitN(repository, \":\", 2)\n\tif len(parts) == 1 {\n\t\tr.source = releaseSourceGithub\n\t} else if len(parts) == 2 {\n\t\tr.Repository = parts[1]\n\n\t\tswitch parts[0] {\n\t\tcase string(releaseSourceGithub):\n\t\t\tr.source = releaseSourceGithub\n\t\tcase string(releaseSourceGitlab):\n\t\t\tr.source = releaseSourceGitlab\n\t\tcase string(releaseSourceDockerHub):\n\t\t\tr.source = releaseSourceDockerHub\n\t\tcase string(releaseSourceCodeberg):\n\t\t\tr.source = releaseSourceCodeberg\n\t\tdefault:\n\t\t\treturn errors.New(\"invalid source\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc fetchLatestReleases(requests []*releaseRequest) (appReleaseList, error) {\n\tjob := newJob(fetchLatestReleaseTask, requests).withWorkers(20)\n\tresults, errs, err := workerPoolDo(job)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar failed int\n\n\treleases := make(appReleaseList, 0, len(requests))\n\n\tfor i := range results {","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/glanceapp/glance/blob/91324e8de762702e97b0ac5c8e36271d644d8642/internal/glance/widget-releases.go#L131-L167","documentation":"Thrown by the releases widget's unmarshaling when a repository string contains a colon and the prefix before it is not one of the recognized sources. Recognized prefixes are exactly github, gitlab, codeberg, and dockerhub (lowercase); the suffix after the colon becomes the repository and the prefix selects the API. An unprefixed 'owner/repo' silently defaults to github.","triggerScenarios":"A repository entry like 'gitea:owner/repo', 'gh:owner/repo', 'docker:owner/repo', or any capitalized prefix like 'Gitlab:owner/repo'. Any single-colon string whose prefix is not in the switch hits the default case.","commonSituations":"Assuming 'docker:' works as shorthand for dockerhub; using a self-hosted Gitea/Forgejo instance and guessing a prefix; capitalizing the source name; entries that legitimately contain a colon in the repo name (dockerhub images with registry ports, e.g. 'registry.local:5000/team/app').","solutions":["Use one of the exact lowercase prefixes: github:, gitlab:, dockerhub:, or codeberg:","For GitHub repos, drop the prefix entirely ('owner/repo' defaults to github)","For registries whose image name contains a colon (port numbers), note that the first colon is parsed as the source separator; use the dockerhub: prefix with the remainder intact if it matches that shape"],"exampleFix":"# before\n- type: releases\n  repositories:\n    - docker:library/nginx\n\n# after\n- type: releases\n  repositories:\n    - dockerhub:library/nginx","handlingStrategy":"type-guard","validationCode":"SOURCES = {'github', 'gitlab', 'codeberg', 'dockerhub'}\nfor r in w.get('repositories', []):\n    repo = r if isinstance(r, str) else r.get('repository', '')\n    if ':' in repo and repo.split(':', 1)[0].lower() not in SOURCES:\n        raise ValueError(f\"unknown source prefix in {repo!r}; valid: {sorted(SOURCES)}\")","typeGuard":"const SOURCES = new Set(['github', 'gitlab', 'codeberg', 'dockerhub']);\nfunction hasValidSource(repo: string): boolean {\n  const i = repo.indexOf(':');\n  if (i === -1) return true; // defaults to github\n  return SOURCES.has(repo.slice(0, i));\n}","tryCatchPattern":null,"preventionTips":["Memorize the four exact lowercase prefixes: github, gitlab, codeberg, dockerhub","GitHub repos need no prefix at all","Beware image names containing colons (registry ports): the first colon is the source separator"],"tags":["config","yaml","releases","validation"],"backgroundTag":null,"analyzedSha":"91324e8de762702e97b0ac5c8e36271d644d8642","analyzedAt":"2026-08-15T14:12:54.279Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}