Skip to content

Commit 12ec907

Browse files
authored
Merge pull request #6076 from saadmk11/gitlab-build-status
GitLab Build Status Reporting for PR Builder
2 parents 5c03027 + b4e7834 commit 12ec907

File tree

5 files changed

+227
-10
lines changed

5 files changed

+227
-10
lines changed

readthedocs/builds/constants.py

+8
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,26 @@
7272
GITHUB_BUILD_STATUS_PENDING = 'pending'
7373
GITHUB_BUILD_STATUS_SUCCESS = 'success'
7474

75+
# GitLab Build Statuses
76+
GITLAB_BUILD_STATUS_FAILURE = 'failed'
77+
GITLAB_BUILD_STATUS_PENDING = 'pending'
78+
GITLAB_BUILD_STATUS_SUCCESS = 'success'
79+
7580
# Used to select correct Build status and description to be sent to each service API
7681
SELECT_BUILD_STATUS = {
7782
BUILD_STATUS_FAILURE: {
7883
'github': GITHUB_BUILD_STATUS_FAILURE,
84+
'gitlab': GITLAB_BUILD_STATUS_FAILURE,
7985
'description': 'Read the Docs build failed!',
8086
},
8187
BUILD_STATUS_PENDING: {
8288
'github': GITHUB_BUILD_STATUS_PENDING,
89+
'gitlab': GITLAB_BUILD_STATUS_PENDING,
8390
'description': 'Read the Docs build is in progress!',
8491
},
8592
BUILD_STATUS_SUCCESS: {
8693
'github': GITHUB_BUILD_STATUS_SUCCESS,
94+
'gitlab': GITLAB_BUILD_STATUS_SUCCESS,
8795
'description': 'Read the Docs build succeeded!',
8896
},
8997
}

readthedocs/oauth/services/gitlab.py

+92
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from django.urls import reverse
1010
from requests.exceptions import RequestException
1111

12+
from readthedocs.builds.constants import (
13+
BUILD_STATUS_SUCCESS,
14+
RTD_BUILD_STATUS_API_NAME,
15+
SELECT_BUILD_STATUS,
16+
)
1217
from readthedocs.builds.utils import get_gitlab_username_repo
1318
from readthedocs.integrations.models import Integration
1419
from readthedocs.projects.models import Project
@@ -399,3 +404,90 @@ def update_webhook(self, project, integration):
399404
debug_data = resp.content
400405
log.debug('GitLab webhook update failure response: %s', debug_data)
401406
return (False, resp)
407+
408+
def send_build_status(self, build, commit, state):
409+
"""
410+
Create GitLab commit status for project.
411+
412+
:param build: Build to set up commit status for
413+
:type build: Build
414+
:param state: build state failure, pending, or success.
415+
:type state: str
416+
:param commit: commit sha of the pull request
417+
:type commit: str
418+
:returns: boolean based on commit status creation was successful or not.
419+
:rtype: Bool
420+
"""
421+
session = self.get_session()
422+
project = build.project
423+
424+
repo_id = self._get_repo_id(project)
425+
426+
if repo_id is None:
427+
return (False, None)
428+
429+
# select the correct state and description.
430+
gitlab_build_state = SELECT_BUILD_STATUS[state]['gitlab']
431+
description = SELECT_BUILD_STATUS[state]['description']
432+
433+
target_url = build.get_full_url()
434+
435+
if state == BUILD_STATUS_SUCCESS:
436+
target_url = build.version.get_absolute_url()
437+
438+
data = {
439+
'state': gitlab_build_state,
440+
'target_url': target_url,
441+
'description': description,
442+
'context': RTD_BUILD_STATUS_API_NAME
443+
}
444+
url = self.adapter.provider_base_url
445+
446+
resp = None
447+
448+
try:
449+
resp = session.post(
450+
f'{url}/api/v4/projects/{repo_id}/statuses/{commit}',
451+
data=json.dumps(data),
452+
headers={'content-type': 'application/json'},
453+
)
454+
455+
if resp.status_code == 201:
456+
log.info(
457+
"GitLab commit status created for project: %s, commit status: %s",
458+
project,
459+
gitlab_build_state,
460+
)
461+
return True
462+
463+
if resp.status_code in [401, 403, 404]:
464+
log.info(
465+
'GitLab project does not exist or user does not have '
466+
'permissions: project=%s',
467+
project,
468+
)
469+
return False
470+
471+
return False
472+
473+
# Catch exceptions with request or deserializing JSON
474+
except (RequestException, ValueError):
475+
log.exception(
476+
'GitLab commit status creation failed for project: %s',
477+
project,
478+
)
479+
# Response data should always be JSON, still try to log if not
480+
# though
481+
if resp is not None:
482+
try:
483+
debug_data = resp.json()
484+
except ValueError:
485+
debug_data = resp.content
486+
else:
487+
debug_data = resp
488+
489+
log.debug(
490+
'GitLab commit status creation failure response: %s',
491+
debug_data,
492+
)
493+
return False

readthedocs/projects/tasks.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@
6464
from readthedocs.doc_builder.python_environments import Conda, Virtualenv
6565
from readthedocs.oauth.models import RemoteRepository
6666
from readthedocs.oauth.notifications import GitBuildStatusFailureNotification
67-
from readthedocs.oauth.services.github import GitHubService
68-
from readthedocs.projects.constants import GITHUB_BRAND
67+
from readthedocs.projects.constants import GITHUB_BRAND, GITLAB_BRAND
6968
from readthedocs.projects.models import APIProject, Feature
7069
from readthedocs.search.utils import index_new_files, remove_indexed_files
7170
from readthedocs.sphinx_domains.models import SphinxDomain
@@ -1936,7 +1935,7 @@ def send_build_status(build_pk, commit, status):
19361935
build = Build.objects.get(pk=build_pk)
19371936
provider_name = build.project.git_provider_name
19381937

1939-
if provider_name == GITHUB_BRAND:
1938+
if provider_name in [GITHUB_BRAND, GITLAB_BRAND]:
19401939
# get the service class for the project e.g: GitHubService.
19411940
service_class = build.project.git_service_class()
19421941
try:
@@ -1984,7 +1983,7 @@ def send_build_status(build_pk, commit, status):
19841983

19851984
return False
19861985

1987-
# TODO: Send build status for other providers.
1986+
# TODO: Send build status for BitBucket.
19881987

19891988

19901989
def send_external_build_status(version_type, build_pk, commit, status):

readthedocs/rtd_tests/tests/test_celery.py

+63-6
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,8 @@ def test_fileify_logging_when_wrong_version_pk(self, mock_logger):
334334
tasks.fileify(version_pk=345343, commit=None, build=1)
335335
mock_logger.warning.assert_called_with("Version not found for given kwargs. {'pk': 345343}")
336336

337-
@patch('readthedocs.projects.tasks.GitHubService.send_build_status')
338-
def test_send_build_status_task_with_remote_repo(self, send_build_status):
337+
@patch('readthedocs.oauth.services.github.GitHubService.send_build_status')
338+
def test_send_build_status_with_remote_repo_github(self, send_build_status):
339339
self.project.repo = 'https://github.com/test/test/'
340340
self.project.save()
341341

@@ -356,8 +356,8 @@ def test_send_build_status_task_with_remote_repo(self, send_build_status):
356356
)
357357
self.assertEqual(Message.objects.filter(user=self.eric).count(), 0)
358358

359-
@patch('readthedocs.projects.tasks.GitHubService.send_build_status')
360-
def test_send_build_status_task_with_social_account(self, send_build_status):
359+
@patch('readthedocs.oauth.services.github.GitHubService.send_build_status')
360+
def test_send_build_status_with_social_account_github(self, send_build_status):
361361
social_account = get(SocialAccount, user=self.eric, provider='github')
362362

363363
self.project.repo = 'https://github.com/test/test/'
@@ -376,8 +376,8 @@ def test_send_build_status_task_with_social_account(self, send_build_status):
376376
)
377377
self.assertEqual(Message.objects.filter(user=self.eric).count(), 0)
378378

379-
@patch('readthedocs.projects.tasks.GitHubService.send_build_status')
380-
def test_send_build_status_task_without_remote_repo_or_social_account(self, send_build_status):
379+
@patch('readthedocs.oauth.services.github.GitHubService.send_build_status')
380+
def test_send_build_status_no_remote_repo_or_social_account_github(self, send_build_status):
381381
self.project.repo = 'https://github.com/test/test/'
382382
self.project.save()
383383
external_version = get(Version, project=self.project, type=EXTERNAL)
@@ -390,3 +390,60 @@ def test_send_build_status_task_without_remote_repo_or_social_account(self, send
390390

391391
send_build_status.assert_not_called()
392392
self.assertEqual(Message.objects.filter(user=self.eric).count(), 1)
393+
394+
@patch('readthedocs.oauth.services.gitlab.GitLabService.send_build_status')
395+
def test_send_build_status_with_remote_repo_gitlab(self, send_build_status):
396+
self.project.repo = 'https://gitlab.com/test/test/'
397+
self.project.save()
398+
399+
social_account = get(SocialAccount, provider='gitlab')
400+
remote_repo = get(RemoteRepository, account=social_account, project=self.project)
401+
remote_repo.users.add(self.eric)
402+
403+
external_version = get(Version, project=self.project, type=EXTERNAL)
404+
external_build = get(
405+
Build, project=self.project, version=external_version
406+
)
407+
tasks.send_build_status(
408+
external_build.id, external_build.commit, BUILD_STATUS_SUCCESS
409+
)
410+
411+
send_build_status.assert_called_once_with(
412+
external_build, external_build.commit, BUILD_STATUS_SUCCESS
413+
)
414+
self.assertEqual(Message.objects.filter(user=self.eric).count(), 0)
415+
416+
@patch('readthedocs.oauth.services.gitlab.GitLabService.send_build_status')
417+
def test_send_build_status_with_social_account_gitlab(self, send_build_status):
418+
social_account = get(SocialAccount, user=self.eric, provider='gitlab')
419+
420+
self.project.repo = 'https://gitlab.com/test/test/'
421+
self.project.save()
422+
423+
external_version = get(Version, project=self.project, type=EXTERNAL)
424+
external_build = get(
425+
Build, project=self.project, version=external_version
426+
)
427+
tasks.send_build_status(
428+
external_build.id, external_build.commit, BUILD_STATUS_SUCCESS
429+
)
430+
431+
send_build_status.assert_called_once_with(
432+
external_build, external_build.commit, BUILD_STATUS_SUCCESS
433+
)
434+
self.assertEqual(Message.objects.filter(user=self.eric).count(), 0)
435+
436+
@patch('readthedocs.oauth.services.gitlab.GitLabService.send_build_status')
437+
def test_send_build_status_no_remote_repo_or_social_account_gitlab(self, send_build_status):
438+
self.project.repo = 'https://gitlab.com/test/test/'
439+
self.project.save()
440+
external_version = get(Version, project=self.project, type=EXTERNAL)
441+
external_build = get(
442+
Build, project=self.project, version=external_version
443+
)
444+
tasks.send_build_status(
445+
external_build.id, external_build.commit, BUILD_STATUS_SUCCESS
446+
)
447+
448+
send_build_status.assert_not_called()
449+
self.assertEqual(Message.objects.filter(user=self.eric).count(), 1)

readthedocs/rtd_tests/tests/test_oauth.py

+61
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,10 @@ def setUp(self):
491491
self.org = RemoteOrganization.objects.create(slug='testorga', json='')
492492
self.privacy = self.project.version_privacy_level
493493
self.service = GitLabService(user=self.user, account=None)
494+
self.external_version = get(Version, project=self.project, type=EXTERNAL)
495+
self.external_build = get(
496+
Build, project=self.project, version=self.external_version
497+
)
494498

495499
def get_private_repo_data(self):
496500
"""Manipulate repo response data to get private repo data."""
@@ -568,3 +572,60 @@ def test_setup_webhook(self):
568572
success, response = self.service.setup_webhook(self.project)
569573
self.assertFalse(success)
570574
self.assertIsNone(response)
575+
576+
@mock.patch('readthedocs.oauth.services.gitlab.log')
577+
@mock.patch('readthedocs.oauth.services.gitlab.GitLabService.get_session')
578+
@mock.patch('readthedocs.oauth.services.gitlab.GitLabService._get_repo_id')
579+
def test_send_build_status_successful(self, repo_id, session, mock_logger):
580+
session().post.return_value.status_code = 201
581+
repo_id().return_value = '9999'
582+
583+
success = self.service.send_build_status(
584+
self.external_build,
585+
self.external_build.commit,
586+
BUILD_STATUS_SUCCESS
587+
)
588+
589+
self.assertTrue(success)
590+
mock_logger.info.assert_called_with(
591+
"GitLab commit status created for project: %s, commit status: %s",
592+
self.project,
593+
BUILD_STATUS_SUCCESS
594+
)
595+
596+
@mock.patch('readthedocs.oauth.services.gitlab.log')
597+
@mock.patch('readthedocs.oauth.services.gitlab.GitLabService.get_session')
598+
@mock.patch('readthedocs.oauth.services.gitlab.GitLabService._get_repo_id')
599+
def test_send_build_status_404_error(self, repo_id, session, mock_logger):
600+
session().post.return_value.status_code = 404
601+
repo_id().return_value = '9999'
602+
603+
success = self.service.send_build_status(
604+
self.external_build,
605+
self.external_build.commit,
606+
BUILD_STATUS_SUCCESS
607+
)
608+
609+
self.assertFalse(success)
610+
mock_logger.info.assert_called_with(
611+
'GitLab project does not exist or user does not have '
612+
'permissions: project=%s',
613+
self.project
614+
)
615+
616+
@mock.patch('readthedocs.oauth.services.gitlab.log')
617+
@mock.patch('readthedocs.oauth.services.gitlab.GitLabService.get_session')
618+
@mock.patch('readthedocs.oauth.services.gitlab.GitLabService._get_repo_id')
619+
def test_send_build_status_value_error(self, repo_id, session, mock_logger):
620+
session().post.side_effect = ValueError
621+
repo_id().return_value = '9999'
622+
623+
success = self.service.send_build_status(
624+
self.external_build, self.external_build.commit, BUILD_STATUS_SUCCESS
625+
)
626+
627+
self.assertFalse(success)
628+
mock_logger.exception.assert_called_with(
629+
'GitLab commit status creation failed for project: %s',
630+
self.project,
631+
)

0 commit comments

Comments
 (0)