l3montree-dev / l3montree-dev/devguard
Orphaned GitHub App installation blocks repository listing org-wide
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 161
- Forks
- 43
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 37
Description
Severity: High – no repos can be listed, no new assets can be connected
Component: github_client.go (ListRepositories)
Summary
When an org has multiple GitHub App installations and one is uninstalled on GitHub
without the uninstall webhook reaching DevGuard, the installation stays behind as an
orphaned record. It can't be deleted from the UI and the user gets no feedback
(see screenshot: two juliankepka entries, one dead). Because ListRepositories
fans out over all installations and fails fast on the first error, the dead
installation's 404 aborts the entire listing.
Steps to Reproduce
- Connect two GitHub App installations to one org.
- Uninstall one directly on GitHub so the webhook is not processed.
- Open Settings → Third-Party Integrations → dead entry remains, not deletable.
- Try to list/connect repositories for a new asset → fails.
Expected
- A dead installation must not break listing for the healthy ones (graceful degradation).
- User gets clear feedback that the installation is no longer valid.
- Org owner can delete the orphaned entry in the frontend.
Actual
- Listing fails completely; no repos, no new assets. No feedback. Entry not deletable.
Log
error while listing repositories … could not refresh installation id 146269091's
token: received non 2xx response status "404 Not Found" when fetching
https://api.github.com/app/installations/146269091/access_tokens
Root Cause
WaitAndCollect() is fail-fast: one goroutine returning err aborts the whole
collection. A single uninstalled app (404 on the access-token endpoint) kills the
listing for every installation.
func (githubOrgClient *githubBatchClient) ListRepositories(
ctx context.Context,
search string,
) ([]githubRepository, error) {
wg := utils.ErrGroup[[]githubRepository](10)
for _, client := range githubOrgClient.clients {
wg.Go(func() ([]githubRepository, error) {
result, err := fetchAllRepos(ctx, client)
if err != nil {
return nil, nil // current workaround: swallows ALL errors
}
if search != "" {
result = utils.Filter(result, func(el *github.Repository) bool {
return strings.Contains(*el.FullName, search)
})
}
return utils.Map(result, func(el *github.Repository) githubRepository {
return githubRepository{el, client.githubAppInstallationID}
}), nil
})
}
results, err := wg.WaitAndCollect()
if err != nil {
return nil, err
}
return utils.Flat(results), nil
}
Current Workaround (not a real fix)
return nil, nil at github_client.go:102 restores listing for healthy
installations but swallows all errors — including transient 5xx / rate limits —
gives no feedback, and never cleans up the orphaned record.
Proposed Direction
- Collect result and error per installation instead of fail-fast; return healthy results.
- Classify errors: permanent (404 = installation gone) vs. transient (5xx/403/network).
- On permanent error, mark installation as
invalid/orphanedso the API can surface it. - Add a delete endpoint (org-owner only, idempotent) for orphaned installations.
- Log raw errors internally; show the user a sanitized message (no internal IDs).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in github_client.go at ListRepositories and line 102, then trace how installation errors and installation records reach the API and frontend. Reproduce the two-installation case and verify that healthy repositories remain listable, permanent and transient failures receive different handling, and an org owner can remove an orphaned entry with a user-facing message.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend, frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100