Skip to content

fix: adds missing status check methods for merge requests #3128

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 21 additions & 6 deletions docs/gl_objects/status_checks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ Examples

List external status checks for a project::

status_checks = project.external_status_checks.list(get_all=True)
external_status_checks = project.external_status_checks.list()

Create an external status check with shared secret::

status_checks = project.external_status_checks.create({
external_status_checks = project.external_status_checks.create({
"name": "mr_blocker",
"external_url": "https://example.com/mr-status-check",
"shared_secret": "secret-string"
Expand All @@ -38,7 +38,7 @@ Create an external status check with shared secret for protected branches::

protected_branch = project.protectedbranches.get('main')

status_check = project.external_status_checks.create({
external_status_check = project.external_status_checks.create({
"name": "mr_blocker",
"external_url": "https://example.com/mr-status-check",
"shared_secret": "secret-string",
Expand All @@ -48,10 +48,25 @@ Create an external status check with shared secret for protected branches::

Update an external status check::

status_check.external_url = "https://example.com/mr-blocker"
status_check.save()
external_status_check.external_url = "https://example.com/mr-blocker"
external_status_check.save()

Delete an external status check::

status_check.delete(status_check_id)
external_status_check.delete(externa_status_check_id)
Copy link
Preview

Copilot AI Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a typo in the variable name 'externa_status_check_id'; it should be 'external_status_check_id'.

Suggested change
external_status_check.delete(externa_status_check_id)
external_status_check.delete(external_status_check_id)

Copilot uses AI. Check for mistakes.


List external status check for a project merge request::

merge_request = project.mergerequests.get(1)

merge_request.external_status_checks.list()

Set external status check for a project merge request::

merge_request = project.mergerequests.get(1)

merge_request.external_status_check_response.update({
"external_status_check_id": external_status_check_id,
"status": "passed",
"sha": merge_request.sha
})
8 changes: 6 additions & 2 deletions gitlab/v4/objects/merge_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@
from .notes import ProjectMergeRequestNoteManager # noqa: F401
from .pipelines import ProjectMergeRequestPipelineManager # noqa: F401
from .reviewers import ProjectMergeRequestReviewerDetailManager
from .status_checks import ProjectMergeRequestStatusCheckManager
from .status_checks import (
ProjectMergeRequestStatusCheckManager,
ProjectMergeRequestStatusCheckResponseManager,
)

__all__ = [
"MergeRequest",
Expand Down Expand Up @@ -170,7 +173,8 @@ class ProjectMergeRequest(
resourcemilestoneevents: ProjectMergeRequestResourceMilestoneEventManager
resourcestateevents: ProjectMergeRequestResourceStateEventManager
reviewer_details: ProjectMergeRequestReviewerDetailManager
status_checks: ProjectMergeRequestStatusCheckManager
external_status_checks: ProjectMergeRequestStatusCheckManager
external_status_check_response: ProjectMergeRequestStatusCheckResponseManager

@cli.register_custom_action(cls_names="ProjectMergeRequest")
@exc.on_http_error(exc.GitlabMROnBuildSuccessError)
Expand Down
41 changes: 35 additions & 6 deletions gitlab/v4/objects/status_checks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from gitlab.base import RESTObject
from typing import Any, Dict, Optional

from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
CreateMixin,
DeleteMixin,
Expand Down Expand Up @@ -27,6 +29,7 @@ class ProjectExternalStatusCheckManager(
CreateMixin[ProjectExternalStatusCheck],
UpdateMixin[ProjectExternalStatusCheck],
DeleteMixin[ProjectExternalStatusCheck],
RESTManager[ProjectExternalStatusCheck],
):
_path = "/projects/{project_id}/external_status_checks"
_obj_cls = ProjectExternalStatusCheck
Expand All @@ -41,15 +44,41 @@ class ProjectExternalStatusCheckManager(
_types = {"protected_branch_ids": ArrayAttribute}


class ProjectMergeRequestStatusCheck(SaveMixin, RESTObject):
class ProjectMergeRequestStatusCheckResponse(SaveMixin, RESTObject):
pass


class ProjectMergeRequestStatusCheckManager(ListMixin[ProjectMergeRequestStatusCheck]):
_path = "/projects/{project_id}/merge_requests/{merge_request_iid}/status_checks"
_obj_cls = ProjectMergeRequestStatusCheck
_from_parent_attrs = {"project_id": "project_id", "merge_request_iid": "iid"}
class ProjectMergeRequestStatusCheckResponseManager(
UpdateMixin[ProjectMergeRequestStatusCheckResponse],
RESTManager[ProjectMergeRequestStatusCheckResponse],
):
_path = "/projects/{project_id}/merge_requests/{mr_iid}/status_check_responses"
_obj_cls = ProjectMergeRequestStatusCheckResponse
_from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
_update_attrs = RequiredOptional(
required=("sha", "external_status_check_id", "status")
)
_update_method = UpdateMethod.POST

def update( # type: ignore[override]
self, new_data: Optional[Dict[str, Any]] = None, **kwargs: Any
) -> Dict[str, Any]:
"""Update a Label on the server.

Args:
**kwargs: Extra options to send to the server (e.g. sudo)
"""
return super().update(id=None, new_data=new_data, **kwargs)


class ProjectMergeRequestStatusCheck(RESTObject):
pass


class ProjectMergeRequestStatusCheckManager(
ListMixin[ProjectMergeRequestStatusCheck],
RESTManager[ProjectMergeRequestStatusCheck],
):
_path = "/projects/{project_id}/merge_requests/{mr_iid}/status_checks"
_obj_cls = ProjectMergeRequestStatusCheck
_from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
43 changes: 43 additions & 0 deletions tests/functional/api/test_merge_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,46 @@ def test_merge_request_merge_ref_should_fail(project, merge_request) -> None:
with pytest.raises(gitlab.exceptions.GitlabGetError):
response = merge_request.merge_ref()
assert "commit_id" not in response


@pytest.mark.gitlab_premium
def test_merge_request_external_status_check_set_status(project, merge_request):
project.external_status_checks.create(
{
"name": "external_status_check",
"external_url": "https://example.com/mr-blocker",
}
)

mr_external_status_checks = merge_request.external_status_checks.list()
assert len(mr_external_status_checks) == 1

expected_external_status_check = None

for mr_external_status_check in mr_external_status_checks:
if mr_external_status_check.name == "external_status_check":
expected_external_status_check = mr_external_status_check
break

assert expected_external_status_check is not None

# set the external status check value to 'passed'
merge_request.external_status_check_response.update(
{
"external_status_check_id": expected_external_status_check.id,
"status": "passed",
"sha": merge_request.sha,
}
)

time.sleep(2)

# Check the status again to validate the passed status
mr_status_checks = merge_request.external_status_checks.list()

for mr_status_check in mr_status_checks:
if mr_status_check.name == "external_status_check":
expected_status_check = mr_status_check
break

assert expected_status_check.status == "passed"
118 changes: 118 additions & 0 deletions tests/unit/objects/test_status_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@
import pytest
import responses

mr_content = {
"id": 1,
"iid": 1,
"project_id": 1,
"title": "test1",
"description": "fixed login page css paddings",
"state": "merged",
"sha": "somerandomstring",
"merged_by": {
"id": 87854,
"name": "Douwe Maan",
"username": "DouweM",
"state": "active",
"avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
"web_url": "https://gitlab.com/DouweM",
},
"reviewers": [
{
"id": 2,
"name": "Sam Bauch",
"username": "kenyatta_oconnell",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/956c92487c6f6f7616b536927e22c9a0?s=80&d=identicon",
"web_url": "http://gitlab.example.com//kenyatta_oconnell",
}
],
}

external_status_checks_content = [
{
"id": 2,
"name": "Service 2",
"external_url": "https://gitlab.example.com/test-endpoint-2",
"status": "pending",
},
{
"id": 1,
"name": "Service 1",
"external_url": "https://gitlab.example.com/test-endpoint-1",
"status": "pending",
},
]


@pytest.fixture
def external_status_check():
Expand Down Expand Up @@ -104,6 +147,54 @@ def resp_delete_external_status_checks():
content_type="application/json",
status=200,
)

yield rsps


@pytest.fixture
def resp_list_merge_requests_status_checks():
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/merge_requests/1",
json=mr_content,
content_type="application/json",
status=200,
)
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/merge_requests/1/status_checks",
json=external_status_checks_content,
content_type="application/json",
status=200,
)
yield rsps


@pytest.fixture
def resp_list_merge_requests_status_checks_set_value():
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/merge_requests/1",
json=mr_content,
content_type="application/json",
status=200,
)
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/merge_requests/1/status_checks",
json=external_status_checks_content,
content_type="application/json",
status=200,
)
rsps.add(
method=responses.POST,
url="http://localhost/api/v4/projects/1/merge_requests/1/status_check_responses",
json={"status": "passed"},
content_type="application/json",
status=200,
)
yield rsps


Expand All @@ -125,3 +216,30 @@ def test_delete_external_status_checks(gl, resp_delete_external_status_checks):
gl.projects.get(1, lazy=True).external_status_checks.delete(1)
status_checks = gl.projects.get(1, lazy=True).external_status_checks.list()
assert len(status_checks) == 0


def test_get_merge_request_external_status_checks(
gl, resp_list_merge_requests_status_checks
):
merge_request = gl.projects.get(1, lazy=True).mergerequests.get(1)
external_status_checks = merge_request.external_status_checks.list()
assert len(external_status_checks) == 2


def test_get_merge_request_external_status_checks_set_value(
gl, resp_list_merge_requests_status_checks_set_value
):
merge_request = gl.projects.get(1, lazy=True).mergerequests.get(1)
external_status_checks = merge_request.external_status_checks.list()

assert len(external_status_checks) == 2
for external_status_check in external_status_checks:
if external_status_check.name == "Service 2":
response = merge_request.external_status_check_response.update(
{
"external_status_check_id": external_status_check.id,
"status": "passed",
"sha": merge_request.sha,
}
)
response["status"] == "passed"
Copy link
Preview

Copilot AI Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status update check is currently a no-op; use an assertion (e.g., assert response["status"] == "passed") to properly validate the response.

Suggested change
response["status"] == "passed"
assert response["status"] == "passed"

Copilot uses AI. Check for mistakes.