Fix null repos handling for all-repos installations - #180
Conversation
Preserve null semantics in the repos column when a GitHub App is installed on all repos in an account. Previously payload.repositories was coerced from null to [] via ?? [], breaking repos IS NULL queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hsyntax
left a comment
There was a problem hiding this comment.
Review: 2 finding(s) across the changes.
|
This PR is not merge-ready yet because repo-scope transitions can remain overbroad and the new integration tests do not match GitHub's documented
Category Breakdown
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hsyntax
left a comment
There was a problem hiding this comment.
Review: 3 finding(s) across the changes.
|
|
||
| let repos: ReadonlyArray<{ full_name: string }> | null | ||
| if (eventType === "installation_repositories") { | ||
| const current = yield* store.getInstallationRepos(installationId) |
There was a problem hiding this comment.
🟡 WARNING (authorization)
installation-repo-state-can-stay-overbroad — installation_repositories updates currently treat both a missing installation row and an existing repos = null row as terminal states. That breaks legitimate state transitions: if the initial installation event was missed or arrives out of order, the first repo delta is dropped; and if an installation narrows from all repositories to selected repositories, repos can remain NULL, which this codebase treats as full access and can overgrant authorization after access has been revoked.
Suggested fixes
- Track deletion separately from absence so
installation_repositoriesevents can rebuild state for installations that are missing but not known-deleted. - When
repository_selectionisselectedand the current row is missing or hasrepos = null, replacereposwith an authoritative selected-repo list fetched from GitHub instead of returning early or preservingnull. - If an inline resync is not possible, fail closed by marking the installation as needing resync and stop treating
repos IS NULLas authoritative until the repo list is refreshed.
| } | ||
| } else { | ||
| repos = current | ||
| repos = payload.repositories ?? null |
There was a problem hiding this comment.
🟡 WARNING (state-management)
installation-events-can-clobber-repo-scope — Non-deleted installation actions now write repos = payload.repositories ?? null, but GitHub's installation payload does not always include repositories. For actions like suspend, unsuspend, or new_permissions_accepted, an omitted field will rewrite a selected-repo installation to NULL, incorrectly broadening it to 'all repositories'.
Suggested fixes
- Only derive
reposfrompayload.repositorieswhen the payload explicitly includes repository data, and preserve the stored value otherwise. - For non-
deletedinstallation actions, load the current installation first and fall back to its existing repo set unless the payload explicitly indicates an all-repos installation. - Limit full repo replacement behavior to actions whose payload shape is known to carry authoritative repository scope, such as
installation.created.
| @@ -399,6 +477,63 @@ describe("webhook receiver", () => { | |||
| { timeout: 60_000 }, | |||
There was a problem hiding this comment.
🟡 WARNING (quality)
installation-repositories-tests-use-invalid-payloads — The new installation_repositories integration tests omit repositories_added and repositories_removed, even though GitHub documents both arrays as required for this event. That weakens coverage because the tests validate handler behavior against a synthetic payload shape rather than real deliveries.
Suggested fixes
- Add
repositories_added: []andrepositories_removed: []to both newinstallation_repositoriestest payloads. - Extract a shared helper or fixture for
installation_repositoriespayloads so every test includes the documented required arrays andrepository_selectionconsistently.
Note
This finding applies to line 314 but is shown here because that line is not in the diff.
Summary
nullsemantics ininstallations.reposwhen a GitHub App is installed on all repos (previouslynullwas coerced to[]via?? [], breakingrepos IS NULLqueries)nullthroughinstallation_repositoriesincremental merge (all-repos installations stay null)UpsertInstallationRequestschema to acceptNullOr(Array(RepoEntry))Test plan
installation event with all repos → repos is null in DBpnpm test:integrationgreenImplements
specs/github-app/2-installations.md