Repository: python-gitlab/python-gitlab

Issue:
Deprecate tag.set_release_description and remove it from docs
## Description of the problem, including code/CLI snippet
It seems that the code we use to create a release from a tag [here](https://github.com/python-gitlab/python-gitlab/blob/8753add72061ea01c508a42d16a27388b1d92677/gitlab/v4/objects/tags.py#L19) has been deprecated and removed

We should update to use the new release API

I'll try to investigate a little later tonight as it is blocking my team for some automated release tools.

## Expected Behavior
- set_release_description() create or update a release

## Actual Behavior
- 404 error

## Specifications

  - python-gitlab version: 2.8 (and 2.5)
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): gitlab.com



Discussion:
`python-gitlab` already supports the new API, which has been around for a while: https://python-gitlab.readthedocs.io/en/stable/gl_objects/releases.html

So please just use the Releases API as the official upstream docs deprecated using the tags API a long time ago.
I was thinking to update the function to make it use the new API that is indeed already implemented.  This allows previous scripts to work and keep backward compatibility with python-gitlab.

I agree it is probably a better approach to stick as much as possible to the GitLab  API and therefore remove completely the function.

Can you at least remove it from the python-gitlab doc (or mark it as deprecated)? 
  - I used it 3 months ago for a new project without knowing it was deprecated.  
  - And this is annoying 
Yes, already edited the issue title to reflect that :)

Removing it completely also makes sense as this was kind of a special/custom method that did more than just wrap the gitlab API (it idempotently created/updated the notes), something we've said we probably don't want for this library.
I also see another issue: With the Release API, I cannot anymore update the content.

There is another issue here: 
 - With the current implementation, the Release Manager is using `NoUpdateMixin`
 - This prevents editing a release note.
    - while it is [supporter](https://docs.gitlab.com/ee/api/releases/#update-a-release) on gitlab API

That's a little annoying as previous the function `set_release_description` which is a "regression" in terms of possibilities of python-gitlab.

By adding `SaveMixin` https://github.com/python-gitlab/python-gitlab/blob/8753add72061ea01c508a42d16a27388b1d92677/gitlab/v4/objects/releases.py#L12

And upgrading form `NoUpdateMixin` to `CRUDMixin` https://github.com/python-gitlab/python-gitlab/blob/8753add72061ea01c508a42d16a27388b1d92677/gitlab/v4/objects/releases.py#L17

I was able to make it work.

I can create a Pull Request if you want
> I was able to make it work.
> 
> I can create a Pull Request if you want

A PR would be great! Thanks 😊

If can also add an `_update_attrs` attribute that would be good too.

Resolving patch:
diff --git a/gitlab/v4/objects/releases.py b/gitlab/v4/objects/releases.py
--- a/gitlab/v4/objects/releases.py
+++ b/gitlab/v4/objects/releases.py
@@ -1,5 +1,5 @@
 from gitlab.base import RequiredOptional, RESTManager, RESTObject
-from gitlab.mixins import CRUDMixin, NoUpdateMixin, ObjectDeleteMixin, SaveMixin
+from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
 
 __all__ = [
     "ProjectRelease",
@@ -9,18 +9,21 @@
 ]
 
 
-class ProjectRelease(RESTObject):
+class ProjectRelease(SaveMixin, RESTObject):
     _id_attr = "tag_name"
     _managers = (("links", "ProjectReleaseLinkManager"),)
 
 
-class ProjectReleaseManager(NoUpdateMixin, RESTManager):
+class ProjectReleaseManager(CRUDMixin, RESTManager):
     _path = "/projects/%(project_id)s/releases"
     _obj_cls = ProjectRelease
     _from_parent_attrs = {"project_id": "id"}
     _create_attrs = RequiredOptional(
         required=("name", "tag_name", "description"), optional=("ref", "assets")
     )
+    _update_attrs = RequiredOptional(
+        optional=("name", "description", "milestones", "released_at")
+    )
 
 
 class ProjectReleaseLink(ObjectDeleteMixin, SaveMixin, RESTObject):


Test patch:
diff --git a/tests/functional/api/test_releases.py b/tests/functional/api/test_releases.py
--- a/tests/functional/api/test_releases.py
+++ b/tests/functional/api/test_releases.py
@@ -23,6 +23,15 @@ def test_create_project_release(project, project_file):
     assert release.description == release_description
 
 
+def test_update_save_project_release(project, release):
+    updated_description = f"{release.description} updated"
+    release.description = updated_description
+    release.save()
+
+    release = project.releases.get(release.tag_name)
+    assert release.description == updated_description
+
+
 def test_delete_project_release(project, release):
     project.releases.delete(release.tag_name)
     assert release not in project.releases.list()
diff --git a/tests/unit/objects/test_releases.py b/tests/unit/objects/test_releases.py
--- a/tests/unit/objects/test_releases.py
+++ b/tests/unit/objects/test_releases.py
@@ -10,7 +10,11 @@
 
 from gitlab.v4.objects import ProjectReleaseLink
 
+tag_name = "v1.0.0"
 encoded_tag_name = "v1%2E0%2E0"
+release_name = "demo-release"
+release_description = "my-rel-desc"
+released_at = "2019-03-15T08:00:00Z"
 link_name = "hello-world"
 link_url = "https://gitlab.example.com/group/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64"
 direct_url = f"https://gitlab.example.com/group/hello/-/releases/{encoded_tag_name}/downloads/hello-world"
@@ -24,6 +28,18 @@
     "link_type": "other",
 }
 
+release_content = {
+    "id": 3,
+    "tag_name": tag_name,
+    "name": release_name,
+    "description": release_description,
+    "milestones": [],
+    "released_at": released_at,
+}
+
+release_url = re.compile(
+    rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}"
+)
 links_url = re.compile(
     rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links"
 )
@@ -100,6 +116,21 @@ def resp_delete_link(no_content):
         yield rsps
 
 
+@pytest.fixture
+def resp_update_release():
+    updated_content = dict(release_content)
+
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.PUT,
+            url=release_url,
+            json=updated_content,
+            content_type="application/json",
+            status=200,
+        )
+        yield rsps
+
+
 def test_list_release_links(release, resp_list_links):
     links = release.links.list()
     assert isinstance(links, list)
@@ -129,3 +160,11 @@ def test_update_release_link(release, resp_update_link):
 def test_delete_release_link(release, resp_delete_link):
     link = release.links.get(1, lazy=True)
     link.delete()
+
+
+def test_update_release(release, resp_update_release):
+    release.name = release_name
+    release.description = release_description
+    release.save()
+    assert release.name == release_name
+    assert release.description == release_description