Detect GitHub OAuth access revocation via repeated sync failures, and let users re-authorize
What does this MR do and why
When a user revokes GitLab's OAuth access on GitHub's side, there's no webhook telling us — GitHub OAuth apps don't get a revocation event the way GitHub Apps do (installation.deleted). Sync just silently stops working with no signal to anyone. This MR adds detection for that by watching for repeated auth failures — after 5 in a row, we move the repository into a "disconnected", read-only state and record what it needs to restore to once the user re-authorizes.
What's in this MR:
- A
consecutive_auth_failurescounter column onimport_sync_repositories. - New
Import::Sync::Repositorymodel methods:record_auth_failure!increments the counter and disconnects the repository once it hits 5 in a row,clear_auth_failures!resets it after a successful call,disconnect!moves the repository into the disconnected/read-only state, andrestore!reverses that. WebhookManagementService#enablenow callsrecord_auth_failure!when creating the GitHub webhook fails with a 401. This is the only place in the codebase today that can actually hit this error, since the continuous-sync/reconciliation engine itself (a separate work item, still unbuilt) doesn't exist yet.- A new
Import::Sync::Github::Oauth::ReauthorizeService: takes a freshly-obtained OAuth client after the user re-authorizes, checks it's for the same GitHub account (so a different GitHub user can't hijack the reconnect), restores the repository's state, and re-creates the webhook.
What this MR deliberately leaves out: there's no new controller, route, or UI button for a user to actually trigger re-authorization. The settings panel meant to host that button is a separate work item that hasn't been built yet, so there's nowhere to wire it in. ReauthorizeService is ready to be called once that panel exists.
The failure threshold (5 in a row) is a judgment call — the original issue left the exact number open on purpose, to tolerate one-off blips without over-triggering.
All of this sits behind the existing github_continuous_import feature flag (wip type, default off), so there's no user-facing changelog entry beyond the one on the database migration (migrations always get one).
Note on testing: I wrote specs for all new model methods, the webhook service's new 401 handling, and the new re-authorize service, but could not run them locally — my local dev environment's gem bundle is out of sync (pre-existing issue, unrelated to this change) and the caproni exec wrapper is broken. This needs a real CI run to confirm the specs pass.
References
Part of gitlab-org#23553 (GitHub Sync - Experiment)
Implements #630404 (closed)
Depends on (already merged) #630400 (closed)
Follow-up needed once #628593 lands: wire ReauthorizeService up to a controller/route and the settings panel's "re-authorize" action.
Screenshots or screen recordings
Not applicable — backend-only change, no UI.
How to set up and validate locally
- In a Rails console, enable the feature flag:
Feature.enable(:github_continuous_import) - Create a test repository:
repo = FactoryBot.create(:import_sync_repository) - Call
repo.record_auth_failure!five times in a row, then reload:repo.reload.authority_stateshould be"disconnected", andrepo.disconnect_reasonshould be"token_revoked" - Call
repo.restore!and confirmrepo.reload.authority_stateis back to"provider_canonical", withdisconnect_reasonandrestore_authority_statebothnil - Run the new/changed specs:
bundle exec rspec ee/spec/models/import/sync/repository_spec.rb bundle exec rspec ee/spec/services/import/sync/github/oauth/webhook_management_service_spec.rb bundle exec rspec ee/spec/services/import/sync/github/oauth/reauthorize_service_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.