Skip to content

Commit

Permalink
feat: changelog filters are specialized per vcs type (#890)
Browse files Browse the repository at this point in the history
* test(github): sync pr url expectation with GitHub api documentation

* fix(github): correct changelog filter for pull request urls

* refactor(hvcs-base): change to an abstract class & simplify interface

* refactor(remote-hvcs-base): extend the base abstract class with common remote base class

* refactor(github): adapt to new abstract base class

* refactor(gitea): adapt to new abstract base class

* refactor(gitlab): adapt to new abstract base class

* refactor(bitbucket): adapt to new abstract base class

* refactor(cmds): prevent hvcs from executing if not remote hosted vcs

* feat(changelog): changelog filters are hvcs focused

* test(hvcs): add validation for issue_url generation

* feat(changelog-github): add issue url filter to changelog context

* feat(changelog-gitea): add issue url filter to changelog context

* refactor(cmd-version): consolidate asset uploads with release creation

* style: resolve ruff errors

* feat(changelog-context): add flag to jinja env for which hvcs is available

* test(changelog-context): demonstrate per hvcs filters upon render

* docs(changelog-context): explain new hvcs specific context filters

* refactor(config): adjust default token resolution w/ subclasses
  • Loading branch information
codejedi365 committed Apr 27, 2024
1 parent 8586e48 commit 76ed593
Show file tree
Hide file tree
Showing 21 changed files with 1,117 additions and 702 deletions.
48 changes: 43 additions & 5 deletions docs/changelog_templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ project.

.. _changelog-templates-template-rendering-template-context:

Template Context:
^^^^^^^^^^^^^^^^^
Template Context
^^^^^^^^^^^^^^^^

Alongside the rendering of a directory tree, Python Semantic Release makes information
about the history of the project available within the templating environment in order
Expand All @@ -156,19 +156,57 @@ Python terms, ``context`` is a `dataclass`_ with the following attributes:

* ``repo_name: str``: the name of the current repository parsed from the Git url.
* ``repo_owner: str``: the owner of the current repository parsed from the Git url.
* ``hvcs_type: str``: the name of the VCS server type currently configured.
* ``history: ReleaseHistory``: a :py:class:`semantic_release.changelog.ReleaseHistory` instance.
(See :ref:`changelog-templates-template-rendering-template-context-release-history`)
* ``filters: Tuple[Callable[..., Any], ...]``: a tuple of filters for the template environment.
These are added to the environment's ``filters``, and therefore there should be no need to
access these from the ``context`` object inside the template.

Currently, two filters are defined:
The filters provided vary based on the VCS configured and available features:

* ``create_server_url: Callable[[str, str | None, str | None, str | None], str]``: 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 resulting url.
Parameter order is as described above respectively.

* ``create_repo_url: Callable[[str, str | None, str | None], str]``: when given a repository
path, prepend the configured vcs server host, and repo namespace. Optionally you can provide,
an additional query string and/or a url fragment to also put in the url. Parameter order is
as described above respectively. This is similar to ``create_server_url`` but includes the repo
namespace and owner automatically.

* ``pull_request_url: Callable[[str], str]``: given a pull request number, return a URL
to the pull request in the remote.
* ``commit_hash_url: Callable[[str], str]``: given a commit hash, return a URL to the
commit in the remote.

* ``compare_url: Callable[[str, str], str]``: given a starting git reference and a ending git
reference create a comparison url between the two references that can be opened on the remote

* ``issue_url: Callable[[str | int], str]``: given an issue number, return a URL to the issue
on the remote vcs.

* ``merge_request_url: Callable[[str | int], str]``: given a merge request number, return a URL
to the merge request in the remote. This is an alias to the ``pull_request_url`` but only
available for the VCS that uses the merge request terminology.

* ``pull_request_url: Callable[[str | int], str]``: given a pull request number, return a URL
to the pull request in the remote. For remote vcs' that use merge request terminology, this
filter is an alias to the ``merge_request_url`` filter function.

Availability of the documented filters can be found in the table below:

====================== ========= ===== ====== ======
**filter - hvcs_type** bitbucket gitea github gitlab
====================== ========= ===== ====== ======
create_server_url ✅ ✅ ✅ ✅
create_repo_url ✅ ✅ ✅ ✅
commit_hash_url ✅ ✅ ✅ ✅
compare_url ✅ ❌ ✅ ✅
issue_url ❌ ✅ ✅ ✅
merge_request_url ❌ ❌ ❌ ✅
pull_request_url ✅ ✅ ✅ ✅
====================== ========= ===== ====== ======

.. seealso::
* `Filters <https://jinja.palletsprojects.com/en/3.1.x/templates/#filters>`_

Expand Down
6 changes: 5 additions & 1 deletion semantic_release/changelog/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
class ChangelogContext:
repo_name: str
repo_owner: str
hvcs_type: str
history: ReleaseHistory
filters: tuple[Callable[..., Any], ...] = ()

Expand All @@ -31,5 +32,8 @@ def make_changelog_context(
repo_name=hvcs_client.repo_name,
repo_owner=hvcs_client.owner,
history=release_history,
filters=(hvcs_client.pull_request_url, hvcs_client.commit_hash_url),
hvcs_type=hvcs_client.__class__.__name__.lower(),
filters=(
*hvcs_client.get_changelog_context_filters(),
),
)
3 changes: 2 additions & 1 deletion semantic_release/cli/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
render_release_notes,
)
from semantic_release.cli.util import noop_report
from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase

if TYPE_CHECKING:
from semantic_release.cli.commands.cli_context import CliContextObj
Expand Down Expand Up @@ -79,7 +80,7 @@ def changelog(cli_ctx: CliContextObj, release_tag: str | None = None) -> None:
else:
recursive_render(template_dir, environment=env, _root_dir=repo.working_dir)

if release_tag:
if release_tag and isinstance(hvcs_client, RemoteHvcsBase):
if runtime.global_cli_options.noop:
noop_report(
f"would have posted changelog to the release for tag {release_tag}"
Expand Down
6 changes: 6 additions & 0 deletions semantic_release/cli/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import click

from semantic_release.cli.util import noop_report
from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
from semantic_release.version import tags_and_versions

if TYPE_CHECKING:
Expand Down Expand Up @@ -45,6 +46,11 @@ def publish(cli_ctx: CliContextObj, tag: str = "latest") -> None:
f"No tags found with format {translator.tag_format!r}, couldn't "
"identify latest version"
)

if not isinstance(hvcs_client, RemoteHvcsBase):
log.info("Remote does not support artifact upload. Exiting with no action taken...")
ctx.exit(0)

if runtime.global_cli_options.noop:
noop_report(
"would have uploaded files matching any of the globs "
Expand Down
17 changes: 4 additions & 13 deletions semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from semantic_release.const import DEFAULT_SHELL, DEFAULT_VERSION
from semantic_release.enums import LevelBump
from semantic_release.errors import UnexpectedResponse
from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
from semantic_release.version import Version, next_version, tags_and_versions

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -601,7 +602,7 @@ def custom_git_environment() -> ContextManager[None]:

gha_output.released = True

if make_vcs_release:
if make_vcs_release and isinstance(hvcs_client, RemoteHvcsBase):
if opts.noop:
noop_report(
f"would have created a release for the tag {new_version.as_tag()!r}"
Expand Down Expand Up @@ -630,10 +631,11 @@ def custom_git_environment() -> ContextManager[None]:
noop_report(f"would have uploaded the following assets: {runtime.assets}")
else:
try:
release_id = hvcs_client.create_or_update_release(
hvcs_client.create_release(
tag=new_version.as_tag(),
release_notes=release_notes,
prerelease=new_version.is_prerelease,
assets=assets,
)
except HTTPError as err:
log.exception(err)
Expand All @@ -654,15 +656,4 @@ def custom_git_environment() -> ContextManager[None]:
log.exception(e)
ctx.fail(str(e))

for asset in assets:
log.info("Uploading asset %s", asset)
try:
hvcs_client.upload_asset(release_id, asset)
except HTTPError as err:
log.exception(err)
ctx.fail(str.join("\n", [str(err), "Failed to upload asset!"]))
except Exception as e:
log.exception(e)
ctx.fail(str(e))

return str(new_version)
20 changes: 15 additions & 5 deletions semantic_release/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from semantic_release.const import COMMIT_MESSAGE, DEFAULT_COMMIT_AUTHOR, SEMVER_REGEX
from semantic_release.errors import InvalidConfiguration, NotAReleaseBranch
from semantic_release.helpers import dynamic_import
from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
from semantic_release.version import VersionTranslator
from semantic_release.version.declaration import (
PatternVersionDeclaration,
Expand Down Expand Up @@ -141,13 +142,22 @@ def resolve_env_vars(cls, val: Any) -> str | None:
def set_default_token(self) -> Self:
# Set the default token name for the given VCS when no user input is given
if not self.token and self.type in _known_hvcs:
default_token_name = _known_hvcs[self.type].DEFAULT_ENV_TOKEN_NAME
if default_token_name:
env_token = EnvConfigVar(env=default_token_name).getvalue()
if env_token:
self.token = env_token
if env_token := self._get_default_token():
self.token = env_token
return self

def _get_default_token(self) -> str | None:
hvcs_client_class = _known_hvcs[self.type]
default_hvcs_instance = hvcs_client_class("git@example.com:owner/project.git")
if not isinstance(default_hvcs_instance, RemoteHvcsBase):
return None

default_token_name = default_hvcs_instance.DEFAULT_ENV_TOKEN_NAME
if not default_token_name:
return None

return EnvConfigVar(env=default_token_name).getvalue()

@model_validator(mode="after")
def check_url_scheme(self) -> Self:
if self.url and isinstance(self.url, str):
Expand Down
14 changes: 13 additions & 1 deletion semantic_release/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,20 @@ class MissingMergeBaseError(SemanticReleaseBaseError):
"""


class UnexpectedResponse(Exception):
class UnexpectedResponse(SemanticReleaseBaseError):
"""
Raised when an HTTP response cannot be parsed properly or the expected structure
is not found.
"""

class IncompleteReleaseError(SemanticReleaseBaseError):
"""
Raised when there is a failure amongst one of the api requests when creating a
release on a remote hvcs.
"""

class AssetUploadError(SemanticReleaseBaseError):
"""
Raised when there is a failure uploading an asset to a remote hvcs's release artifact
storage.
"""
11 changes: 10 additions & 1 deletion semantic_release/hvcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
from semantic_release.hvcs.gitea import Gitea
from semantic_release.hvcs.github import Github
from semantic_release.hvcs.gitlab import Gitlab
from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
from semantic_release.hvcs.token_auth import TokenAuth

__all__ = ["Bitbucket", "Gitea", "Github", "Gitlab", "HvcsBase", "TokenAuth"]
__all__ = [
"Bitbucket",
"Gitea",
"Github",
"Gitlab",
"HvcsBase",
"RemoteHvcsBase",
"TokenAuth",
]

0 comments on commit 76ed593

Please sign in to comment.