Skip to content

Migrate GitHub OAuth App to GitHub App #11942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 111 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
926f95d
Migrate GitHub OAuth App to GitHub App
stsewd Jan 22, 2025
9c5746c
Merge branch 'main' into migrate-to-gh-apps
stsewd Jan 28, 2025
1314a9c
wip
stsewd Jan 30, 2025
22de5d6
Migration and stuff
stsewd Feb 3, 2025
5119e48
Fixes
stsewd Feb 3, 2025
a854c7e
More hooks!
stsewd Feb 4, 2025
7235cd8
Bug fixes and refactor
stsewd Feb 4, 2025
632e2e5
Format
stsewd Feb 4, 2025
b0e2181
More events
stsewd Feb 4, 2025
ba47293
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 4, 2025
91d23aa
More hooks and fixes
stsewd Feb 5, 2025
ddadee1
Implement members hook
stsewd Feb 5, 2025
2c4c0cc
Dead code
stsewd Feb 5, 2025
3c61db6
Delete code
stsewd Feb 5, 2025
88b0021
Poor man implementation
stsewd Feb 6, 2025
2987cf8
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 6, 2025
ad5655e
Stash
stsewd Feb 6, 2025
fa53c1c
Git service: depend on the project instead of users
stsewd Feb 7, 2025
f031b4b
Fix name
stsewd Feb 7, 2025
f7a3917
Fix tests
stsewd Feb 10, 2025
083cfa0
Small updates
stsewd Feb 10, 2025
8e6caa3
Format
stsewd Feb 10, 2025
4671a46
Merge branch 'refactor-services' into migrate-to-gh-apps
stsewd Feb 10, 2025
6d686f3
Format
stsewd Feb 10, 2025
d771092
Refactor
stsewd Feb 10, 2025
0d0511c
Fix import
stsewd Feb 10, 2025
4308bbf
Check for None
stsewd Feb 10, 2025
7065003
refactor
stsewd Feb 10, 2025
b2a8a2b
Fix import
stsewd Feb 10, 2025
b0d9dcc
Full feature!
stsewd Feb 11, 2025
54e7ec6
More refactor
stsewd Feb 11, 2025
4d5a229
More fixes and updates
stsewd Feb 11, 2025
12aab79
Docstrings and updates
stsewd Feb 11, 2025
c2ffe7d
Note on GH user access tokens
stsewd Feb 11, 2025
56b024f
Remove code
stsewd Feb 11, 2025
c09186b
Error handling and logging
stsewd Feb 12, 2025
0b2c686
Validate when trying to connect to an existing account
stsewd Feb 12, 2025
a7f1cb7
Less indentation
stsewd Feb 12, 2025
50637b5
And both apps lived happy forever after.
stsewd Feb 12, 2025
7fc1466
Updates from review
stsewd Feb 12, 2025
27ad8c6
Skip incompatible integrations
stsewd Feb 12, 2025
6ea1655
Format
stsewd Feb 12, 2025
5daf4da
Check for attribute instead
stsewd Feb 13, 2025
8e73ab4
Fix querysets
stsewd Feb 13, 2025
1289298
Git service: attach each service to a allauth provider
stsewd Feb 13, 2025
5319608
Missed this file
stsewd Feb 13, 2025
c9eb355
Use just the hostname for the base_api_url
stsewd Feb 13, 2025
c7b259b
Merge branch 'abstract-allauth-from-services' into migrate-to-gh-apps
stsewd Feb 13, 2025
e031477
Stash
stsewd Feb 14, 2025
bf5014c
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 14, 2025
6165e4c
Format
stsewd Feb 14, 2025
37738df
stash
stsewd Feb 17, 2025
160d41f
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 17, 2025
cd5015c
Basic migration page
stsewd Feb 18, 2025
f67e028
Don't use the ID to get the organization
stsewd Feb 18, 2025
933658c
Docstring
stsewd Feb 18, 2025
346cc34
Fix template
stsewd Feb 18, 2025
cac9567
Cleanup
stsewd Feb 18, 2025
db1950f
More updates
stsewd Feb 18, 2025
75cc864
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 19, 2025
87dad06
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 19, 2025
4b84536
Check for githuapp
stsewd Feb 19, 2025
3899104
Uninstalling the app from a repository, means unlinking it from the p…
stsewd Feb 19, 2025
acfda4c
Format
stsewd Feb 19, 2025
d1e3cb4
Add a sync repositories permissions method
stsewd Feb 20, 2025
de06013
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 24, 2025
b3ad1a2
Add tests for webhook
stsewd Feb 25, 2025
3fb1083
Stash
stsewd Feb 26, 2025
34052b3
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 26, 2025
c5ea8a1
Merge branch 'main' into migrate-to-gh-apps
stsewd Feb 26, 2025
eacf058
Tests
stsewd Feb 27, 2025
33d6151
Merge branch 'main' into migrate-to-gh-apps
stsewd Mar 10, 2025
619858c
Tests
stsewd Mar 10, 2025
80e6c88
More tests
stsewd Mar 10, 2025
926ed6d
Tests for tasks
stsewd Mar 10, 2025
0c338ab
stash
stsewd Mar 11, 2025
616166e
Format to avoid conflicts
stsewd Mar 11, 2025
2c81a2a
Merge branch 'main' into migrate-to-gh-apps
stsewd Mar 11, 2025
b58c3b8
Format
stsewd Mar 11, 2025
d106277
Update migration
stsewd Mar 11, 2025
c2a89d7
Remove suspended installations
stsewd Mar 11, 2025
7fffcea
Delete installation when suspended
stsewd Mar 11, 2025
0adc4b4
Handle authorization event
stsewd Mar 11, 2025
7e3f3f4
Tests
stsewd Mar 12, 2025
5c1ce0b
Merge branch 'main' into migrate-to-gh-apps
stsewd Mar 12, 2025
fa1b70c
Small changes
stsewd Mar 12, 2025
339cf73
Complete migrate view
stsewd Mar 18, 2025
7ff1e96
Create notifications when an error happens during the migration
stsewd Mar 18, 2025
9b14e95
Fixes
stsewd Mar 18, 2025
704ecb4
Check for users with multiple GH accounts
stsewd Mar 19, 2025
19dc240
Better default for accounts linked to multiple GH accounts
stsewd Mar 19, 2025
5a5d532
Rollback changes to avoid merge conflicts
stsewd Mar 20, 2025
26ef7eb
Re-add pygithub
stsewd Mar 20, 2025
34ddb25
Merge branch 'main' into migrate-to-gh-apps
stsewd Mar 20, 2025
33893de
Clean up and docstrings
stsewd Mar 20, 2025
5964ff1
Merge branch 'main' into migrate-to-gh-apps
stsewd Mar 20, 2025
8f23fa4
Docs about local dev
stsewd Mar 24, 2025
192f3ac
Add tests for migration page
stsewd Mar 25, 2025
66fdd95
Merge branch 'main' into migrate-to-gh-apps
stsewd Mar 25, 2025
08f9d2c
Fix merge conflict
stsewd Mar 25, 2025
ce6c2da
Match changes
stsewd Mar 25, 2025
c489137
Decouple migration from current GH account
stsewd Mar 27, 2025
3a88a62
More decoupling
stsewd Mar 27, 2025
928984b
Very WIP docs
stsewd Mar 27, 2025
6227ea3
Update docs
stsewd Mar 31, 2025
c962205
Add old_github_accounts so we can use it as a listing
stsewd Apr 14, 2025
6c5eefb
Add type
stsewd Apr 14, 2025
60a5228
Merge branch 'main' into migrate-to-gh-apps
stsewd Apr 15, 2025
f2d5498
Fix merge conflicts
stsewd Apr 15, 2025
34d7ed6
Format
stsewd Apr 15, 2025
225c640
Fix reference
stsewd Apr 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
12 changes: 12 additions & 0 deletions readthedocs/allauth/providers/githubapp/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from allauth.socialaccount.providers.github.provider import GitHubProvider

from readthedocs.allauth.providers.githubapp.views import GitHubAppOAuth2Adapter


class GitHubAppProvider(GitHubProvider):
id = "githubapp"
name = "GitHub App"
oauth2_adapter_class = GitHubAppOAuth2Adapter


provider_classes = [GitHubAppProvider]
5 changes: 5 additions & 0 deletions readthedocs/allauth/providers/githubapp/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns

from readthedocs.allauth.providers.githubapp.provider import GitHubAppProvider

urlpatterns = default_urlpatterns(GitHubAppProvider)
13 changes: 13 additions & 0 deletions readthedocs/allauth/providers/githubapp/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.oauth2.views import (
OAuth2CallbackView,
OAuth2LoginView,
)


class GitHubAppOAuth2Adapter(GitHubOAuth2Adapter):
provider_id = "githubapp"


oauth2_login = OAuth2LoginView.adapter_view(GitHubAppOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(GitHubAppOAuth2Adapter)
12 changes: 12 additions & 0 deletions readthedocs/core/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import structlog
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers.github.provider import GitHubProvider
from django.utils.encoding import force_str

from readthedocs.allauth.providers.githubapp.provider import GitHubAppProvider
from readthedocs.core.utils import send_email_from_object
from readthedocs.invitations.models import Invitation

Expand Down Expand Up @@ -65,3 +68,12 @@ def pre_social_login(self, request, sociallogin):
sociallogin.email_addresses = [
email for email in sociallogin.email_addresses if email.primary
]

provider = sociallogin.account.get_provider()
if provider.id == GitHubAppProvider.id and not sociallogin.is_existing:
social_account = SocialAccount.objects.filter(
provider=GitHubProvider.id,
uid=sociallogin.account.uid,
).first()
if social_account:
sociallogin.connect(request, social_account.user)
44 changes: 38 additions & 6 deletions readthedocs/core/views/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
log = structlog.get_logger(__name__)


def _build_version(project, slug, already_built=()):
def _build_version(project, version):
"""
Where we actually trigger builds for a project and slug.

Expand All @@ -27,24 +27,27 @@ def _build_version(project, slug, already_built=()):
# Previously we were building the latest version (inactive or active)
# when building the default version,
# some users may have relied on this to update the version list #4450
version = project.versions.filter(active=True, slug=slug).first()
if version and slug not in already_built:
if version.active:
log.info(
"Building.",
project_slug=project.slug,
version_slug=version.slug,
)
trigger_build(project=project, version=version)
return slug
return version.slug

log.info("Not building.", version_slug=slug)
log.info("Not building.", version_slug=version.slug)
return None


def build_branches(project, branch_list):
"""
Build the branches for a specific project.

.. warning::

Deprecated, use ``build_versions_from_names`` instead.

Returns:
to_build - a list of branches that were built
not_building - a list of branches that we won't build
Expand All @@ -59,14 +62,43 @@ def build_branches(project, branch_list):
project_slug=project.slug,
version_slug=version.slug,
)
ret = _build_version(project, version.slug, already_built=to_build)
if version.slug in to_build:
continue
ret = _build_version(project, version)
if ret:
to_build.add(ret)
else:
not_building.add(version.slug)
return (to_build, not_building)


def build_versions_from_names(project, version_names: list[tuple[str, str]]):
"""
Build the branches or tags from the project.

:param project: Project instance
:param version_names: A list of tuples with the version name and type.
:returns: A tuple with the versions that were built and the versions that were not built.
"""
to_build = set()
not_building = set()
for version_name, version_type in version_names:
for version in project.versions_from_name(version_name, version_type):
log.debug(
"Processing.",
project_slug=project.slug,
version_slug=version.slug,
)
if version.slug in to_build:
continue
triggered = _build_version(project, version)
if triggered:
to_build.add(triggered)
else:
not_building.add(version.slug)
return to_build, not_building


def trigger_sync_versions(project):
"""
Sync the versions of a repo using its latest version.
Expand Down
3 changes: 3 additions & 0 deletions readthedocs/oauth/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
from django.contrib import admin

from .models import (
GitHubAppInstallation,
RemoteOrganization,
RemoteOrganizationRelation,
RemoteRepository,
RemoteRepositoryRelation,
)

admin.site.register(GitHubAppInstallation)


@admin.register(RemoteRepository)
class RemoteRepositoryAdmin(admin.ModelAdmin):
Expand Down
39 changes: 39 additions & 0 deletions readthedocs/oauth/migrations/0017_githubapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.2.18 on 2025-02-03 21:58
from django_safemigrate import Safe

from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields


class Migration(migrations.Migration):

safe = Safe.before_deploy

dependencies = [
('oauth', '0016_deprecate_old_vcs'),
]

operations = [
migrations.CreateModel(
name='GitHubAppInstallation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('installation_id', models.PositiveBigIntegerField(db_index=True, help_text='The application installation ID', unique=True)),
('target_id', models.PositiveBigIntegerField(help_text='A GitHub account ID, it can be from a user or an organization')),
('target_type', models.CharField(choices=[('User', 'User'), ('Organization', 'Organization')], help_text='Account type that the target_id belongs to (user or organization)', max_length=255)),
('extra_data', models.JSONField(default=dict, help_text='Extra data returned by the webhook when the installation is created')),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
migrations.AddField(
model_name='remoterepository',
name='github_app_installation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='repositories', to='oauth.githubappinstallation', verbose_name='GitHub App Installation'),
),
]
57 changes: 55 additions & 2 deletions readthedocs/oauth/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""OAuth service models."""

from functools import cached_property

from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import User
from django.core.validators import URLValidator
Expand All @@ -15,8 +17,46 @@
from .querysets import RemoteOrganizationQuerySet, RemoteRepositoryQuerySet


class RemoteOrganization(TimeStampedModel):
class GitHubAccountType(models.TextChoices):
USER = "User", _("User")
ORGANIZATION = "Organization", _("Organization")


class GitHubAppInstallation(TimeStampedModel):
installation_id = models.PositiveBigIntegerField(
help_text=_("The application installation ID"),
unique=True,
db_index=True,
)
target_id = models.PositiveBigIntegerField(
help_text=_("A GitHub account ID, it can be from a user or an organization"),
)
target_type = models.CharField(
help_text=_(
"Account type that the target_id belongs to (user or organization)"
),
choices=GitHubAccountType.choices,
max_length=255,
)
extra_data = models.JSONField(
help_text=_(
"Extra data returned by the webhook when the installation is created"
),
default=dict,
)

class Meta(TimeStampedModel.Meta):
pass

@cached_property
def service(self):
"""Return the service for this installation."""
from readthedocs.oauth.services.githubapp import GitHubAppService

return GitHubAppService(self)


class RemoteOrganization(TimeStampedModel):
"""
Organization from remote service.

Expand Down Expand Up @@ -98,7 +138,6 @@ class Meta:


class RemoteRepository(TimeStampedModel):

"""
Remote importable repositories.

Expand Down Expand Up @@ -174,6 +213,20 @@ class RemoteRepository(TimeStampedModel):
_("VCS provider"), choices=VCS_PROVIDER_CHOICES, max_length=32
)

github_app_installation = models.ForeignKey(
GitHubAppInstallation,
verbose_name=_("GitHub App Installation"),
related_name="repositories",
null=True,
blank=True,
# Delete the repository if the installation is deleted?
# or keep the repository and just remove the installation?
# I think we should keep the repository, but only if it's linked to a project,
# since a user could re-install the app, they shouldn't need to
# manually link each project to the repository again.
on_delete=models.SET_NULL,
)

objects = RemoteRepositoryQuerySet.as_manager()

class Meta:
Expand Down
Loading