Skip to content
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

Re-sync GitHub (RemoteRepository and RemoteOrganization) on webhook #7336

Closed
wants to merge 7 commits into from
29 changes: 28 additions & 1 deletion readthedocs/api/v2/views/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import logging
import re

from allauth.socialaccount.providers.github.provider import GitHubProvider
from allauth.socialaccount.models import SocialAccount

from django.shortcuts import get_object_or_404
from rest_framework import permissions, status
from rest_framework.exceptions import NotFound, ParseError
Expand All @@ -14,6 +17,7 @@
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView

from readthedocs.core.permissions import AdminPermission
from readthedocs.core.signals import (
webhook_bitbucket,
webhook_github,
Expand All @@ -27,12 +31,14 @@
trigger_sync_versions,
)
from readthedocs.integrations.models import HttpExchange, Integration
from readthedocs.projects.models import Feature, Project
from readthedocs.oauth.tasks import sync_remote_repositories, sync_remote_repositories_organizations
from readthedocs.projects.models import Project, Feature

log = logging.getLogger(__name__)

GITHUB_EVENT_HEADER = 'HTTP_X_GITHUB_EVENT'
GITHUB_SIGNATURE_HEADER = 'HTTP_X_HUB_SIGNATURE'
GITHUB_MEMBER = 'member'
GITHUB_PUSH = 'push'
GITHUB_PULL_REQUEST = 'pull_request'
GITHUB_PULL_REQUEST_OPENED = 'opened'
Expand Down Expand Up @@ -452,6 +458,27 @@ def handle_webhook(self):
except KeyError:
raise ParseError('Parameter "ref" is required')

# Re-sync repositories for the user if any permission has changed
if event == GITHUB_MEMBER:
uid = self.data.get('member').get('id')
socialaccount = SocialAccount.objects.get(
provider=GitHubProvider.id,
uid=uid,
)

# Retrieve all organization the user belongs to
organization_slugs = set(
AdminPermission.projects(
socialaccount.user,
admin=True,
member=True,
).values_list('organizations__slug', flat=True)
)
if organization_slugs:
sync_remote_repositories_organizations(organization_slugs=organization_slugs)
else:
sync_remote_repositories.delay(socialaccount.user.pk)

return None

def _normalize_ref(self, ref):
Expand Down
20 changes: 20 additions & 0 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,26 @@ def is_external(self):
type = self.version.type
return type == EXTERNAL

@property
def can_rebuild(self):
"""
Check if external build can be rebuilt.

Rebuild can be done only if the build is external,
build version is active and
it's the latest build for the version.
see https://github.com/readthedocs/readthedocs.org/pull/6995#issuecomment-852918969
"""
if self.is_external:
is_latest_build = (
self == Build.objects.filter(
project=self.project,
version=self.version
).only('id').first()
)
return self.version and self.version.active and is_latest_build
return False

@property
def external_version_name(self):
if self.is_external:
Expand Down
6 changes: 0 additions & 6 deletions readthedocs/builds/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,6 @@ def get_context_data(self, **kwargs):
context['project'] = self.project

build = self.get_object()
context['is_latest_build'] = (
build == Build.objects.filter(
project=build.project,
version=build.version,
).first()
)

if build.error != BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format(build_id=build.pk):
# Do not suggest to open an issue if the error is not generic
Expand Down
2 changes: 1 addition & 1 deletion readthedocs/oauth/services/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def get_webhook_data(self, project, integration):
'secret': integration.secret,
'content_type': 'json',
},
'events': ['push', 'pull_request', 'create', 'delete'],
'events': ['push', 'pull_request', 'create', 'delete', 'member'],
})

def get_provider_data(self, project, integration):
Expand Down
63 changes: 63 additions & 0 deletions readthedocs/rtd_tests/tests/test_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,69 @@ def test_version_deleted(self):
self.assertEqual(build.version_type, BRANCH)
self.assertEqual(build.commit, 'a1b2c3')

def test_can_rebuild_with_regular_version(self):
build = get(
Build,
project=self.project,
version=self.version,
_config={'version': 1},
)

self.assertFalse(build.can_rebuild)

def test_can_rebuild_with_external_active_version(self):
# Turn the build version to EXTERNAL type.
self.version.type = EXTERNAL
self.version.active = True
self.version.save()

external_build = get(
Build,
project=self.project,
version=self.version,
_config={'version': 1},
)

self.assertTrue(external_build.can_rebuild)

def test_can_rebuild_with_external_inactive_version(self):
# Turn the build version to EXTERNAL type.
self.version.type = EXTERNAL
self.version.active = False
self.version.save()

external_build = get(
Build,
project=self.project,
version=self.version,
_config={'version': 1},
)

self.assertFalse(external_build.can_rebuild)

def test_can_rebuild_with_old_build(self):
# Turn the build version to EXTERNAL type.
self.version.type = EXTERNAL
self.version.active = True
self.version.save()

old_external_build = get(
Build,
project=self.project,
version=self.version,
_config={'version': 1},
)

latest_external_build = get(
Build,
project=self.project,
version=self.version,
_config={'version': 1},
)

self.assertFalse(old_external_build.can_rebuild)
self.assertTrue(latest_external_build.can_rebuild)


@mock.patch('readthedocs.projects.tasks.update_docs_task')
class DeDuplicateBuildTests(TestCase):
Expand Down
48 changes: 46 additions & 2 deletions readthedocs/sso/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
"""Admin interface for SSO models."""
import logging

from django.contrib import admin
from django.contrib import admin, messages

from readthedocs.core.permissions import AdminPermission
from readthedocs.oauth.tasks import sync_remote_repositories

from .models import SSODomain, SSOIntegration

admin.site.register(SSOIntegration)

log = logging.getLogger(__name__)


class SSOIntegrationAdmin(admin.ModelAdmin):

"""Admin configuration for SSOIntegration."""

list_display = ('organization', 'provider')
search_fields = ('organization__slug', 'organization__name', 'domains__domain')
list_filter = ('provider',)

actions = [
'resync_sso_user_accounts',
]

def resync_sso_user_accounts(self, request, queryset): # pylint: disable=no-self-use
users_count = 0
organizations_count = queryset.count()

for ssointegration in queryset.select_related('organization'):
members = AdminPermission.members(ssointegration.organization)
log.info(
'Triggering SSO re-sync for organization. organization=%s users=%s',
ssointegration.organization.slug,
members.count(),
)
users_count += members.count()
for user in members:
sync_remote_repositories.delay(user.pk)

messages.add_message(
request,
messages.INFO,
f'Triggered resync for {organizations_count} organizations and {users_count} users.'
)

resync_sso_user_accounts.short_description = 'Re-sync all SSO user accounts'


admin.site.register(SSOIntegration, SSOIntegrationAdmin)
admin.site.register(SSODomain)
5 changes: 2 additions & 3 deletions readthedocs/templates/builds/build_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,9 @@
</li>
</div>


{# Show rebuild button only if the version is external and it's the latest build for this version #}
{# Show rebuild button only if the version is external, active and it's the latest build for this version #}
{# see https://github.com/readthedocs/readthedocs.org/pull/6995#issuecomment-852918969 #}
{% if request.user|is_admin:project and build.version.type == "external" and is_latest_build %}
{% if request.user|is_admin:project and build.can_rebuild %}
<div data-bind="visible: finished()">
<form method="post" name="rebuild_commit" action="{% url "builds_project_list" project.slug %}">
{% csrf_token %}
Expand Down