Skip to content

Move GitHub integration from the OAuth App to a GitHub App - #533

Merged
simonhamp merged 9 commits into
mainfrom
github-app-migration
Sep 26, 2026
Merged

simonhamp merged 9 commits into
mainfrom
github-app-migration

Conversation

@simonhamp

@simonhamp simonhamp commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Our GitHub OAuth App asks for the repo scope, which gives us read and write access to every private repo a user owns. This moves nativephp.com to a GitHub App, so we only see the repos people choose to share with us. The old OAuth App keeps working while people move over.

How it works

  • "Continue with GitHub" and account linking go through the GitHub App's own sign-in and only ask for identity. People are matched on their GitHub user ID, so existing users sign in as normal.
  • Repo work (plugin sync, release sync, repo review) uses installation tokens, which only reach the repos the user granted when installing the app.
  • Tokens are tried in this order: installation token, then the user's token, then the platform token.
  • GitHub App user tokens expire after 8 hours. We store the refresh token and renew them automatically.
  • Installations live in a new github_installations table. They're kept up to date by:
    • the app webhook at /webhooks/github-app
    • the post-install redirect to /auth/github/setup
    • a daily github:sync-installations job that catches anything the webhook missed
  • The setup redirect checks the installation_id against the user's own installations before saving it, since that value comes from the query string.
  • Push and release events for repos the app covers now come through the app webhook. Those plugins no longer need a per-repo webhook. Plugins the app can't reach still get the old one.
  • If the GITHUB_APP_* env vars aren't set, everything falls back to the old OAuth App, so this can be deployed before the app is configured.

The platform GITHUB_TOKEN is unchanged and still handles invites to nativephp/mobile and nativephp/claude-code.

Moving people over

  • users.github_auth_type records which flow someone is on. A migration marks everyone with an existing token as oauth.
  • When a legacy user signs in with GitHub, they're switched to the app and their old OAuth grant is revoked on GitHub. If they have plugin repos the app can't reach, they're sent to the install page first.
  • Banner on Integrations and Plugins:
    • plugin authors on the old connection get an urgent version with the deadline and a list of repos to grant
    • everyone else gets a soft note saying they don't need to do anything
  • Legacy users can't create new plugins until they connect the app.
  • App users whose plugin repos aren't covered see a banner listing those repos. The create page prompts them to install rather than showing an empty repo list.
  • Integrations shows each installation and whether each plugin repo is covered.
  • The Filament users table has a GitHub connection column, a filter by connection type, and a filter for plugin authors still on the old connection.

Rollout

  1. Create the GitHub App (settings below) and set GITHUB_APP_ID, GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY and GITHUB_APP_WEBHOOK_SECRET. The private key goes in .env inline, in double quotes, either across multiple lines or on one line with \n. The app's slug is fixed as nativephp-plugin-marketplace in config/services.php.
  2. Pick a cutoff date and set GITHUB_LEGACY_OAUTH_CUTOFF_DATE. It appears in the banner and the email.
  3. Check the email with php artisan github:send-app-migration-notice --preview=you@example.com, then send it with php artisan github:send-app-migration-notice. Run it with --dry-run first to see who gets it. Each person is only emailed once, and it won't send without a cutoff date.
  4. After the cutoff date, legacy tokens are ignored automatically and anything that needed one uses the platform token instead, so public plugin repos keep syncing. Delete the old OAuth App on GitHub, then run php artisan github:retire-legacy-oauth. It clears the stored legacy tokens, keeps GitHub IDs and usernames, and lists plugin authors who never moved over.

GitHub App settings

  • Callback URL: https://nativephp.com/auth/github/callback (shared with the old OAuth App, which is fine)
  • Expire user authorization tokens: on
  • Request user authorization (OAuth) during installation: off. With it on, GitHub skips the Setup URL and our callback rejects the request.
  • Setup URL: https://nativephp.com/auth/github/setup, with "Redirect on update" ticked so we pick up repo changes straight away
  • Webhook URL: https://nativephp.com/webhooks/github-app, subscribed to Installation, Installation repositories, Push and Release
  • Repository permissions: Contents (read), Metadata (read)
  • Account permissions: Email addresses (read). Without this, GitHub sign-ups arrive with no email.

Known gaps

  • People who connected GitHub between November 2025 and January 2026 have a GitHub ID but no stored token. The backfill doesn't tag them as legacy, so they get no banner or email. Tagging them is a small change to the backfill migration.
  • When a plugin sync fails because the app can't reach the repo, nothing tells the author at that moment. They only see it in the banner the next time they visit the dashboard.
  • Plugins that already have a per-repo webhook and are also covered by the app sync twice per push. This is harmless because syncing skips releases it already has.
  • Out of scope for this PR: installation webhooks sent by someone we can't match to a user (e.g. an org admin who isn't the author) are dropped, and Satis builds get an installation token that expires after an hour.

Testing

The full suite passes (1806 tests, 1 skipped). New tests cover sign-in and linking, token refresh and revocation, setup verification, installation syncing, the app webhook, token resolution, the banners and create page prompt, the email and both commands, and the Filament filter.

🤖 Generated with Claude Code

simonhamp and others added 8 commits April 11, 2026 13:44
Replace the broad-scope OAuth App integration with a fine-grained GitHub
App that uses user access tokens for login and installation access tokens
for repo operations. Legacy OAuth users see a blocking banner encouraging
them to upgrade, and plugin submission is gated until they do.

- Add github_installations table and github_auth_type column on users
- Register github-app Socialite driver alongside legacy github driver
- Add GitHubAppService for JWT generation and installation token management
- Add webhook controller for installation lifecycle events
- Update token resolution with 3-tier priority: installation > user > platform
- Add migration banner to integrations, plugins index, and plugin create pages
- Add GitHubAppStatus Livewire component showing installation coverage
- Block legacy OAuth users from plugin submission
- Add 27 new tests covering auth, webhooks, token resolution, and UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved 6 conflicts:
- User.php: combined imports from both branches
- routes/web.php: kept dashboard/ prefix from main + new github setup route
- 4 modify/delete: accepted main's deletion of old blade views and
  CustomerPluginController, re-applied migration banner and blocking
  logic to new Livewire components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Conflicts were all cases where both sides added something next to
each other, so both were kept:

- User.php: our GitHubAuthType import and cast alongside main's
  MustVerifyEmail import and early adopter role cast
- GitHubIntegrationController: Cache and Http imports
- .env.example: GitHub App vars alongside the new Turnstile vars
- integrations view: main's new GitHub Account card and disconnect
  modal, followed by our GitHub App status panel

Main also added GitHubUserService::webhookExists(), which read the
user's token directly. Switched it to resolveTokenForRepo() so GitHub
App users get the installation token like the other repo calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signing in with the GitHub App switched legacy users over without ever
asking them to install the app, which quietly broke syncing for their
plugin repos. Login now redirects anyone with uncovered plugin repos to
the install page, the banner lists repos the app can't reach, and the
create page prompts for an install instead of showing an empty repo list.

Also clears the cached repo list when an installation is recorded or
its repos change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Verify the installation_id on the setup redirect against the user's own
  installations, and fetch the repo list during setup
- Add github:sync-installations (daily) to catch missed webhooks
- Store refresh tokens and renew expired GitHub App user tokens
- Revoke the legacy OAuth grant when someone moves to the app
- Handle push and release events through the app webhook, so covered
  plugins no longer need a per-repo webhook
- Add github:send-app-migration-notice (with --preview) and the email
- Add GITHUB_LEGACY_OAUTH_CUTOFF_DATE: legacy tokens are ignored after it,
  and github:retire-legacy-oauth clears them
- Banner: urgent for plugin authors, a soft note for everyone else
- GitHub connection column and filters in the Filament users table

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI copies .env.example, which has an empty GITHUB_APP_PRIVATE_KEY_PATH.
That reached file_get_contents('') and threw a ValueError that the
surrounding catch blocks don't handle. Treat an empty value as unset and
throw a clear exception when the key file is missing.

Also fixes import style in four files Pint flagged, and moves
GitHubAppServiceTest into tests/Feature, since phpunit.xml only runs the
Feature suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace GITHUB_APP_PRIVATE_KEY_PATH with an inline GITHUB_APP_PRIVATE_KEY.
  It can be pasted across multiple lines or kept on one line with \n.
- Set the app slug to nativephp-plugin-marketplace in config, since it's
  public and fixed, and drop GITHUB_APP_SLUG.
- Build the Integrations panel's install links with installationUrl()
  like everywhere else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simonhamp
simonhamp marked this pull request as ready for review September 26, 2026 11:52
@simonhamp
simonhamp merged commit dbd9a61 into main Sep 26, 2026
3 checks passed
@simonhamp
simonhamp deleted the github-app-migration branch September 26, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant