diff --git a/readthedocs/oauth/migrations/0019_githubapp_add_extra_fields.py b/readthedocs/oauth/migrations/0019_githubapp_add_extra_fields.py new file mode 100644 index 00000000000..56871fa019f --- /dev/null +++ b/readthedocs/oauth/migrations/0019_githubapp_add_extra_fields.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.11 on 2026-03-04 23:02 + +from django.db import migrations +from django.db import models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.before_deploy() + dependencies = [ + ("oauth", "0018_githubapp"), + ] + + operations = [ + migrations.AddField( + model_name="githubappinstallation", + name="all_repositories_selected", + field=models.BooleanField( + db_default=False, + default=False, + help_text="Whether the installation has access to all repositories or just some of them", + ), + ), + migrations.AddField( + model_name="githubappinstallation", + name="target_login", + field=models.CharField( + help_text="The account login the installation belongs to", + max_length=255, + null=True, + ), + ), + ] diff --git a/readthedocs/oauth/models.py b/readthedocs/oauth/models.py index 5cf93bb7d6d..90038eb2743 100644 --- a/readthedocs/oauth/models.py +++ b/readthedocs/oauth/models.py @@ -34,14 +34,26 @@ def get_or_create_installation( Only the installation_id is unique, the target_id and target_type could change, but this should never happen. """ + extra_data = extra_data or {} installation, created = self.get_or_create( installation_id=installation_id, defaults={ "target_id": target_id, "target_type": target_type, - "extra_data": extra_data or {}, + "extra_data": extra_data, }, ) + + # Keep the extra data about the installation up to date. + new_installation_data = extra_data.get("installation", {}) + installation_has_changed = ( + new_installation_data + and new_installation_data != installation.extra_data.get("installation", {}) + ) + if not created and installation_has_changed: + installation.extra_data = extra_data + installation.save() + # NOTE: An installation can't change its target_id or target_type. # This should never happen, unless this assumption is wrong. if installation.target_id != target_id or installation.target_type != target_type: @@ -78,6 +90,16 @@ class GitHubAppInstallation(TimeStampedModel): choices=GitHubAccountType, max_length=255, ) + target_login = models.CharField( + help_text=_("The account login the installation belongs to"), + null=True, + max_length=255, + ) + all_repositories_selected = models.BooleanField( + help_text=_("Whether the installation has access to all repositories or just some of them"), + db_default=False, + default=False, + ) extra_data = models.JSONField( help_text=_("Extra data returned by the webhook when the installation is created"), default=dict, @@ -173,6 +195,12 @@ def delete_organization(self, organization_id: int): target_type=self.target_type, ) + def save(self, *args, **kwargs): + installation_data = self.extra_data.get("installation", {}) + self.target_login = installation_data.get("account", {}).get("login") + self.all_repositories_selected = installation_data.get("repository_selection") == "all" + super().save(*args, **kwargs) + class RemoteOrganization(TimeStampedModel): """ diff --git a/readthedocs/oauth/tests/test_githubapp_webhook.py b/readthedocs/oauth/tests/test_githubapp_webhook.py index 2b1a3e9d5c8..dc0b802a4cb 100644 --- a/readthedocs/oauth/tests/test_githubapp_webhook.py +++ b/readthedocs/oauth/tests/test_githubapp_webhook.py @@ -101,6 +101,10 @@ def test_installation_created(self, sync): "id": new_installation_id, "target_id": 2222, "target_type": GitHubAccountType.USER, + "repository_selection": "all", + "account": { + "login": "user", + }, }, } r = self.post_webhook("installation", payload) @@ -111,6 +115,8 @@ def test_installation_created(self, sync): ) assert installation.target_id == 2222 assert installation.target_type == GitHubAccountType.USER + assert installation.all_repositories_selected + assert installation.target_login == "user" sync.assert_called_once() @mock.patch.object(GitHubAppService, "sync") @@ -121,6 +127,10 @@ def test_installation_created_with_existing_installation(self, sync): "id": self.installation.installation_id, "target_id": self.installation.target_id, "target_type": self.installation.target_type, + "repository_selection": "all", + "account": { + "login": "user", + }, }, } r = self.post_webhook("installation", paylod) @@ -129,6 +139,8 @@ def test_installation_created_with_existing_installation(self, sync): self.installation.refresh_from_db() assert self.installation.target_id == 1111 assert self.installation.target_type == GitHubAccountType.USER + assert self.installation.all_repositories_selected + assert self.installation.target_login == "user" assert GitHubAppInstallation.objects.count() == 1 @mock.patch.object(GitHubAppService, "sync")