Skip to content

Commit 387f142

Browse files
stsewdagjohnsonhumitos
authored
GitHub App: open beta (#12217)
- Closes readthedocs/meta#187 - Closes #12202 --------- Co-authored-by: Anthony <[email protected]> Co-authored-by: Manuel Kaufmann <[email protected]>
1 parent 34512f6 commit 387f142

File tree

7 files changed

+217
-2
lines changed

7 files changed

+217
-2
lines changed

docs/user/reference/git-integration.rst

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,159 @@ depending on where the project you are trying to access has permissions from.
229229
.. seealso:: GitHub doc on `requesting access to your organization OAuth`_ for step-by-step instructions.
230230

231231
.. _requesting access to your organization OAuth: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/managing-your-membership-in-organizations/requesting-organization-approval-for-oauth-apps
232+
233+
GitHub App
234+
----------
235+
236+
.. warning::
237+
238+
Our GitHub App is currently in beta, see our `blog post <https://about.readthedocs.com/blog/2025/06/welcome-to-our-beta-github-app/>`__ for more information.
239+
240+
We are in the process of migrating our GitHub OAuth application to a `GitHub App <https://docs.github.com/en/apps/overview>`__.
241+
We have two GitHub Apps, one for each of our platforms:
242+
243+
- `Read the Docs Community <https://github.com/apps/read-the-docs-community>`__
244+
- `Read the Docs Business <https://github.com/apps/read-the-docs-business>`__
245+
246+
Features
247+
~~~~~~~~
248+
249+
When using GitHub, Read the Docs uses a GitHub App to interact with your repositories.
250+
This has the following benefits over using an OAuth application:
251+
252+
- More control over which repositories Read the Docs can access.
253+
You don't need to grant access to all your repositories in order to create an account or connect a project to a single repository.
254+
- No need to create webhooks on your repositories.
255+
The GitHub App subscribes to all required events when you install it.
256+
- No need to create a deploy key on your repository (|com_brand| only).
257+
The GitHub App can clone your private repositories using a temporal token.
258+
- If the original user who connected the repository to Read the Docs loses access to the project or repository,
259+
the GitHub App will still have access to the repository.
260+
- You can revoke access to the GitHub App at any time from your GitHub settings.
261+
- Never out of sync with changes on your repository.
262+
The GitHub App subscribes to all required events and will always keep your project up to date with your repository.
263+
264+
Adding a project from a repository
265+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
266+
267+
To add a project from a repository,
268+
you need to install the Read the Docs GitHub App and grant access to that repository.
269+
270+
- `Read the Docs Community <https://github.com/apps/read-the-docs-community/installations/new/>`__
271+
- `Read the Docs Business <https://github.com/apps/read-the-docs-business/installations/new/>`__
272+
273+
Once you have installed the GitHub App, click on the :guilabel:`Projects` tab, and click on :guilabel:`Add project`,
274+
search for the repository you want to create a project for, and then follow the instructions from there.
275+
276+
Connect a repository to an existing project
277+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
278+
279+
In case you manually added a project on Read the Docs,
280+
or if you want to connect your project to a different repository,
281+
you need to install the Read the Docs GitHub App and grant access to the repository you want to connect.
282+
283+
- `Read the Docs Community <https://github.com/apps/read-the-docs-community/installations/new/>`__
284+
- `Read the Docs Business <https://github.com/apps/read-the-docs-business/installations/new/>`__
285+
286+
Once you have installed the GitHub App, go the :guilabel:`Settings` page of the project,
287+
and select the repository you want to connect from the :guilabel:`Connected repository` dropdown.
288+
289+
Manually migrating a project
290+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
291+
292+
We recommend using the migration page to migrate your projects from the old OAuth application to the new GitHub App.
293+
294+
- `Read the Docs Community <https://app.readthedocs.com/accounts/migrate-to-github-app/>`__
295+
- `Read the Docs Business <https://app.readthedocs.com/accounts/migrate-to-github-app/>`__
296+
297+
But in case you need to manually migrate a project,
298+
you can follow these steps:
299+
300+
- Go to the :guilabel:`Settings` page of your Read the Docs project,
301+
and click on :guilabel:`Integrations`, and delete all the integrations that are listed there.
302+
- Go to the settings page of your GitHub repository,
303+
click on :guilabel:`Webhooks`, and delete all the webhooks with URLs that start with:
304+
305+
- ``https://readthedocs.org/api/v2/webhook/<your-project-slug>`` or ``https://app.readthedocs.org/api/v2/webhook/<your-project-slug>`` for Read the Docs Community.
306+
- ``https://readthedocs.com/api/v2/webhook/<your-project-slug>`` or ``https://app.readthedocs.com/api/v2/webhook/<your-project-slug>`` for Read the Docs Business.
307+
308+
- For projects using Read the Docs Business,
309+
go to the settings page of your GitHub repository,
310+
click on :guilabel:`Deploy keys`, and delete the deploy with a title matching the format ``[email protected] (<your-project-slug>)``.
311+
- :ref:`Connect the project to the repository <reference/git-integration:Connect a repository to an existing project>`.
312+
313+
Revoking access
314+
~~~~~~~~~~~~~~~
315+
316+
.. warning::
317+
318+
If you revoke access to the GitHub App with any of the methods below,
319+
all projects linked to that repository will stop working,
320+
but the projects and its documentation will still be available.
321+
If you grant access to the repository again,
322+
you will need to :ref:`manually connect your project to the repository <reference/git-integration:Connect a repository to an existing project>`.
323+
324+
You can revoke access to the Read the Docs GitHub App at any time from your GitHub settings.
325+
326+
- `Read the Docs Community <https://github.com/apps/read-the-docs-community/installations/new/>`__
327+
- `Read the Docs Business <https://github.com/apps/read-the-docs-business/installations/new/>`__
328+
329+
There are three ways to revoke access to the Read the Docs GitHub App:
330+
331+
Revoke access to one or more repositories:
332+
Remove the repositories from the list of repositories that the GitHub App has access to.
333+
Suspend the GitHub App:
334+
This will suspend the GitHub App and revoke access to all repositories.
335+
The installation and configuration will still be available,
336+
and you can re-enable the GitHub App at any time.
337+
Uninstall the GitHub App:
338+
This will uninstall the GitHub App and revoke access to all repositories.
339+
The installation and configuration will be removed,
340+
and you will need to re-install the GitHub App and reconfigure it to use it again.
341+
342+
Security
343+
~~~~~~~~
344+
345+
When cloning private repositories (|com_brand| only)
346+
Read the Docs creates an `installation access token <https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app>`__,
347+
which has read access to the `contents permission <https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-contents>`__,
348+
and it's scoped to the repository to be cloned.
349+
350+
This token is valid for one hour and GitHub automatically grants read access to the `metadata permission <https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-metadata>`__,
351+
which allows to query the repository collaborators, events, and other metadata.
352+
By default, Read the Docs doesn't show this token during the build,
353+
but the token is available during the whole build process.
354+
Make sure to not print it in your build logs,
355+
and that only trusted users are able to trigger builds on your project.
356+
357+
.. note::
358+
359+
If your repository is public, Read the Docs will not create an installation access token.
360+
361+
.. note::
362+
363+
The build log page is publicly accessible only if your project and version to build are marked as public.
364+
See more in :doc:`/commercial/privacy-level`.
365+
366+
Troubleshooting
367+
~~~~~~~~~~~~~~~
368+
369+
Repository not found in the repository list
370+
Make sure you have installed the corresponding GitHub App in your GitHub account or organization,
371+
and have granted access to the repository your project will be connected to.
372+
373+
- `Read the Docs Community <https://github.com/apps/read-the-docs-community/installations/new/>`__
374+
- `Read the Docs Business <https://github.com/apps/read-the-docs-business/installations/new/>`__
375+
376+
If you still can't see the repository in the list,
377+
you may need to wait a couple of minutes and refresh the page,
378+
or click on the "Refresh your repositories" button on the project creation page.
379+
380+
Repository is in the list, but isn't usable
381+
Make sure you have admin access to the repository you are trying to use for your project.
382+
If you are using |org_brand|, make sure your project is public,
383+
or use |com_brand| to create projects from private repositories.
384+
385+
If you still can't use the repository for your project,
386+
you may need to wait a couple of minutes and refresh the page,
387+
or click on the "Refresh your repositories" button on the project creation page.

readthedocs/core/adapters.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
88
from allauth.socialaccount.models import SocialAccount
99
from allauth.socialaccount.providers.github.provider import GitHubProvider
10+
from django.conf import settings
1011
from django.contrib import messages
1112
from django.http import HttpResponseRedirect
1213
from django.urls import reverse
@@ -134,7 +135,7 @@ def _can_use_github_app(self, user):
134135
135136
Only staff users can use the GitHub App for now.
136137
"""
137-
return user.is_staff
138+
return settings.RTD_ALLOW_GITHUB_APP or user.is_staff
138139

139140
def _block_use_of_old_github_oauth_app(self, request, sociallogin):
140141
"""

readthedocs/core/tests/test_adapters.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from allauth.socialaccount.models import SocialAccount, SocialLogin
66
from allauth.socialaccount.providers.github.provider import GitHubProvider
77
from django.contrib.auth.models import AnonymousUser, User
8-
from django.test import TestCase
8+
from django.test import TestCase, override_settings
99
from django_dynamic_fixture import get
1010

1111
from readthedocs.allauth.providers.githubapp.provider import GitHubAppProvider
@@ -16,6 +16,7 @@ def setUp(self):
1616
self.user = get(User, username="test")
1717
self.adapter = get_social_account_adapter()
1818

19+
@override_settings(RTD_ALLOW_GITHUB_APP=False)
1920
def test_dont_allow_using_githubapp_for_non_staff_users(self):
2021
assert not SocialAccount.objects.filter(provider=GitHubAppProvider.id).exists()
2122

@@ -45,6 +46,21 @@ def test_dont_allow_using_githubapp_for_non_staff_users(self):
4546
provider=GitHubAppProvider.id
4647
).exists()
4748

49+
@override_settings(RTD_ALLOW_GITHUB_APP=True)
50+
def test_allow_using_githubapp_for_all_users(self):
51+
assert not self.user.is_staff
52+
53+
request = mock.MagicMock(user=self.user)
54+
sociallogin = SocialLogin(
55+
user=User(email="[email protected]"),
56+
account=SocialAccount(provider=GitHubAppProvider.id),
57+
)
58+
self.adapter.pre_social_login(request, sociallogin)
59+
# No exception raised, but the account is not created, as that is done in another step by allauth.
60+
assert not self.user.socialaccount_set.filter(
61+
provider=GitHubAppProvider.id
62+
).exists()
63+
4864
def test_allow_using_githubapp_for_staff_users(self):
4965
self.user.is_staff = True
5066
self.user.save()

readthedocs/oauth/notifications.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.utils.translation import gettext_lazy as _
66

77
from readthedocs.notifications.constants import ERROR
8+
from readthedocs.notifications.constants import INFO
89
from readthedocs.notifications.constants import WARNING
910
from readthedocs.notifications.messages import Message
1011
from readthedocs.notifications.messages import registry
@@ -17,6 +18,7 @@
1718
MESSAGE_OAUTH_DEPLOY_KEY_ATTACHED_FAILED = "oauth:deploy-key:attached-failed"
1819
MESSAGE_OAUTH_WEBHOOK_NOT_REMOVED = "oauth:migration:webhook-not-removed"
1920
MESSAGE_OAUTH_DEPLOY_KEY_NOT_REMOVED = "oauth:migration:ssh-key-not-removed"
21+
MESSAGE_PROJECTS_TO_MIGRATE_TO_GITHUB_APP = "oauth:migration:projects-to-migrate-to-github-app"
2022

2123
messages = [
2224
Message(
@@ -112,5 +114,17 @@
112114
),
113115
type=WARNING,
114116
),
117+
Message(
118+
id=MESSAGE_PROJECTS_TO_MIGRATE_TO_GITHUB_APP,
119+
header=_("You have projects that need to be migrated to the new GitHub App"),
120+
body=_(
121+
textwrap.dedent(
122+
"""
123+
Migrate your projects automatically using the <a href="{% url "migrate_to_github_app" %}">migration page</a>.
124+
"""
125+
).strip(),
126+
),
127+
type=INFO,
128+
),
115129
]
116130
registry.add(messages)

readthedocs/oauth/signals.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from django.db.models.signals import post_save
77
from django.dispatch import receiver
88

9+
from readthedocs.allauth.providers.githubapp.provider import GitHubAppProvider
10+
from readthedocs.notifications.models import Notification
11+
from readthedocs.oauth.migrate import has_projects_pending_migration
912
from readthedocs.oauth.models import RemoteRepository
13+
from readthedocs.oauth.notifications import MESSAGE_PROJECTS_TO_MIGRATE_TO_GITHUB_APP
1014
from readthedocs.oauth.tasks import sync_remote_repositories
1115
from readthedocs.projects.models import Feature
1216

@@ -46,3 +50,17 @@ def update_project_clone_url(sender, instance, created, *args, **kwargs):
4650
instance.projects.exclude(feature__feature_id=Feature.DONT_SYNC_WITH_REMOTE_REPO).update(
4751
repo=instance.clone_url
4852
)
53+
54+
55+
@receiver(social_account_added, sender=SocialLogin)
56+
def notify_user_about_migration_on_github_app_connection(sender, request, sociallogin, **kwargs):
57+
provider = sociallogin.account.get_provider()
58+
if provider.id != GitHubAppProvider.id:
59+
return
60+
61+
if has_projects_pending_migration(sociallogin.user):
62+
Notification.objects.add(
63+
message_id=MESSAGE_PROJECTS_TO_MIGRATE_TO_GITHUB_APP,
64+
attached_to=sociallogin.user,
65+
dismissable=True,
66+
)

readthedocs/profiles/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ def post(self, request, *args, **kwargs):
399399
else:
400400
projects = get_valid_projects_missing_migration(request.user)
401401

402+
migrated_projects_count = 0
402403
for project in projects:
403404
result = migrate_project_to_github_app(project=project, user=request.user)
404405
if not result.webhook_removed:
@@ -421,5 +422,13 @@ def post(self, request, *args, **kwargs):
421422
"project_slug": project.slug,
422423
},
423424
)
425+
migrated_projects_count += 1
426+
427+
if migrated_projects_count > 0:
428+
messages.success(
429+
request, _(f"Successfully migrated {migrated_projects_count} project(s).")
430+
)
431+
else:
432+
messages.info(request, _("No projects were migrated."))
424433

425434
return HttpResponseRedirect(reverse("migrate_to_github_app") + "?step=migrate")

readthedocs/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ def SOCIALACCOUNT_PROVIDERS(self):
738738
GITHUB_APP_NAME = "readthedocs"
739739
GITHUB_APP_PRIVATE_KEY = ""
740740
GITHUB_APP_WEBHOOK_SECRET = ""
741+
RTD_ALLOW_GITHUB_APP = True
741742

742743
@property
743744
def GITHUB_APP_CLIENT_ID(self):

0 commit comments

Comments
 (0)