GeezerVison is showing github.com — feat: add support for deployment approval endpoint · mkarthiatgithub/python-gitlab@9c9eeb9
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: docs/gl_objects/deployments.rst +12Lines changed: 12 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -40,6 +40,18 @@ Update a deployment:: | 40 | 40 | deployment.status = "failed" | 41 | 41 | deployment.save() | 42 | 42 |
| | 43 | +Approve a deployment:: | | 44 | + | | 45 | + deployment = project.deployments.get(42) | | 46 | + # `status` must be either "approved" or "rejected". | | 47 | + deployment.approval(status="approved") | | 48 | + | | 49 | +Reject a deployment:: | | 50 | + | | 51 | + deployment = project.deployments.get(42) | | 52 | + # Using the optional `comment` and `represented_as` arguments | | 53 | + deployment.approval(status="rejected", comment="Fails CI", represented_as="security") | | 54 | + | 43 | 55 | Merge requests associated with a deployment | 44 | 56 | =========================================== | 45 | 57 |
| |
Collapse file Copy file name to clipboardExpand all lines: gitlab/exceptions.py +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -301,6 +301,10 @@ class GitlabUserRejectError(GitlabOperationError): | 301 | 301 | pass | 302 | 302 |
| 303 | 303 |
| | 304 | +class GitlabDeploymentApprovalError(GitlabOperationError): | | 305 | + pass | | 306 | + | | 307 | + | 304 | 308 | # For an explanation of how these type-hints work see: | 305 | 309 | # https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators | 306 | 310 | # | |
Collapse file Copy file name to clipboardExpand all lines: gitlab/v4/objects/deployments.py +51-1Lines changed: 51 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -from typing import Any, cast, Union | | 1 | +""" | | 2 | +GitLab API: | | 3 | +https://docs.gitlab.com/ee/api/deployments.html | | 4 | +""" | | 5 | +from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union | 2 | 6 |
| | 7 | +from gitlab import cli | | 8 | +from gitlab import exceptions as exc | 3 | 9 | from gitlab.base import RESTManager, RESTObject | 4 | 10 | from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin | 5 | 11 | from gitlab.types import RequiredOptional | | 15 | 21 | class ProjectDeployment(SaveMixin, RESTObject): | 16 | 22 | mergerequests: ProjectDeploymentMergeRequestManager | 17 | 23 |
| | 24 | + @cli.register_custom_action( | | 25 | + "ProjectDeployment", | | 26 | + mandatory=("status",), | | 27 | + optional=("comment", "represented_as"), | | 28 | + ) | | 29 | + @exc.on_http_error(exc.GitlabDeploymentApprovalError) | | 30 | + def approval( | | 31 | + self, | | 32 | + status: str, | | 33 | + comment: Optional[str] = None, | | 34 | + represented_as: Optional[str] = None, | | 35 | + **kwargs: Any, | | 36 | + ) -> Dict[str, Any]: | | 37 | + """Approve or reject a blocked deployment. | | 38 | +
| | 39 | + Args: | | 40 | + status: Either "approved" or "rejected" | | 41 | + comment: A comment to go with the approval | | 42 | + represented_as: The name of the User/Group/Role to use for the | | 43 | + approval, when the user belongs to multiple | | 44 | + approval rules. | | 45 | + **kwargs: Extra options to send to the server (e.g. sudo) | | 46 | +
| | 47 | + Raises: | | 48 | + GitlabAuthenticationError: If authentication is not correct | | 49 | + GitlabMRApprovalError: If the approval failed | | 50 | +
| | 51 | + Returns: | | 52 | + A dict containing the result. | | 53 | +
| | 54 | + https://docs.gitlab.com/ee/api/deployments.html#approve-or-reject-a-blocked-deployment | | 55 | + """ | | 56 | + path = f"{self.manager.path}/{self.encoded_id}/approval" | | 57 | + data = {"status": status} | | 58 | + if comment is not None: | | 59 | + data["comment"] = comment | | 60 | + if represented_as is not None: | | 61 | + data["represented_as"] = represented_as | | 62 | + | | 63 | + server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) | | 64 | + if TYPE_CHECKING: | | 65 | + assert isinstance(server_data, dict) | | 66 | + return server_data | | 67 | + | 18 | 68 |
| 19 | 69 | class ProjectDeploymentManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager): | 20 | 70 | _path = "/projects/{project_id}/deployments" | |
Collapse file Copy file name to clipboardExpand all lines: tests/unit/objects/test_deployments.py +132-2Lines changed: 132 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 6 | 6 |
| 7 | 7 |
| 8 | 8 | @pytest.fixture | 9 | | -def resp_deployment(): | | 9 | +def resp_deployment_get(): | | 10 | + with responses.RequestsMock() as rsps: | | 11 | + rsps.add( | | 12 | + method=responses.GET, | | 13 | + url="http://localhost/api/v4/projects/1/deployments/42", | | 14 | + json=response_get_content, | | 15 | + content_type="application/json", | | 16 | + status=200, | | 17 | + ) | | 18 | + yield rsps | | 19 | + | | 20 | + | | 21 | +@pytest.fixture | | 22 | +def deployment(project): | | 23 | + return project.deployments.get(42, lazy=True) | | 24 | + | | 25 | + | | 26 | +@pytest.fixture | | 27 | +def resp_deployment_create(): | 10 | 28 | content = {"id": 42, "status": "success", "ref": "main"} | 11 | 29 |
| 12 | 30 | with responses.RequestsMock() as rsps: | @@ -31,7 +49,42 @@ def resp_deployment(): | 31 | 49 | yield rsps | 32 | 50 |
| 33 | 51 |
| 34 | | -def test_deployment(project, resp_deployment): | | 52 | +@pytest.fixture | | 53 | +def resp_deployment_approval(): | | 54 | + content = { | | 55 | + "user": { | | 56 | + "id": 100, | | 57 | + "username": "security-user-1", | | 58 | + "name": "security user-1", | | 59 | + "state": "active", | | 60 | + "avatar_url": "https://www.gravatar.com/avatar/e130fcd3a1681f41a3de69d10841afa9?s=80&d=identicon", | | 61 | + "web_url": "http://localhost:3000/security-user-1", | | 62 | + }, | | 63 | + "status": "approved", | | 64 | + "created_at": "2022-02-24T20:22:30.097Z", | | 65 | + "comment": "Looks good to me", | | 66 | + } | | 67 | + | | 68 | + with responses.RequestsMock() as rsps: | | 69 | + rsps.add( | | 70 | + method=responses.POST, | | 71 | + url="http://localhost/api/v4/projects/1/deployments/42/approval", | | 72 | + json=content, | | 73 | + content_type="application/json", | | 74 | + status=200, | | 75 | + ) | | 76 | + yield rsps | | 77 | + | | 78 | + | | 79 | +def test_deployment_get(project, resp_deployment_get): | | 80 | + deployment = project.deployments.get(42) | | 81 | + assert deployment.id == 42 | | 82 | + assert deployment.iid == 2 | | 83 | + assert deployment.status == "success" | | 84 | + assert deployment.ref == "main" | | 85 | + | | 86 | + | | 87 | +def test_deployment_create(project, resp_deployment_create): | 35 | 88 | deployment = project.deployments.create( | 36 | 89 | { | 37 | 90 | "environment": "Test", | @@ -48,3 +101,80 @@ def test_deployment(project, resp_deployment): | 48 | 101 | deployment.status = "failed" | 49 | 102 | deployment.save() | 50 | 103 | assert deployment.status == "failed" | | 104 | + | | 105 | + | | 106 | +def test_deployment_approval(deployment, resp_deployment_approval) -> None: | | 107 | + result = deployment.approval(status="approved") | | 108 | + assert result["status"] == "approved" | | 109 | + assert result["comment"] == "Looks good to me" | | 110 | + | | 111 | + | | 112 | +response_get_content = { | | 113 | + "id": 42, | | 114 | + "iid": 2, | | 115 | + "ref": "main", | | 116 | + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", | | 117 | + "created_at": "2016-08-11T11:32:35.444Z", | | 118 | + "updated_at": "2016-08-11T11:34:01.123Z", | | 119 | + "status": "success", | | 120 | + "user": { | | 121 | + "name": "Administrator", | | 122 | + "username": "root", | | 123 | + "id": 1, | | 124 | + "state": "active", | | 125 | + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", | | 126 | + "web_url": "http://localhost:3000/root", | | 127 | + }, | | 128 | + "environment": { | | 129 | + "id": 9, | | 130 | + "name": "production", | | 131 | + "external_url": "https://about.gitlab.com", | | 132 | + }, | | 133 | + "deployable": { | | 134 | + "id": 664, | | 135 | + "status": "success", | | 136 | + "stage": "deploy", | | 137 | + "name": "deploy", | | 138 | + "ref": "main", | | 139 | + "tag": False, | | 140 | + "coverage": None, | | 141 | + "created_at": "2016-08-11T11:32:24.456Z", | | 142 | + "started_at": None, | | 143 | + "finished_at": "2016-08-11T11:32:35.145Z", | | 144 | + "user": { | | 145 | + "id": 1, | | 146 | + "name": "Administrator", | | 147 | + "username": "root", | | 148 | + "state": "active", | | 149 | + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", | | 150 | + "web_url": "http://gitlab.dev/root", | | 151 | + "created_at": "2015-12-21T13:14:24.077Z", | | 152 | + "bio": None, | | 153 | + "location": None, | | 154 | + "skype": "", | | 155 | + "linkedin": "", | | 156 | + "twitter": "", | | 157 | + "website_url": "", | | 158 | + "organization": "", | | 159 | + }, | | 160 | + "commit": { | | 161 | + "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", | | 162 | + "short_id": "a91957a8", | | 163 | + "title": "Merge branch 'rename-readme' into 'main'\r", | | 164 | + "author_name": "Administrator", | | 165 | + | | 166 | + "created_at": "2016-08-11T13:28:26.000+02:00", | | 167 | + "message": "Merge branch 'rename-readme' into 'main'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2", | | 168 | + }, | | 169 | + "pipeline": { | | 170 | + "created_at": "2016-08-11T07:43:52.143Z", | | 171 | + "id": 42, | | 172 | + "ref": "main", | | 173 | + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", | | 174 | + "status": "success", | | 175 | + "updated_at": "2016-08-11T07:43:52.143Z", | | 176 | + "web_url": "http://gitlab.dev/root/project/pipelines/5", | | 177 | + }, | | 178 | + "runner": None, | | 179 | + }, | | 180 | +} |
|