Skip to content

Commit

Permalink
feat: add support for deployment approval endpoint
Browse files Browse the repository at this point in the history
Add support for the deployment approval endpoint[1]

[1] https://docs.gitlab.com/ee/api/deployments.html#approve-or-reject-a-blocked-deployment
Closes: #2253
  • Loading branch information
JohnVillalovos committed Aug 25, 2022
1 parent e095735 commit 9c9eeb9
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 3 deletions.
12 changes: 12 additions & 0 deletions docs/gl_objects/deployments.rst
Expand Up @@ -40,6 +40,18 @@ Update a deployment::
deployment.status = "failed"
deployment.save()

Approve a deployment::

deployment = project.deployments.get(42)
# `status` must be either "approved" or "rejected".
deployment.approval(status="approved")

Reject a deployment::

deployment = project.deployments.get(42)
# Using the optional `comment` and `represented_as` arguments
deployment.approval(status="rejected", comment="Fails CI", represented_as="security")

Merge requests associated with a deployment
===========================================

Expand Down
4 changes: 4 additions & 0 deletions gitlab/exceptions.py
Expand Up @@ -301,6 +301,10 @@ class GitlabUserRejectError(GitlabOperationError):
pass


class GitlabDeploymentApprovalError(GitlabOperationError):
pass


# For an explanation of how these type-hints work see:
# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
#
Expand Down
52 changes: 51 additions & 1 deletion gitlab/v4/objects/deployments.py
@@ -1,5 +1,11 @@
from typing import Any, cast, Union
"""
GitLab API:
https://docs.gitlab.com/ee/api/deployments.html
"""
from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union

from gitlab import cli
from gitlab import exceptions as exc
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin
from gitlab.types import RequiredOptional
Expand All @@ -15,6 +21,50 @@
class ProjectDeployment(SaveMixin, RESTObject):
mergerequests: ProjectDeploymentMergeRequestManager

@cli.register_custom_action(
"ProjectDeployment",
mandatory=("status",),
optional=("comment", "represented_as"),
)
@exc.on_http_error(exc.GitlabDeploymentApprovalError)
def approval(
self,
status: str,
comment: Optional[str] = None,
represented_as: Optional[str] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""Approve or reject a blocked deployment.
Args:
status: Either "approved" or "rejected"
comment: A comment to go with the approval
represented_as: The name of the User/Group/Role to use for the
approval, when the user belongs to multiple
approval rules.
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabMRApprovalError: If the approval failed
Returns:
A dict containing the result.
https://docs.gitlab.com/ee/api/deployments.html#approve-or-reject-a-blocked-deployment
"""
path = f"{self.manager.path}/{self.encoded_id}/approval"
data = {"status": status}
if comment is not None:
data["comment"] = comment
if represented_as is not None:
data["represented_as"] = represented_as

server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
if TYPE_CHECKING:
assert isinstance(server_data, dict)
return server_data


class ProjectDeploymentManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager):
_path = "/projects/{project_id}/deployments"
Expand Down
134 changes: 132 additions & 2 deletions tests/unit/objects/test_deployments.py
Expand Up @@ -6,7 +6,25 @@


@pytest.fixture
def resp_deployment():
def resp_deployment_get():
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/deployments/42",
json=response_get_content,
content_type="application/json",
status=200,
)
yield rsps


@pytest.fixture
def deployment(project):
return project.deployments.get(42, lazy=True)


@pytest.fixture
def resp_deployment_create():
content = {"id": 42, "status": "success", "ref": "main"}

with responses.RequestsMock() as rsps:
Expand All @@ -31,7 +49,42 @@ def resp_deployment():
yield rsps


def test_deployment(project, resp_deployment):
@pytest.fixture
def resp_deployment_approval():
content = {
"user": {
"id": 100,
"username": "security-user-1",
"name": "security user-1",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e130fcd3a1681f41a3de69d10841afa9?s=80&d=identicon",
"web_url": "http://localhost:3000/security-user-1",
},
"status": "approved",
"created_at": "2022-02-24T20:22:30.097Z",
"comment": "Looks good to me",
}

with responses.RequestsMock() as rsps:
rsps.add(
method=responses.POST,
url="http://localhost/api/v4/projects/1/deployments/42/approval",
json=content,
content_type="application/json",
status=200,
)
yield rsps


def test_deployment_get(project, resp_deployment_get):
deployment = project.deployments.get(42)
assert deployment.id == 42
assert deployment.iid == 2
assert deployment.status == "success"
assert deployment.ref == "main"


def test_deployment_create(project, resp_deployment_create):
deployment = project.deployments.create(
{
"environment": "Test",
Expand All @@ -48,3 +101,80 @@ def test_deployment(project, resp_deployment):
deployment.status = "failed"
deployment.save()
assert deployment.status == "failed"


def test_deployment_approval(deployment, resp_deployment_approval) -> None:
result = deployment.approval(status="approved")
assert result["status"] == "approved"
assert result["comment"] == "Looks good to me"


response_get_content = {
"id": 42,
"iid": 2,
"ref": "main",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"updated_at": "2016-08-11T11:34:01.123Z",
"status": "success",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root",
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com",
},
"deployable": {
"id": 664,
"status": "success",
"stage": "deploy",
"name": "deploy",
"ref": "main",
"tag": False,
"coverage": None,
"created_at": "2016-08-11T11:32:24.456Z",
"started_at": None,
"finished_at": "2016-08-11T11:32:35.145Z",
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.dev/root",
"created_at": "2015-12-21T13:14:24.077Z",
"bio": None,
"location": None,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": "",
},
"commit": {
"id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"short_id": "a91957a8",
"title": "Merge branch 'rename-readme' into 'main'\r",
"author_name": "Administrator",
"author_email": "admin@example.com",
"created_at": "2016-08-11T13:28:26.000+02:00",
"message": "Merge branch 'rename-readme' into 'main'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
},
"pipeline": {
"created_at": "2016-08-11T07:43:52.143Z",
"id": 42,
"ref": "main",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"status": "success",
"updated_at": "2016-08-11T07:43:52.143Z",
"web_url": "http://gitlab.dev/root/project/pipelines/5",
},
"runner": None,
},
}

0 comments on commit 9c9eeb9

Please sign in to comment.