Skip to content

[Bug]: installation_repositories webhooks are never handled — repos added to an existing installation are silently dropped (and the installation "added" branch is dead code) #257

Description

@galuis116

Summary

processGitHubWebhook handles the installation event but has no handler for the installation_repositories event, which is the event GitHub sends when a maintainer adds or removes repositories from an already-installed app. As a result, repos added to an existing installation are never upserted with their installation linkage.

The webhook entrypoint enqueues every delivery without an event allowlist:

// src/github/webhook.ts:10-62 — no event filtering; every x-github-event is queued
const eventName = c.req.header("x-github-event") ?? null;
...
// enqueues { deliveryId, eventName, payload, installationId } for processGitHubWebhook

So installation_repositories deliveries do reach processGitHubWebhook, where they match none of the branches and fall through. The only installation-related branches are:

// src/queue/processors.ts:562 — installation deleted
if (eventName === "installation" && payload.action === "deleted" && payload.installation?.id) { ... }

// src/queue/processors.ts:577 — installation created/added
if (eventName === "installation" && (payload.action === "created" || payload.action === "added")) {
  const installedRepos = payload.repositories?.map((repo) => repo.full_name)...;
  ...
}

// src/queue/processors.ts:593 — generic repo upsert
const installationId = getInstallationId(payload);
if (payload.repositories) {
  for (const repo of payload.repositories) await upsertRepositoryFromGitHub(env, repo, installationId ?? undefined);
}
if (payload.repository) await upsertRepositoryFromGitHub(env, payload.repository, installationId ?? undefined);

Two defects:

  1. Dead code: the || payload.action === "added" disjunct at processors.ts:577 can never fire. GitHub's installation event only emits actions created, deleted, suspend, unsuspend, and new_permissions_accepted — never added. "Added" repos arrive on the separate installation_repositories event.

  2. Missing handler (the real bug): the installation_repositories event (actions added / removed) carries its repos in repositories_added[] / repositories_removed[], not in repositories[]. So:

    • The eventName === "installation" branches don't match (the event name is installation_repositories).
    • The generic if (payload.repositories) at processors.ts:593 is false (the data is in repositories_added).
    • The added repos are never passed to upsertRepositoryFromGitHub, so they get no row and no installationId association.

The payload type confirms the gap — GitHubWebhookPayload (src/types.ts) declares repositories? but has no repositories_added / repositories_removed fields, so even a hand-written handler couldn't read them without a type change.

Failure mode (concrete example)

  1. An org installs the app on repo acme/a → GitHub sends installation/created with repositories: [acme/a]acme/a is upserted with installationId (processors.ts:577-596). Correct.
  2. Later the maintainer adds acme/b via GitHub's "Repository access" UI → GitHub sends installation_repositories with action: "added", repositories_added: [acme/b], and the same installation.id.
  3. Current behavior: no branch matches; payload.repositories is undefined → acme/b is silently dropped. It has no repository row and is not associated with the installation.
  4. Correct behavior: acme/b should be upserted and linked to the installation immediately, exactly as the initial created set is.

The repo only becomes visible later, by accident, if some unrelated pull_request / issue / push webhook for acme/b happens to fire and lazily upsert it — and even then it may miss the installation linkage depending on that payload.

Downstream impact

  • Installation health undercounts. refreshInstallationHealth enumerates repos by repo.installationId === installation.id (in src/github/backfill.ts); a repo never linked to the installation is excluded from health, backfill scheduling, and registration-readiness until something else upserts it.
  • No usage signal. No github_installation_* product-usage event is recorded for repos added post-install, so githubInstalledRepos / installation-growth analytics (src/db/repositories.ts:3258) under-report adoption.
  • Removals are unhandled too. When a repo is removed from an installation (action: "removed", repositories_removed[]), nothing clears its installationId / isInstalled, so the engine keeps treating a no-longer-accessible repo as installed (the inverse of markInstallationDeleted, src/db/repositories.ts:178).

Steps to reproduce

  1. Install the app on one repo (full installation/created).
  2. Add a second repo to the same installation through GitHub's repository-access settings.
  3. Observe that the second repo never appears with an installationId association (no repository row from the webhook), and refreshInstallationHealth does not count it — until an unrelated activity webhook for that repo arrives.

Expected behavior

processGitHubWebhook handles installation_repositories: on added, upsert every repo in repositories_added with the installation id (and record an installation usage event); on removed, clear the installation linkage for every repo in repositories_removed — mirroring how the initial installation/created set and markInstallationDeleted are handled.

Actual behavior

installation_repositories events are received and enqueued but match no handler, so added repos are dropped and removed repos are never unlinked. The installation/"added" disjunct that looks like it was meant to cover this is dead code against the wrong event name and the wrong payload field.

Suggested fix

  • Add the missing payload fields to GitHubWebhookPayload (src/types.ts): repositories_added?: GitHubRepositoryPayload[] and repositories_removed?: GitHubRepositoryPayload[].
  • Add an installation_repositories branch in processGitHubWebhook (src/queue/processors.ts):
    • action === "added": for (const repo of payload.repositories_added ?? []) await upsertRepositoryFromGitHub(env, repo, installationId ?? undefined), and record a github_installation_* product-usage event (mirroring processors.ts:581).
    • action === "removed": clear installationId / isInstalled for each repositories_removed repo (a small markInstallationRepositoryRemoved helper in src/db/repositories.ts, paralleling markInstallationDeleted at :178).
  • Remove the dead || payload.action === "added" disjunct at processors.ts:577.
  • Tests: a queue/processor test asserting that an installation_repositories/added delivery upserts repositories_added with the installation id and records the usage event, and that removed clears the linkage; plus a payload-type fixture covering the new fields.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.slopAI slop and/or attempts to game additional points via manipulation or alt profiles.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions