Skip to content

fix(integrations): let users pick which GitHub installation to link - #75667

Merged
Twixes merged 15 commits into
masterfrom
posthog-code/github-link-existing-installation-picker
Jul 31, 2026
Merged

fix(integrations): let users pick which GitHub installation to link#75667
Twixes merged 15 commits into
masterfrom
posthog-code/github-link-existing-installation-picker

Conversation

@joshsny

@joshsny joshsny commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

A GitHub App installs once per organization, so a second project can't reinstall — it reuses an existing installation via the Link existing installation button. That button posted to github/link_existing with an empty payload, which lands in the backend's auto-resolve branch. Auto-resolve only works when the org has exactly one installation: with two or more it can't decide and raises "Your organization has multiple GitHub installations; specify which one to link via installation_id".

The backend already supports disambiguating via an installation_id parameter, but the UI had no way to pick one — so an org with more than one installation had no path to link at all.

Changes

  • Backend: new read action GET integrations/github/available_installations (backed by list_org_github_installations) that returns the org's distinct GitHub installations — one entry per installation_id, excluding the current project's own, with account name/type for display.
  • Frontend: the GitHub integration setup now fetches available installations when the project has none linked. With a single installation the one-click behavior is unchanged; with more than one it renders a Link <account> button per installation that passes the chosen installation_id to the existing github/link_existing endpoint.

How did you test this code?

Added backend tests in posthog/api/test/test_integration.py:

  • test_link_existing_with_installation_id_disambiguates_multiple — with multiple org installations, passing an explicit installation_id links that specific one instead of raising the ambiguous-installation error (the fix path, previously untested).
  • test_list_org_github_installations_dedupes_and_excludes_target_team — the picker source lists one entry per distinct installation, excludes the target team's own install, and carries account metadata.

Ran ruff check and ruff format --check on the changed Python files (clean). I was not able to run the JS/TS typecheck or the Python test suite in this environment (no dev dependencies installed).

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Investigated from a support question about the "multiple GitHub installations" error, traced it to the empty-payload auto-resolve path, then implemented the picker. Authored with the PostHog Slack app (Claude Code). Followed the repo's DRF and generated-API-type conventions; the corresponding skills (/improving-drf-endpoints, /adopting-generated-api-types) are repo-local and weren't loadable in this environment, so their documented rules were applied by hand.

Follow-up: frontend/src/lib/api.ts defines a temporary GitHubAvailableInstallation interface. Run hogli build:openapi to generate GitHubAvailableInstallationApi from the new serializer and replace the hand-written type (codegen wasn't runnable here).


Created with PostHog from a Slack thread

@joshsny joshsny self-assigned this Jul 30, 2026
@trunk-io

trunk-io Bot commented Jul 30, 2026

Copy link
Copy Markdown

😎 This pull request was merged.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @joshsny! 👋

It looks like your git author email on this PR isn't your @posthog.com address (joshua@timenavi.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 65.43 MiB · no change

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.25 MiB · 22 files no change ███░░░░░░░ 27.7% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.13 MiB · 3,023 files no change ████████░░ 83.7% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
285.6 KiB ../node_modules/.pnpm/posthog-js@1.409.4/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
231.4 KiB ../node_modules/.pnpm/posthog-js@1.409.4/node_modules/posthog-js/dist/module.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.2 KiB src/lib/api.ts
94.7 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Toolbar bundle — eager 2.19 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.19 MiB · 17 files no change ████░░░░░░ 38.3% of 5.72 MiB
Deferred (lazy) 2.08 MiB · 33 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
717.7 KiB dist/toolbar/toolbar-app-RHGTKKCY.css
551.2 KiB dist/toolbar/chunk-chunk-LMAMJSO4.js
484.6 KiB dist/toolbar/chunk-chunk-I33HJPDC.js
133.6 KiB dist/toolbar/chunk-chunk-KNWVTMEH.js
131.8 KiB dist/toolbar/chunk-chunk-T5KY5WYR.js
71.0 KiB dist/toolbar/toolbar-app-SA5LAQYV.js
69.0 KiB dist/toolbar/chunk-chunk-27JL52RE.js
35.6 KiB dist/toolbar/chunk-chunk-AOSTOGBQ.js
20.9 KiB dist/toolbar/chunk-chunk-PPPVIVMR.js
12.2 KiB dist/toolbar/chunk-chunk-PIK3PADE.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

Dist folder size — 🔺 +15.2 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1373.79 MiB · 🔺 +15.2 KiB (+0.0%)

ℹ️ MCP UI apps size — 32 app(s), 17069.0 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 599.6 KB 187.7 KB
action 457.8 KB 187.7 KB
action-list 564.4 KB 187.7 KB
cohort 456.8 KB 187.7 KB
cohort-list 563.4 KB 187.7 KB
email-template 456.6 KB 187.7 KB
error-details 472.4 KB 187.7 KB
error-issue 457.5 KB 187.7 KB
error-issue-list 564.3 KB 187.7 KB
experiment 561.5 KB 187.7 KB
experiment-list 565.2 KB 187.7 KB
experiment-results 563.2 KB 187.7 KB
feature-flag 567.2 KB 187.7 KB
feature-flag-list 570.9 KB 187.7 KB
feature-flag-testing 461.0 KB 187.7 KB
insight-actors 562.2 KB 187.7 KB
invite-email-preview 456.0 KB 187.7 KB
llm-costs 559.5 KB 187.7 KB
session-recording 458.6 KB 187.7 KB
session-summary 463.9 KB 187.7 KB
survey 458.4 KB 187.7 KB
survey-global-stats 562.2 KB 187.7 KB
survey-list 565.1 KB 187.7 KB
survey-stats 562.2 KB 187.7 KB
trace-span 457.2 KB 187.7 KB
trace-span-list 564.3 KB 187.7 KB
workflow 457.1 KB 187.7 KB
workflow-list 563.7 KB 187.7 KB
loops-review 461.2 KB 187.7 KB
query-results 746.8 KB 187.7 KB
render-ui 827.4 KB 187.7 KB
visual-review-snapshots 461.6 KB 187.7 KB
⚠️ Playwright — 1 flaky

🎭 Playwright report · View test results →

⚠️ 1 flaky test:

  • Can delete a person (chromium)

These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!

⚠️ Backend coverage — 95.0% of changed backend lines covered — 4 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 95.0% (90 / 94)

File Patch Uncovered changed lines
posthog/api/integration.py 83.3% 1620, 1625
posthog/api/github_callback/team_services.py 90.5% 435, 547

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 30638269702 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.4% 8,791 / 22,299
demo ███████████░░░░░░░░░ 56.3% 1,497 / 2,661
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
data_tools ██████████████░░░░░░ 70.0% 63 / 90
tasks ██████████████░░░░░░ 70.1% 32,888 / 46,903
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 81.3% 24,974 / 30,718
cdp ████████████████░░░░ 82.1% 3,285 / 3,999
data_modeling █████████████████░░░ 85.7% 7,716 / 9,008
notebooks █████████████████░░░ 86.0% 7,794 / 9,060
wizard █████████████████░░░ 86.4% 1,060 / 1,227
actions █████████████████░░░ 86.6% 717 / 828
cohorts ██████████████████░░ 87.8% 6,181 / 7,040
product_tours ██████████████████░░ 87.9% 1,303 / 1,482
exports ██████████████████░░ 88.2% 7,046 / 7,986
data_warehouse ██████████████████░░ 88.4% 12,192 / 13,798
business_knowledge ██████████████████░░ 89.0% 4,391 / 4,936
engineering_analytics ██████████████████░░ 89.3% 6,529 / 7,309
conversations ██████████████████░░ 89.3% 17,600 / 19,701
dashboards ██████████████████░░ 89.4% 5,983 / 6,693
visual_review ██████████████████░░ 89.5% 5,870 / 6,558
alerts ██████████████████░░ 90.3% 4,482 / 4,966
links ██████████████████░░ 90.6% 183 / 202
streamlit_apps ██████████████████░░ 90.7% 2,630 / 2,901
mcp_analytics ██████████████████░░ 90.9% 3,191 / 3,511
error_tracking ██████████████████░░ 91.0% 10,928 / 12,010
slack_app ██████████████████░░ 91.1% 9,664 / 10,610
marketing_analytics ██████████████████░░ 91.2% 12,092 / 13,265
stamphog ██████████████████░░ 91.3% 4,505 / 4,936
mcp_store ██████████████████░░ 91.9% 4,273 / 4,650
product_analytics ███████████████████░ 92.5% 5,849 / 6,321
managed_migrations ███████████████████░ 92.6% 1,556 / 1,681
early_access_features ███████████████████░ 92.6% 1,287 / 1,390
notifications ███████████████████░ 92.6% 1,017 / 1,098
ai_observability ███████████████████░ 92.8% 15,618 / 16,821
surveys ███████████████████░ 93.1% 5,771 / 6,197
posthog_ai ███████████████████░ 93.2% 1,326 / 1,422
web_analytics ███████████████████░ 93.3% 14,911 / 15,976
approvals ███████████████████░ 93.3% 3,437 / 3,682
reminders ███████████████████░ 93.4% 468 / 501
legal_documents ███████████████████░ 93.8% 1,628 / 1,736
workflows ███████████████████░ 94.1% 7,241 / 7,698
endpoints ███████████████████░ 94.2% 8,655 / 9,192
tracing ███████████████████░ 94.5% 2,671 / 2,827
review_hog ███████████████████░ 94.6% 8,101 / 8,563
skills ███████████████████░ 94.6% 3,158 / 3,337
messaging ███████████████████░ 94.7% 2,885 / 3,048
logs ███████████████████░ 95.4% 10,466 / 10,966
experiments ███████████████████░ 95.5% 26,047 / 27,287
growth ███████████████████░ 96.1% 3,245 / 3,376
annotations ███████████████████░ 96.2% 732 / 761
revenue_analytics ███████████████████░ 96.3% 1,887 / 1,960
feature_flags ███████████████████░ 96.4% 17,488 / 18,144
replay_vision ███████████████████░ 96.4% 16,081 / 16,679
user_interviews ███████████████████░ 96.5% 2,638 / 2,734
access_control ███████████████████░ 96.9% 870 / 898
customer_analytics ███████████████████░ 97.1% 10,444 / 10,758
warehouse_sources ███████████████████░ 97.3% 359,732 / 369,739
data_catalog ████████████████████ 97.8% 2,589 / 2,648
analytics_platform ████████████████████ 98.0% 2,153 / 2,197
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

@joshsny
joshsny force-pushed the posthog-code/github-link-existing-installation-picker branch from eba59c5 to ce62803 Compare July 30, 2026 17:59
@joshsny
joshsny requested a review from a team July 30, 2026 18:14
@joshsny
joshsny marked this pull request as ready for review July 30, 2026 18:14
Copilot AI review requested due to automatic review settings July 30, 2026 18:15
Comment thread posthog/api/github_callback/team_services.py Outdated
joshsny added 2 commits July 30, 2026 19:17
A GitHub App installs once per org, so a second project reuses an existing
installation via "Link existing installation". That button sent no
installation_id, hitting the backend auto-resolve path, which raises an
ambiguous-installation error when the org has more than one installation —
leaving no way to link.

Add a github/available_installations endpoint that lists the org's distinct
installations, and surface a per-installation picker in the UI that passes the
chosen installation_id to the existing github/link_existing endpoint. When the
org has a single installation the one-click behavior is unchanged.

Generated-By: PostHog Code
Task-Id: ae6ad9b6-969c-4e53-9d09-4d2086ea6bf5
@veria-ai

veria-ai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request updates the GitHub integration linking flow so users can choose which existing GitHub installation to link to a project.

One issue has been addressed, but a cross-project authorization gap remains. A team-scoped API or OAuth token with integration write access to one project can enumerate and link a GitHub installation belonging to another project accessible through the same user account, bypassing the token’s intended scope.

Open issues (1)

Fixed/addressed: 1 · PR risk: 7/10

joshsny added 2 commits July 30, 2026 19:17
Replace the hand-written GitHubAvailableInstallation interface in lib/api with
the generated GitHubAvailableInstallationApi, and regenerate the kea-typegen
blocks for integrationsLogic.

Generated-By: PostHog Code
Task-Id: ae6ad9b6-969c-4e53-9d09-4d2086ea6bf5
@joshsny
joshsny force-pushed the posthog-code/github-link-existing-installation-picker branch from 609a561 to 0d70231 Compare July 30, 2026 18:18
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 6723505 · box box-50e2908fbcc6 · ready in 962s (push → usable) · build log · rebuilds on every push, torn down on close

Comment thread posthog/api/integration.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
### Issue 1
posthog/api/integration.py:1573
**Align the generated response shape**

The `many=True` response annotation generates a paginated API contract, while this action directly returns a bare JSON array without invoking DRF pagination. Generated consumers therefore expect a `results` field that the endpoint does not provide, preventing them from enumerating installations through the generated client.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: 609a561 | Re-trigger Greptile

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for linking an existing GitHub App installation when an organization has multiple installations, by introducing a backend “available installations” listing endpoint and updating the GitHub integration UI to let users choose which installation to link.

Changes:

  • Added a new GET integrations/github/available_installations action backed by list_org_github_installations.
  • Updated GitHub integration UI to show a single “Link existing installation” button (1 install) or a dropdown picker (multiple installs) that passes installation_id.
  • Added backend tests covering installation-id disambiguation and installation listing/deduping, plus committed regenerated API artifacts.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
services/mcp/src/api/generated.ts Regenerated MCP API types to include the new GitHub installations schema/operation.
products/integrations/mcp/tools.yaml Registers the new operation in the integrations MCP tools catalog (disabled).
products/integrations/frontend/generated/api.ts Adds generated client for integrations_github_available_installations_list.
products/integrations/frontend/generated/api.schemas.ts Adds generated schemas for GitHubAvailableInstallation* and list params.
posthog/api/test/test_integration.py Adds tests for linking by explicit installation_id and listing org installations.
posthog/api/integration.py Adds GitHubAvailableInstallationSerializer and the new github_available_installations action.
posthog/api/github_callback/team_services.py Implements list_org_github_installations dedupe + exclude-team logic.
frontend/src/scenes/integrations/components/Integrations.tsx Updates GitHub integration setup UI and adds the installation picker component.
frontend/src/scenes/integrations/components/Integrations.test.tsx Adds a Jest test for the multi-installation picker UI behavior.
frontend/src/lib/integrations/integrationsLogic.ts Fetches available installations when no GitHub integration exists; supports linking with installation_id.
frontend/src/lib/api.ts Adds a legacy api.integrations.githubAvailableInstallations helper for the new endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread posthog/api/integration.py Outdated
Comment thread frontend/src/lib/integrations/integrationsLogic.ts
@posthog

posthog Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

10 updated
Run: 5739f000-0af9-4540-ba57-75d45dfed730

Co-authored-by: joshsny <7135900+joshsny@users.noreply.github.com>
@github-actions
github-actions Bot requested a deployment to preview-pr-75667 July 30, 2026 18:43 In progress
Comment thread posthog/api/github_callback/team_services.py Outdated
… bare list

Greptile flagged that the many=True response annotation generates a paginated
API contract (a `results` field) while the action returned a bare JSON array,
so generated consumers couldn't enumerate installations. Wrap the list in an
{ installations: [...] } object — matching the github_repos response shape — so
the generated types and MCP tool describe the real payload. CI regenerates the
OpenAPI/MCP types from the serializer.

Generated-By: PostHog Code
Task-Id: ae6ad9b6-969c-4e53-9d09-4d2086ea6bf5
… access

Listing and linking an existing GitHub installation only checked org membership plus
target-team admin, never whether the requester could access the source project the
installation lives on. Because user.teams honours project-based permissioning, an org
member who admins the target project but is locked out of a private sibling could
enumerate that sibling's installation and link it (and its repositories) into their
project.

Restrict both list_org_github_installations and link_existing_team_github_integration
to source teams in the requester's accessible teams, so the picker never surfaces — and
the link endpoint never reuses — an installation the user couldn't otherwise reach.
Org admins/owners keep implicit access to all projects, so the legitimate cross-project
reuse flow is unchanged.

Generated-By: PostHog Code
Task-Id: 38be48ca-47ba-42b9-9b97-18e666c96270
@github-actions
github-actions Bot requested a deployment to preview-pr-75667 July 30, 2026 19:18 In progress
…tion-picker

Generated-By: PostHog Code
Task-Id: 38be48ca-47ba-42b9-9b97-18e666c96270
…aller's access

The source-project access check ran after resolving the source across the whole
org, so the picker and the link path disagreed:

- an installation shared by a private project and an accessible one resolved to
  the private project's lower-id row and was rejected, despite being offered
- auto-resolve counted installations the caller can't see, so the one-click
  button dead-ended on the ambiguous-installation error the picker exists to avoid

Filter candidates by the accessible team set during resolution instead.

Also defer the repository cache when listing installations, and call the
generated client instead of the legacy api.ts helper.
'Connect organization' was wrong three ways: nothing connects in PostHog when
you click it, you can install on a personal account rather than an org, and
where you land is GitHub's call. A GitHub App installs at most once per account,
so GitHub offers install where it's missing and configure where it isn't, which
is why clicking it with one installation just opens that installation's settings.

Name the destination instead, and always explain what's on the other side. The
supporting line previously only rendered when an existing installation could be
linked, so the common case had no explanation at all.
@github-actions
github-actions Bot requested a deployment to preview-pr-75667 July 31, 2026 10:32 In progress
Quill was built for MCP apps and desktop, not the main app, and its components
read as out of place next to the surrounding Lemon UI. LemonMenu is what the rest
of this scene uses.

The AGENTS.md line pointing new menus at quill is stale and is being corrected
separately.

@Twixes Twixes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sortafreel sortafreel added the reviewhog ($$$) Reviews pull requests before humans do label Jul 31, 2026
@posthog

posthog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🦔 ReviewHog reviewed this pull request

Found 0 must fix, 1 should fix, 3 consider.

Published 4 findings (view the review).

Naming the destination only earns its keep once something is connected. With
nothing linked to the project, connecting is exactly what the button does, and
the original label says so.

Gate the supporting line on the same condition: it explains what 'Manage on
GitHub' leads to, which is only a question once the project is connected. That
also leaves the unconnected state rendering exactly as it did when the visual
baselines were last taken.
@posthog

posthog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReviewHog Report

Changes

Issues: 4 issues

Files (9)
  • frontend/snapshots.yml
  • frontend/src/lib/integrations/integrationsLogic.ts
  • frontend/src/scenes/integrations/components/Integrations.tsx
  • posthog/api/github_callback/team_services.py
  • posthog/api/integration.py
  • products/integrations/frontend/generated/api.schemas.ts
  • products/integrations/frontend/generated/api.ts
  • products/integrations/mcp/tools.yaml
  • services/mcp/src/api/generated.ts

Comment thread frontend/src/lib/integrations/integrationsLogic.ts Outdated
Comment thread posthog/api/github_callback/team_services.py
Comment thread frontend/src/scenes/integrations/components/Integrations.tsx
Comment thread frontend/src/lib/integrations/integrationsLogic.ts
…enders

Also defer the repository cache blob in the auto-resolve branch, which only reads installation ids.
installation rather than reinstalling. This backs the picker: when the org has more than
one installation, the client passes the chosen installation_id to github/link_existing.
"""
installations = list_org_github_installations(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Team-scoped tokens can reuse installations outside their scope

The permission layer checks a scoped API or OAuth token only against the project in this URL, but this helper derives source projects from request.user.teams. A holder of an integration:write token scoped to the target project can therefore enumerate sibling installation IDs here and pass one to github/link_existing, whose source lookup uses the same account-wide team set, linking an installation from a project the token cannot access. Pass the authenticator's scoped team IDs into both helpers and intersect them with the user's accessible projects before listing or linking.

installation rather than reinstalling. This backs the picker: when the org has more than
one installation, the client passes the chosen installation_id to github/link_existing.
"""
installations = list_org_github_installations(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Project-scoped tokens can enumerate and reuse sibling-project GitHub installations

list_org_github_installations (and link_existing_team_github_integration) derives the caller's accessible source projects from request.user.teams — the key owner's full access — while scoped-token enforcement (check_team_and_org_permissions in permissions.py) only validates the project in the URL. A personal API key or OAuth token scoped to project A with integration:read can therefore call this new endpoint and enumerate GitHub installation IDs, account names, and source team IDs belonging to sibling project B (which the user can access but the token is not scoped to); with integration:write the same token can link that installation into A via github/link_existing and then read B's installation's repositories through A's github_repos, bypassing the token's project scope.

Prompt To Fix With AI
In `github_available_installations` and `github_link_existing` (posthog/api/integration.py), when the request is authenticated via PersonalAPIKeyAuthentication or OAuthAccessTokenAuthentication with non-empty scoped_teams/scoped_organizations, intersect the accessible team ids passed to `list_org_github_installations` / `_accessible_org_team_ids` with the token's scoped teams so a project-scoped token can only enumerate and reuse installations from projects within its scope. Session-authenticated users keep the current `user.teams` behavior.

Severity: medium | Confidence: 85% | React with 👍 if useful or 👎 if not

@Twixes
Twixes merged commit c003f0f into master Jul 31, 2026
275 checks passed
@Twixes
Twixes deleted the posthog-code/github-link-existing-installation-picker branch July 31, 2026 14:45
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-31 15:16 UTC Run
prod-us ✅ Deployed 2026-07-31 15:35 UTC Run
prod-eu ✅ Deployed 2026-07-31 15:35 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reviewhog ($$$) Reviews pull requests before humans do

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants