From 8ac281d38ea3df9f81c827f7cee43e23bb8e5b56 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 23:24:47 -0500 Subject: [PATCH 01/10] test(release-notes-context): add unit test to validate use `create_release_url` filter --- .../changelog/test_release_notes.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/unit/semantic_release/changelog/test_release_notes.py b/tests/unit/semantic_release/changelog/test_release_notes.py index 9f506d332..576cc55ad 100644 --- a/tests/unit/semantic_release/changelog/test_release_notes.py +++ b/tests/unit/semantic_release/changelog/test_release_notes.py @@ -4,6 +4,7 @@ from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING +from unittest import mock import pytest @@ -601,3 +602,48 @@ def test_release_notes_context_pypi_url_filter_tagged( ) assert expected_content == actual_content + + +@pytest.mark.parametrize("hvcs_client_class", [Github, Gitlab, Gitea]) +def test_release_notes_context_release_url_filter( + example_git_https_url: str, + hvcs_client_class: type[Github | Gitlab | Gitea], + single_release_history: ReleaseHistory, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +): + version = list(single_release_history.released.keys())[-1] + release = single_release_history.released[version] + + example_project_dir.joinpath(".release_notes.md.j2").write_text( + dedent( + """\ + {{ + "[%s](%s)" | format( + release.version.as_tag(), + release.version.as_tag() | create_release_url, + ) + }} + """ + ) + ) + + with mock.patch.dict(os.environ, {}, clear=True): + hvcs_client = hvcs_client_class(remote_url=example_git_https_url) + + expected_content = dedent( + f"""\ + [{version.as_tag()}]({hvcs_client.create_release_url(version.as_tag())}) + """ + ) + + actual_content = generate_release_notes( + hvcs_client=hvcs_client, + release=release, + template_dir=example_project_dir, + history=single_release_history, + style="angular", + mask_initial_release=False, + ) + + assert expected_content == actual_content From bf130c5eaf3dd61b29e5fe47eb29e2e0ee5226fc Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 23:25:37 -0500 Subject: [PATCH 02/10] test(release-notes-context): add unit test to validate use `format_w_official_vcs_name` filter --- .../changelog/test_release_notes.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/unit/semantic_release/changelog/test_release_notes.py b/tests/unit/semantic_release/changelog/test_release_notes.py index 576cc55ad..ec51bffcd 100644 --- a/tests/unit/semantic_release/changelog/test_release_notes.py +++ b/tests/unit/semantic_release/changelog/test_release_notes.py @@ -647,3 +647,46 @@ def test_release_notes_context_release_url_filter( ) assert expected_content == actual_content + + +@pytest.mark.parametrize("hvcs_client_class", [Github, Gitlab, Gitea, Bitbucket]) +def test_release_notes_context_format_w_official_name_filter( + example_git_https_url: str, + hvcs_client_class: type[Github | Gitlab | Gitea], + single_release_history: ReleaseHistory, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +): + version = list(single_release_history.released.keys())[-1] + release = single_release_history.released[version] + + example_project_dir.joinpath(".release_notes.md.j2").write_text( + dedent( + """\ + {{ "%s" | format_w_official_vcs_name }} + {{ "{}" | format_w_official_vcs_name }} + {{ "{vcs_name}" | format_w_official_vcs_name }} + """ + ) + ) + + with mock.patch.dict(os.environ, {}, clear=True): + hvcs_client = hvcs_client_class(remote_url=example_git_https_url) + expected_content = dedent( + f"""\ + {hvcs_client.OFFICIAL_NAME} + {hvcs_client.OFFICIAL_NAME} + {hvcs_client.OFFICIAL_NAME} + """ + ) + + actual_content = generate_release_notes( + hvcs_client=hvcs_client, + release=release, + template_dir=example_project_dir, + history=single_release_history, + style="angular", + mask_initial_release=False, + ) + + assert expected_content == actual_content From a9bfea3840b3bc80b77d27cc26df29a4864d2c47 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 23:26:37 -0500 Subject: [PATCH 03/10] test(changelog-context): add unit test to validate use `create_release_url` filter --- .../changelog/test_changelog_context.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/semantic_release/changelog/test_changelog_context.py b/tests/unit/semantic_release/changelog/test_changelog_context.py index 8204575f6..0124e1125 100644 --- a/tests/unit/semantic_release/changelog/test_changelog_context.py +++ b/tests/unit/semantic_release/changelog/test_changelog_context.py @@ -4,6 +4,7 @@ from datetime import datetime from textwrap import dedent from typing import TYPE_CHECKING +from unittest import mock import pytest from git import Commit, Object, Repo @@ -659,3 +660,50 @@ def test_changelog_context_pypi_url_filter_tagged( # Evaluate assert expected_changelog == actual_changelog + + +@pytest.mark.parametrize("hvcs_client_class", [Github, Gitlab, Gitea]) +def test_changelog_context_release_url_filter( + example_git_https_url: str, + hvcs_client_class: type[Github | Gitlab | Gitea], + artificial_release_history: ReleaseHistory, + changelog_md_file: Path, +): + version = list(artificial_release_history.released.keys())[-1] + + changelog_tpl = dedent( + """\ + {% set release = context.history.released.values() | first + %}{{ + "[%s](%s)" | format( + release.version.as_tag(), + release.version.as_tag() | create_release_url, + ) + }} + """ + ) + + with mock.patch.dict(os.environ, {}, clear=True): + hvcs_client = hvcs_client_class(remote_url=example_git_https_url) + expected_changelog = dedent( + f"""\ + [{version.as_tag()}]({hvcs_client.create_release_url(version.as_tag())}) + """ + ) + + env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) + context = make_changelog_context( + hvcs_client=hvcs_client, + release_history=artificial_release_history, + mode=ChangelogMode.UPDATE, + prev_changelog_file=changelog_md_file, + insertion_flag="", + mask_initial_release=False, + ) + context.bind_to_environment(env) + + # Create changelog from template with environment + actual_changelog = env.from_string(changelog_tpl).render() + + # Evaluate + assert expected_changelog == actual_changelog From 9c2f68a57d6dc750a699bf935be9c31898f387e4 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 23:26:57 -0500 Subject: [PATCH 04/10] test(changelog-context): add unit test to validate use `format_w_official_vcs_name` filter --- .../changelog/test_changelog_context.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/unit/semantic_release/changelog/test_changelog_context.py b/tests/unit/semantic_release/changelog/test_changelog_context.py index 0124e1125..f665d78aa 100644 --- a/tests/unit/semantic_release/changelog/test_changelog_context.py +++ b/tests/unit/semantic_release/changelog/test_changelog_context.py @@ -707,3 +707,46 @@ def test_changelog_context_release_url_filter( # Evaluate assert expected_changelog == actual_changelog + + +@pytest.mark.parametrize("hvcs_client_class", [Github, Gitlab, Gitea, Bitbucket]) +def test_changelog_context_format_w_official_name_filter( + example_git_https_url: str, + hvcs_client_class: type[Github | Gitlab | Gitea], + artificial_release_history: ReleaseHistory, + changelog_md_file: Path, +): + changelog_tpl = dedent( + """\ + {{ "%s" | format_w_official_vcs_name }} + {{ "{}" | format_w_official_vcs_name }} + {{ "{vcs_name}" | format_w_official_vcs_name }} + """ + ) + + with mock.patch.dict(os.environ, {}, clear=True): + hvcs_client = hvcs_client_class(remote_url=example_git_https_url) + expected_changelog = dedent( + f"""\ + {hvcs_client.OFFICIAL_NAME} + {hvcs_client.OFFICIAL_NAME} + {hvcs_client.OFFICIAL_NAME} + """ + ) + + env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) + context = make_changelog_context( + hvcs_client=hvcs_client, + release_history=artificial_release_history, + mode=ChangelogMode.UPDATE, + prev_changelog_file=changelog_md_file, + insertion_flag="", + mask_initial_release=False, + ) + context.bind_to_environment(env) + + # Create changelog from template with environment + actual_changelog = env.from_string(changelog_tpl).render() + + # Evaluate + assert expected_changelog == actual_changelog From c82069fa574cd2b63758f6de3306ca5c98607c7e Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Tue, 24 Dec 2024 00:51:06 -0500 Subject: [PATCH 05/10] feat(vcs-github): define `create_release_url` & `format_w_official_vcs_name` filters --- src/semantic_release/hvcs/github.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/semantic_release/hvcs/github.py b/src/semantic_release/hvcs/github.py index 5ab58527f..016643267 100644 --- a/src/semantic_release/hvcs/github.py +++ b/src/semantic_release/hvcs/github.py @@ -73,6 +73,7 @@ class Github(RemoteHvcsBase): `server.domain/api/v3` based on the documentation in April 2024. """ + OFFICIAL_NAME = "GitHub" DEFAULT_DOMAIN = "github.com" DEFAULT_API_SUBDOMAIN_PREFIX = "api" DEFAULT_API_DOMAIN = f"{DEFAULT_API_SUBDOMAIN_PREFIX}.{DEFAULT_DOMAIN}" @@ -531,6 +532,24 @@ def pull_request_url(self, pr_number: str | int) -> str: return "" + def create_release_url(self, tag: str = "") -> str: + tag_str = tag.strip() + tag_path = f"tag/{tag_str}" if tag_str else "" + return self.create_repo_url(repo_path=f"releases/{tag_path}") + + @staticmethod + def format_w_official_vcs_name(format_str: str) -> str: + if "%s" in format_str: + return format_str % Github.OFFICIAL_NAME + + if "{}" in format_str: + return format_str.format(Github.OFFICIAL_NAME) + + if "{vcs_name}" in format_str: + return format_str.format(vcs_name=Github.OFFICIAL_NAME) + + return format_str + def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: return ( self.create_server_url, @@ -539,6 +558,8 @@ def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: self.compare_url, self.issue_url, self.pull_request_url, + self.create_release_url, + self.format_w_official_vcs_name, ) From aa9caffae8b61b9758a888d2c82ec6312e5fa6d4 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Tue, 24 Dec 2024 00:57:59 -0500 Subject: [PATCH 06/10] feat(vcs-gitlab): define `create_release_url` & `format_w_official_vcs_name` filters --- src/semantic_release/hvcs/gitlab.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/semantic_release/hvcs/gitlab.py b/src/semantic_release/hvcs/gitlab.py index 005a6f4dc..67b8e7512 100644 --- a/src/semantic_release/hvcs/gitlab.py +++ b/src/semantic_release/hvcs/gitlab.py @@ -41,6 +41,7 @@ class Gitlab(RemoteHvcsBase): # purposefully not CI_JOB_TOKEN as it is not a personal access token, # It is missing the permission to push to the repository, but has all others (releases, packages, etc.) + OFFICIAL_NAME = "GitLab" DEFAULT_DOMAIN = "gitlab.com" def __init__( @@ -276,6 +277,23 @@ def pull_request_url(self, pr_number: str | int) -> str: def upload_dists(self, tag: str, dist_glob: str) -> int: return super().upload_dists(tag, dist_glob) + def create_release_url(self, tag: str = "") -> str: + tag_str = tag.strip() + return self.create_repo_url(repo_path=f"/-/releases/{tag_str}") + + @staticmethod + def format_w_official_vcs_name(format_str: str) -> str: + if "%s" in format_str: + return format_str % Gitlab.OFFICIAL_NAME + + if "{}" in format_str: + return format_str.format(Gitlab.OFFICIAL_NAME) + + if "{vcs_name}" in format_str: + return format_str.format(vcs_name=Gitlab.OFFICIAL_NAME) + + return format_str + def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: return ( self.create_server_url, @@ -285,6 +303,8 @@ def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: self.issue_url, self.merge_request_url, self.pull_request_url, + self.create_release_url, + self.format_w_official_vcs_name, ) From 58929bacf1a3137a3ac2f6feb2e95621d15a3357 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 22:10:55 -0500 Subject: [PATCH 07/10] feat(vcs-gitea): define `create_release_url` & `format_w_official_vcs_name` filters --- src/semantic_release/hvcs/gitea.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/semantic_release/hvcs/gitea.py b/src/semantic_release/hvcs/gitea.py index fa21d1870..c8e241122 100644 --- a/src/semantic_release/hvcs/gitea.py +++ b/src/semantic_release/hvcs/gitea.py @@ -34,6 +34,7 @@ class Gitea(RemoteHvcsBase): """Gitea helper class""" + OFFICIAL_NAME = "Gitea" DEFAULT_DOMAIN = "gitea.com" DEFAULT_API_PATH = "/api/v1" DEFAULT_ENV_TOKEN_NAME = "GITEA_TOKEN" # noqa: S105 @@ -380,6 +381,24 @@ def pull_request_url(self, pr_number: str | int) -> str: return "" + def create_release_url(self, tag: str = "") -> str: + tag_str = tag.strip() + tag_path = f"tag/{tag_str}" if tag_str else "" + return self.create_repo_url(repo_path=f"releases/{tag_path}") + + @staticmethod + def format_w_official_vcs_name(format_str: str) -> str: + if "%s" in format_str: + return format_str % Gitea.OFFICIAL_NAME + + if "{}" in format_str: + return format_str.format(Gitea.OFFICIAL_NAME) + + if "{vcs_name}" in format_str: + return format_str.format(vcs_name=Gitea.OFFICIAL_NAME) + + return format_str + def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: return ( self.create_server_url, @@ -387,6 +406,8 @@ def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: self.commit_hash_url, self.issue_url, self.pull_request_url, + self.create_release_url, + self.format_w_official_vcs_name, ) From 5b4f72b2249f64e451475ca6f4745585c556e90e Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 22:11:35 -0500 Subject: [PATCH 08/10] feat(vcs-bitbucket): define `format_w_official_vcs_name` filter --- src/semantic_release/hvcs/bitbucket.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/semantic_release/hvcs/bitbucket.py b/src/semantic_release/hvcs/bitbucket.py index 6501cacef..e0f9e8656 100644 --- a/src/semantic_release/hvcs/bitbucket.py +++ b/src/semantic_release/hvcs/bitbucket.py @@ -45,6 +45,7 @@ class Bitbucket(RemoteHvcsBase): `server.domain/rest/api/1.0` based on the documentation in April 2024. """ + OFFICIAL_NAME = "Bitbucket" DEFAULT_DOMAIN = "bitbucket.org" DEFAULT_API_SUBDOMAIN_PREFIX = "api" DEFAULT_API_PATH_CLOUD = "/2.0" @@ -216,6 +217,19 @@ def pull_request_url(self, pr_number: str | int) -> str: return "" + @staticmethod + def format_w_official_vcs_name(format_str: str) -> str: + if "%s" in format_str: + return format_str % Bitbucket.OFFICIAL_NAME + + if "{}" in format_str: + return format_str.format(Bitbucket.OFFICIAL_NAME) + + if "{vcs_name}" in format_str: + return format_str.format(vcs_name=Bitbucket.OFFICIAL_NAME) + + return format_str + def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: return ( self.create_server_url, @@ -223,6 +237,7 @@ def get_changelog_context_filters(self) -> tuple[Callable[..., Any], ...]: self.commit_hash_url, self.compare_url, self.pull_request_url, + self.format_w_official_vcs_name, ) def upload_dists(self, tag: str, dist_glob: str) -> int: From 913bfdd4be6210fca99c6d50e75376cba7eb7cb1 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 27 Jan 2025 23:45:30 -0500 Subject: [PATCH 09/10] docs(changelog-templates): define new `create_release_url` & `format_w_official_vcs_name` filters --- docs/changelog_templates.rst | 74 ++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/docs/changelog_templates.rst b/docs/changelog_templates.rst index 1d86376f9..08dfc5edd 100644 --- a/docs/changelog_templates.rst +++ b/docs/changelog_templates.rst @@ -679,6 +679,24 @@ The filters provided vary based on the VCS configured and available features: https://pypi.org/project/example-package https://pypi.org/project/example-package/1.0.0 +* ``create_release_url (Callable[[TagStr], UrlStr])``: given a tag, return a URL to the release + page on the remote vcs. This filter is useful when you want to link to the release page on the + remote vcs. + + *Introduced in ${NEW_RELEASE_TAG}.* + + **Example Usage:** + + .. code:: jinja + + {{ "v1.0.0" | create_release_url }} + + **Markdown Output:** + + .. code:: markdown + + https://example.com/example/repo/releases/tag/v1.0.0 + * ``create_server_url (Callable[[PathStr, AuthStr | None, QueryStr | None, FragmentStr | None], UrlStr])``: when given a path, prepend the configured vcs server host and url scheme. Optionally you can provide, a auth string, a query string or a url fragment to be normalized into the @@ -838,6 +856,29 @@ The filters provided vary based on the VCS configured and available features: [#29](https://example.com/example/repo/pull/29) +* ``format_w_official_vcs_name (Callable[[str], str])``: given a format string, insert + the official VCS type name into the string and return. This filter is useful when you want to + display the proper name of the VCS type in a changelog or release notes. The filter supports + three different replace formats: ``%s``, ``{}``, and ``{vcs_name}``. + + *Introduced in ${NEW_RELEASE_TAG}.* + + **Example Usage:** + + .. code:: jinja + + {{ "%s Releases" | format_w_official_vcs_name }} + {{ "{} Releases" | format_w_official_vcs_name }} + {{ "{vcs_name} Releases" | format_w_official_vcs_name }} + + **Markdown Output:** + + .. code:: markdown + + GitHub Releases + GitHub Releases + GitHub Releases + * ``read_file (Callable[[str], str])``: given a file path, read the file and return the contents as a string. This function was added specifically to enable the changelog update feature where it would load the existing changelog @@ -879,21 +920,24 @@ The filters provided vary based on the VCS configured and available features: Availability of the documented filters can be found in the table below: -====================== ========= ===== ====== ====== -**filter - hvcs_type** bitbucket gitea github gitlab -====================== ========= ===== ====== ====== -autofit_text_width ✅ ✅ ✅ ✅ -convert_md_to_rst ✅ ✅ ✅ ✅ -create_server_url ✅ ✅ ✅ ✅ -create_repo_url ✅ ✅ ✅ ✅ -commit_hash_url ✅ ✅ ✅ ✅ -compare_url ✅ ❌ ✅ ✅ -issue_url ❌ ✅ ✅ ✅ -merge_request_url ❌ ❌ ❌ ✅ -pull_request_url ✅ ✅ ✅ ✅ -read_file ✅ ✅ ✅ ✅ -sort_numerically ✅ ✅ ✅ ✅ -====================== ========= ===== ====== ====== +========================== ========= ===== ====== ====== +**filter - hvcs_type** bitbucket gitea github gitlab +========================== ========= ===== ====== ====== +autofit_text_width ✅ ✅ ✅ ✅ +convert_md_to_rst ✅ ✅ ✅ ✅ +create_pypi_url ✅ ✅ ✅ ✅ +create_server_url ✅ ✅ ✅ ✅ +create_release_url ❌ ✅ ✅ ✅ +create_repo_url ✅ ✅ ✅ ✅ +commit_hash_url ✅ ✅ ✅ ✅ +compare_url ✅ ❌ ✅ ✅ +format_w_official_vcs_name ✅ ✅ ✅ ✅ +issue_url ❌ ✅ ✅ ✅ +merge_request_url ❌ ❌ ❌ ✅ +pull_request_url ✅ ✅ ✅ ✅ +read_file ✅ ✅ ✅ ✅ +sort_numerically ✅ ✅ ✅ ✅ +========================== ========= ===== ====== ====== .. seealso:: * `Filters `_ From ddf71424df576f8c3e2e0cf61da5339a77cb9d0f Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sun, 26 Jan 2025 22:24:38 -0500 Subject: [PATCH 10/10] chore(release-notes): refactor to use latest filters from vcs --- config/release-templates/.release_notes.md.j2 | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/config/release-templates/.release_notes.md.j2 b/config/release-templates/.release_notes.md.j2 index b1e132587..a58588538 100644 --- a/config/release-templates/.release_notes.md.j2 +++ b/config/release-templates/.release_notes.md.j2 @@ -103,14 +103,8 @@ {{ "- %s" | format( format_link( - [ - "https://github.com", - repo_name, - repo_name, - "releases/tag", - release.version.as_tag(), - ] | join("/"), - "GitHub Release Assets", + release.version.as_tag() | create_release_url, + "{vcs_name} Release Assets" | format_w_official_vcs_name, ) ) }}