Repository: relekang/python-semantic-release
Pull request #447 (2022-05-27 13:30:43)

fix: :bug: fix get_current_release_version for tag_only version_source
I _think_ this is a bug, but let me know if not...

`get_current_release_version()` doesn't work when using `tag_only` version source... it defaults back to `get_current_release_version_by_commits` which may show nothing


Patch:
diff --git a/.gitignore b/.gitignore
index 54782e6..7b02cda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,3 +69,4 @@ target/
 .pytest_cache
 .mypy_cache
 .python-version
+*.swp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64703be..061aeb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,50 @@
 
 <!--next-version-placeholder-->
 
+## v7.30.2 (2022-07-26)
+### Fix
+* Declare additional_options as action inputs ([#481](https://github.com/relekang/python-semantic-release/issues/481)) ([`cb5d8c7`](https://github.com/relekang/python-semantic-release/commit/cb5d8c7ce7d013fcfabd7696b5ffb846a8a6f853))
+
+## v7.30.1 (2022-07-25)
+### Fix
+* Don't use commit_subject for tag pattern matching ([#480](https://github.com/relekang/python-semantic-release/issues/480)) ([`ac3f11e`](https://github.com/relekang/python-semantic-release/commit/ac3f11e689f4a290d20b68b9c5c214098eb61b5f))
+
+## v7.30.0 (2022-07-25)
+### Feature
+* Add `additional_options` input for GitHub Action ([#477](https://github.com/relekang/python-semantic-release/issues/477)) ([`aea60e3`](https://github.com/relekang/python-semantic-release/commit/aea60e3d290c6fe3137bff21e0db1ed936233776))
+
+### Fix
+* Allow empty additional options ([#479](https://github.com/relekang/python-semantic-release/issues/479)) ([`c9b2514`](https://github.com/relekang/python-semantic-release/commit/c9b2514d3e164b20e78b33f60989d78c2587e1df))
+
+## v7.29.7 (2022-07-24)
+### Fix
+* Ignore dependency version bumps when parsing version from commit logs ([#476](https://github.com/relekang/python-semantic-release/issues/476)) ([`51bcb78`](https://github.com/relekang/python-semantic-release/commit/51bcb780a9f55fadfaf01612ff65c1f92642c2c1))
+
+## v7.29.6 (2022-07-15)
+### Fix
+* Allow changing prerelease tag using CLI flags ([#466](https://github.com/relekang/python-semantic-release/issues/466)) ([`395bf4f`](https://github.com/relekang/python-semantic-release/commit/395bf4f2de73663c070f37cced85162d41934213))
+
+## v7.29.5 (2022-07-14)
+### Fix
+* **publish:** Get version bump for current release ([#467](https://github.com/relekang/python-semantic-release/issues/467)) ([`dd26888`](https://github.com/relekang/python-semantic-release/commit/dd26888a923b2f480303c19f1916647de48b02bf))
+* Add packaging module requirement ([#469](https://github.com/relekang/python-semantic-release/issues/469)) ([`b99c9fa`](https://github.com/relekang/python-semantic-release/commit/b99c9fa88dc25e5ceacb131cd93d9079c4fb2c86))
+
+## v7.29.4 (2022-06-29)
+### Fix
+* Add text for empty ValueError ([#461](https://github.com/relekang/python-semantic-release/issues/461)) ([`733254a`](https://github.com/relekang/python-semantic-release/commit/733254a99320d8c2f964d799ac4ec29737867faa))
+
+## v7.29.3 (2022-06-26)
+### Fix
+* Ensure that assets can be uploaded successfully on custom GitHub servers ([#458](https://github.com/relekang/python-semantic-release/issues/458)) ([`32b516d`](https://github.com/relekang/python-semantic-release/commit/32b516d7aded4afcafe4aa56d6a5a329b3fc371d))
+
+## v7.29.2 (2022-06-20)
+### Fix
+* Ensure should_bump checks against release version if not prerelease ([#457](https://github.com/relekang/python-semantic-release/issues/457)) ([`da0606f`](https://github.com/relekang/python-semantic-release/commit/da0606f0d67ada5f097c704b9423ead3b5aca6b2))
+
+## v7.29.1 (2022-06-01)
+### Fix
+* Capture correct release version when patch has more than one digit ([#448](https://github.com/relekang/python-semantic-release/issues/448)) ([`426cdc7`](https://github.com/relekang/python-semantic-release/commit/426cdc7d7e0140da67f33b6853af71b2295aaac2))
+
 ## v7.29.0 (2022-05-27)
 ### Feature
 * Allow using ssh-key to push version while using token to publish to hvcs ([#419](https://github.com/relekang/python-semantic-release/issues/419)) ([`7b2dffa`](https://github.com/relekang/python-semantic-release/commit/7b2dffadf43c77d5e0eea307aefcee5c7744df5c))
diff --git a/action.sh b/action.sh
index ae7c1dc..cfd3e54 100755
--- a/action.sh
+++ b/action.sh
@@ -17,5 +17,8 @@ git config --global user.name "github-actions"
 git config --global user.email "action@github.com"
 
 # Run Semantic Release
-/semantic-release/.venv/bin/python -m semantic_release publish -v DEBUG \
-  -D commit_author="github-actions <action@github.com>"
+/semantic-release/.venv/bin/python \
+  -m semantic_release publish \
+  -v DEBUG \
+  -D commit_author="github-actions <action@github.com>" \
+  ${INPUT_ADDITIONAL_OPTIONS}
diff --git a/action.yml b/action.yml
index f7ce10e..bf30d47 100644
--- a/action.yml
+++ b/action.yml
@@ -3,6 +3,9 @@ name: 'Python Semantic Release'
 description: 'Automatic Semantic Versioning for Python projects'
 
 inputs:
+  additional_options:
+    description: 'Additional options for the publish command. Example: --noop'
+    required: false
   directory:
     description: 'Sub-directory to cd into before running semantic-release'
     default: '.'
diff --git a/docs/automatic-releases/github-actions.rst b/docs/automatic-releases/github-actions.rst
index e5e1af4..e9b9e6d 100644
--- a/docs/automatic-releases/github-actions.rst
+++ b/docs/automatic-releases/github-actions.rst
@@ -10,16 +10,18 @@ Inputs
 +--------------------------+----------------------------------------------------------------------------------------+
 | Input                    | Description                                                                            |
 +==========================+========================================================================================+
-| ``github_token``         | See :ref:`env-gh_token`. this is usually set to ``${{ secrets.GITHUB_TOKEN }}``. |
+| ``github_token``         | See :ref:`env-gh_token`. this is usually set to ``${{ secrets.GITHUB_TOKEN }}``.       |
 +--------------------------+----------------------------------------------------------------------------------------+
 | ``pypi_token``           | See :ref:`env-pypi_token`.                                                             |
 +--------------------------+----------------------------------------------------------------------------------------+
-| ``repository_username``  | See :ref:`env-repository_username`.                                                          |
+| ``repository_username``  | See :ref:`env-repository_username`.                                                    |
 +--------------------------+----------------------------------------------------------------------------------------+
-| ``repository_password``  | See :ref:`env-repository_password`.                                                          |
+| ``repository_password``  | See :ref:`env-repository_password`.                                                    |
 +--------------------------+----------------------------------------------------------------------------------------+
 | ``directory``            | A sub-directory to ``cd`` into before running. Defaults to the root of the repository. |
 +--------------------------+----------------------------------------------------------------------------------------+
+| ``additional_options``   | Additional :ref:`cmd-common-options` for the ``publish`` command. Example: ``--noop``  |
++--------------------------+----------------------------------------------------------------------------------------+
 
 Other options are taken from your regular configuration file.
 
diff --git a/docs/automatic-releases/index.rst b/docs/automatic-releases/index.rst
index b54eaa0..da3b417 100644
--- a/docs/automatic-releases/index.rst
+++ b/docs/automatic-releases/index.rst
@@ -75,8 +75,9 @@ Configuring distribution upload
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 In order to upload to an artifact repository, Python Semantic Release needs credentials to access
 the project. You will need to set the environment variables :ref:`env-repository_username` and
-:ref:`env-repository_password`. Use :ref:`config-repository_url` to set a custom repository url.
-As an alternative the repository and/or credentials can be configured using the ``~/.pypirc`` file.
+:ref:`env-repository_password`. Use :ref:`config-repository_url` or :ref:`env-repository_url` to
+set a custom repository url. As an alternative the repository and/or credentials can be configured
+using the ``~/.pypirc`` file.
 
 .. warning::
   Make sure to protect any environment variable containing secrets on your CI service.
diff --git a/docs/commands.rst b/docs/commands.rst
index 267a9fe..a14249f 100644
--- a/docs/commands.rst
+++ b/docs/commands.rst
@@ -55,6 +55,8 @@ Publish will do a sequence of things:
 
 Some of these steps may be disabled based on your configuration.
 
+.. _cmd-common-options:
+
 Common Options
 ~~~~~~~~~~~~~~
 
diff --git a/docs/envvars.rst b/docs/envvars.rst
index 58505a8..37486a3 100644
--- a/docs/envvars.rst
+++ b/docs/envvars.rst
@@ -91,8 +91,8 @@ when pushing tags, publishing releases etc.
 
 .. _env-repository:
 
-Artifact Repository Authentication
-==================================
+Artifact Repository
+===================
 
 .. _env-pypi_token:
 
@@ -150,3 +150,12 @@ Also used for token when using token authentication.
     output. This can break things, for example repository links.
 
   - Find more information on `how to obtain a token <https://pypi.org/help/#apitoken>`_.
+
+.. _env-repository_url:
+
+``REPOSITORY_URL``
+------------------
+Custom repository (package index) URL to upload the package to.
+Takes precedence over :ref:`config-repository_url`
+
+See :ref:`automatic-dist-upload` for more about uploads to custom repositories.
diff --git a/semantic_release/__init__.py b/semantic_release/__init__.py
index d6a8e95..dfbd397 100644
--- a/semantic_release/__init__.py
+++ b/semantic_release/__init__.py
@@ -1,6 +1,6 @@
 """Semantic Release
 """
-__version__ = "7.29.0"
+__version__ = "7.30.2"
 
 
 from .errors import UnknownCommitMessageStyleError  # noqa; noqa
diff --git a/semantic_release/cli.py b/semantic_release/cli.py
index df4a637..f4586bb 100644
--- a/semantic_release/cli.py
+++ b/semantic_release/cli.py
@@ -69,6 +69,7 @@ COMMON_OPTIONS = [
         "--patch", "force_level", flag_value="patch", help="Force patch version."
     ),
     click.option("--prerelease", is_flag=True, help="Creates a prerelease version."),
+    click.option("--prerelease-patch/--no-prerelease-patch", "prerelease_patch", default=True, show_default=True, help="whether or not prerelease always gets at least a patch-level bump"),
     click.option("--post", is_flag=True, help="Post changelog."),
     click.option("--retry", is_flag=True, help="Retry the same release, do not bump."),
     click.option(
@@ -95,7 +96,7 @@ def common_options(func):
     return func
 
 
-def print_version(*, current=False, force_level=None, prerelease=False, **kwargs):
+def print_version(*, current=False, force_level=None, prerelease=False, prerelease_patch=True, **kwargs):
     """
     Print the current or new version to standard output.
     """
@@ -115,9 +116,14 @@ def print_version(*, current=False, force_level=None, prerelease=False, **kwargs
     # Find what the new version number should be
     level_bump = evaluate_version_bump(current_version, force_level)
     new_version = get_new_version(
-        current_version, current_release_version, level_bump, prerelease
+        current_version, current_release_version, level_bump, prerelease, prerelease_patch
     )
-    if should_bump_version(current_version=current_version, new_version=new_version):
+    if should_bump_version(
+        current_version=current_version,
+        new_version=new_version,
+        current_release_version=current_release_version,
+        prerelease=prerelease,
+    ):
         print(new_version, end="")
         return True
 
@@ -125,7 +131,7 @@ def print_version(*, current=False, force_level=None, prerelease=False, **kwargs
     return False
 
 
-def version(*, retry=False, noop=False, force_level=None, prerelease=False, **kwargs):
+def version(*, retry=False, noop=False, force_level=None, prerelease=False, prerelease_patch=True, **kwargs):
     """
     Detect the new version according to git log and semver.
 
@@ -149,11 +155,16 @@ def version(*, retry=False, noop=False, force_level=None, prerelease=False, **kw
     # Find what the new version number should be
     level_bump = evaluate_version_bump(current_release_version, force_level)
     new_version = get_new_version(
-        current_version, current_release_version, level_bump, prerelease
+        current_version, current_release_version, level_bump, prerelease, prerelease_patch
     )
 
     if not should_bump_version(
-        current_version=current_version, new_version=new_version, retry=retry, noop=noop
+        current_version=current_version,
+        new_version=new_version,
+        current_release_version=current_release_version,
+        prerelease=prerelease,
+        retry=retry,
+        noop=noop,
     ):
         return False
 
@@ -166,9 +177,19 @@ def version(*, retry=False, noop=False, force_level=None, prerelease=False, **kw
     return True
 
 
-def should_bump_version(*, current_version, new_version, retry=False, noop=False):
+def should_bump_version(
+    *,
+    current_version,
+    current_release_version,
+    new_version,
+    prerelease,
+    retry=False,
+    noop=False,
+):
+    match_version = current_version if prerelease else current_release_version
+
     """Test whether the version should be bumped."""
-    if new_version == current_version and not retry:
+    if new_version == match_version and not retry:
         logger.info("No release will be made.")
         return False
 
@@ -255,7 +276,7 @@ def changelog(*, unreleased=False, noop=False, post=False, prerelease=False, **k
 
 
 def publish(
-    retry: bool = False, noop: bool = False, prerelease: bool = False, **kwargs
+    retry: bool = False, noop: bool = False, prerelease: bool = False, prerelease_patch = True,  **kwargs
 ):
     """Run the version task, then push to git and upload to an artifact repository / GitHub Releases."""
     current_version = get_current_version()
@@ -274,9 +295,11 @@ def publish(
         current_version = get_previous_release_version(current_version)
     else:
         # Calculate the new version
-        level_bump = evaluate_version_bump(current_version, kwargs.get("force_level"))
+        level_bump = evaluate_version_bump(
+            current_release_version, kwargs.get("force_level")
+        )
         new_version = get_new_version(
-            current_version, current_release_version, level_bump, prerelease
+            current_version, current_release_version, level_bump, prerelease, prerelease_patch
         )
 
     owner, name = get_repository_owner_and_name()
@@ -289,6 +312,8 @@ def publish(
     if should_bump_version(
         current_version=current_version,
         new_version=new_version,
+        current_release_version=current_release_version,
+        prerelease=prerelease,
         retry=retry,
         noop=noop,
     ):
diff --git a/semantic_release/defaults.cfg b/semantic_release/defaults.cfg
index 642c977..d49e598 100644
--- a/semantic_release/defaults.cfg
+++ b/semantic_release/defaults.cfg
@@ -36,6 +36,7 @@ pypi_token_var=PYPI_TOKEN
 pypi_user_var=PYPI_USERNAME
 repository_user_var=REPOSITORY_USERNAME
 repository_pass_var=REPOSITORY_PASSWORD
+repository_url_var=REPOSITORY_URL
 remove_dist=true
 tag_commit=true
 tag_format=v{version}
diff --git a/semantic_release/history/__init__.py b/semantic_release/history/__init__.py
index c418253..89360b3 100644
--- a/semantic_release/history/__init__.py
+++ b/semantic_release/history/__init__.py
@@ -25,12 +25,30 @@ from .parser_tag import parse_commit_message as tag_parser  # noqa isort:skip
 logger = logging.getLogger(__name__)
 
 
-prerelease_pattern = f"-{config.get('prerelease_tag')}\.\d+"
-version_pattern = f"(\d+\.\d+\.\d+({prerelease_pattern})?)"
-release_version_pattern = f"(\d+\.\d+\.\d+(?!{prerelease_pattern}))"
+def get_prerelease_pattern():
+    return f"-{config.get('prerelease_tag')}\.\d+"
 
-release_version_regex = rf"{release_version_pattern}"
-version_regex = rf"{version_pattern}"
+
+def get_pattern_with_commit_subject(pattern):
+    escaped_commit_subject = re.escape(config.get("commit_subject"))
+    return escaped_commit_subject.replace("\{version\}", pattern)
+
+
+def get_version_pattern():
+    prerelease_pattern = get_prerelease_pattern()
+    return f"(\d+\.\d+\.\d+({prerelease_pattern})?)"
+
+
+def get_release_version_pattern():
+    prerelease_pattern = get_prerelease_pattern()
+    return f"v?(\d+\.\d+\.\d+(?!.*{prerelease_pattern}))"
+
+
+def get_commit_release_version_pattern():
+    prerelease_pattern = get_prerelease_pattern()
+    return get_pattern_with_commit_subject(
+        f"v?(\d+\.\d+\.\d+(?!.*{prerelease_pattern}))"
+    )
 
 
 class VersionDeclaration(ABC):
@@ -53,7 +71,8 @@ class VersionDeclaration(ABC):
         variable name.
         """
         path, variable = config_str.split(":", 1)
-        pattern = rf'{variable} *[:=] *["\']{version_regex}["\']'
+        version_pattern = get_version_pattern()
+        pattern = rf'{variable} *[:=] *["\']{version_pattern}["\']'
         return PatternVersionDeclaration(path, pattern)
 
     @staticmethod
@@ -63,7 +82,7 @@ class VersionDeclaration(ABC):
         regular expression matching the version number.
         """
         path, pattern = config_str.split(":", 1)
-        pattern = pattern.format(version=version_regex)
+        pattern = pattern.format(version=get_version_pattern())
         return PatternVersionDeclaration(path, pattern)
 
     @abstractmethod
@@ -187,7 +206,7 @@ def get_current_version_by_tag() -> str:
 
     :return: A string with the version number or 0.0.0 on failure.
     """
-    version = get_last_version(pattern=version_pattern)
+    version = get_last_version(pattern=get_version_pattern())
     if version:
         return version
 
@@ -202,7 +221,7 @@ def get_current_release_version_by_tag() -> str:
 
     :return: A string with the version number or 0.0.0 on failure.
     """
-    version = get_last_version(pattern=release_version_pattern)
+    version = get_last_version(pattern=get_release_version_pattern())
     if version:
         return version
 
@@ -256,7 +275,7 @@ def get_current_release_version() -> str:
 
     :return: A string with the current version number
     """
-    if config.get("version_source") == "tag":
+    if config.get("version_source") in ["tag", "tag_only"]:
         return get_current_release_version_by_tag()
     else:
         return get_current_release_version_by_commits()
@@ -268,6 +287,7 @@ def get_new_version(
     current_release_version: str,
     level_bump: str,
     prerelease: bool = False,
+    prerelease_patch: bool = True
 ) -> str:
     """
     Calculate the next version based on the given bump level with semver.
@@ -290,17 +310,24 @@ def get_new_version(
         not current_version_info.prerelease
         and current_version_info.compare(current_release_version_info) != 0
     ):
-        raise ValueError()
+        raise ValueError(
+            f"Error: Current version {current_version} and current release version {current_release_version} do not match!"
+        )
 
     if level_bump:
         next_version_info = current_release_version_info.next_version(level_bump)
     elif prerelease:
-        # we do at least a patch for prereleases
-        next_version_info = current_release_version_info.next_version("patch")
+        if prerelease_patch:
+            # we do at least a patch for prereleases
+            next_version_info = current_release_version_info.next_version("patch")
+        elif current_version_info.compare(current_release_version_info) > 0:
+            next_version_info = current_version_info
+        else:
+            next_version_info = current_release_version_info
     else:
         next_version_info = current_release_version_info
 
-    if prerelease:
+    if prerelease and (level_bump or prerelease_patch):
         next_raw_version = next_version_info.to_tuple()[:3]
         current_raw_version = current_version_info.to_tuple()[:3]
 
@@ -331,6 +358,8 @@ def get_previous_version(version: str) -> Optional[str]:
     :param version: A string with the version number.
     :return: A string with the previous version number.
     """
+    version_pattern = get_version_pattern()
+
     found_version = False
     for commit_hash, commit_message in get_commit_log():
         logger.debug(f"Checking commit {commit_hash}")
@@ -340,7 +369,7 @@ def get_previous_version(version: str) -> Optional[str]:
             continue
 
         if found_version:
-            match = re.search(rf"{version_pattern}", commit_message)
+            match = re.search(version_pattern, commit_message)
             if match:
                 logger.debug(f"Version matches regex {commit_message}")
                 return match.group(1).strip()
@@ -358,6 +387,8 @@ def get_previous_release_version(version: str) -> Optional[str]:
     :param version: A string with the version number.
     :return: A string with the previous version number.
     """
+    release_version_pattern = get_commit_release_version_pattern()
+
     found_version = False
     for commit_hash, commit_message in get_commit_log():
         logger.debug(f"Checking commit {commit_hash}")
@@ -367,7 +398,7 @@ def get_previous_release_version(version: str) -> Optional[str]:
             continue
 
         if found_version:
-            match = re.search(rf"{release_version_pattern}", commit_message)
+            match = re.search(release_version_pattern, commit_message)
             if match:
                 logger.debug(f"Version matches regex {commit_message}")
                 return match.group(1).strip()
@@ -384,9 +415,11 @@ def get_current_release_version_by_commits() -> str:
 
     :return: A string with the current version number.
     """
+    release_version_re = re.compile(get_commit_release_version_pattern())
+
     for commit_hash, commit_message in get_commit_log():
         logger.debug(f"Checking commit {commit_hash}")
-        match = re.search(rf"{release_version_pattern}", commit_message)
+        match = release_version_re.match(commit_message)
         if match:
             logger.debug(f"Version matches regex {commit_message}")
             return match.group(1).strip()
diff --git a/semantic_release/hvcs.py b/semantic_release/hvcs.py
index 5cfc363..2f9ac6c 100644
--- a/semantic_release/hvcs.py
+++ b/semantic_release/hvcs.py
@@ -105,6 +105,7 @@ class Github(Base):
         hvcs_domain = config.get(
             "hvcs_domain", os.getenv("GITHUB_SERVER_URL", "").replace("https://", "")
         )
+
         domain = hvcs_domain if hvcs_domain else Github.DEFAULT_DOMAIN
         return domain
 
@@ -281,6 +282,31 @@ class Github(Base):
 
         return success
 
+    @classmethod
+    @LoggedFunction(logger)
+    def get_asset_upload_url(
+        cls, owner: str, repo: str, release_id: str
+    ) -> Optional[str]:
+        """Get the correct upload url for a release
+
+        https://docs.github.com/en/enterprise-server@3.5/rest/releases/releases#get-a-release
+
+        :param owner: The owner namespace of the repository
+        :param repo: The repository name
+        :param release_id: ID of the release to upload to
+
+        :return: URL found to upload for a release
+        """
+        try:
+            response = Github.session().get(
+                f"{Github.api_url()}/repos/{owner}/{repo}/releases/{release_id}"
+            )
+            return str(response.json().get("upload_url")).split("{")[0]
+        except HTTPError as e:
+            if e.response.status_code != 404:
+                logger.debug(f"Get release asset upload on Github has failed: {e}")
+            return None
+
     @classmethod
     @LoggedFunction(logger)
     def upload_asset(
@@ -298,7 +324,10 @@ class Github(Base):
 
         :return: The status of the request
         """
-        url = f"https://uploads.github.com/repos/{owner}/{repo}/releases/{release_id}/assets"
+        url = cls.get_asset_upload_url(owner, repo, release_id)
+        if not url:
+            logger.warning("Could not get release upload url")
+            return False
 
         content_type = mimetypes.guess_type(file, strict=False)[0]
         if not content_type:
diff --git a/semantic_release/repository.py b/semantic_release/repository.py
index ead2317..2e99fe2 100644
--- a/semantic_release/repository.py
+++ b/semantic_release/repository.py
@@ -109,7 +109,7 @@ class ArtifactRepo:
 
         Defaults to repository_name `pypi` when both are not set.
         """
-        repository_url = config.get("repository_url")
+        repository_url = get_env_var("repository_url_var") or config.get("repository_url")
         repository_name = config.get("repository")
 
         if repository_url:
diff --git a/semantic_release/vcs_helpers.py b/semantic_release/vcs_helpers.py
index cdfb1ae..a4abb2b 100644
--- a/semantic_release/vcs_helpers.py
+++ b/semantic_release/vcs_helpers.py
@@ -80,9 +80,9 @@ def get_last_version(pattern, skip_tags=None) -> Optional[str]:
         if i.name in skip_tags:
             continue
 
-        match = re.search(rf"{pattern}", i.name)
+        match = re.search(pattern, i.name)
         if match:
-            return match.group(0).strip()
+            return match.group(1).strip()
 
     return None
 
diff --git a/setup.py b/setup.py
index c0f8c11..49abaa7 100644
--- a/setup.py
+++ b/setup.py
@@ -50,6 +50,7 @@ setup(
         "tomlkit>=0.10.0,<0.11.0",
         "dotty-dict>=1.3.0,<2",
         "dataclasses==0.8; python_version < '3.7.0'",
+        "packaging",
     ],
     extras_require={
         "test": [
diff --git a/tox.ini b/tox.ini
index aa8c039..3f89438 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ setenv =
     TESTING = True
 deps = .[test]
 commands =
-    coverage run -p --source=semantic_release -m pytest -v tests
+    coverage run -p --source=semantic_release -m pytest -v {posargs:tests}
 
 [testenv:mypy]
 deps = .[mypy]


Test patch:
diff --git a/tests/history/test_version.py b/tests/history/test_version.py
index 2250cf2..463ce04 100644
--- a/tests/history/test_version.py
+++ b/tests/history/test_version.py
@@ -143,6 +143,18 @@ class TestGetCurrentReleaseVersionByCommits:
     def test_should_return_correct_version_from_prerelease(self):
         assert get_current_release_version_by_commits() == "0.9.0"
 
+    @mock.patch(
+        "semantic_release.history.get_commit_log",
+        lambda: [
+            ("211", "0.0.24-beta.10"),
+            ("13", "0.0.23"),
+            ("14", "0.0.21-beta.0"),
+            ("15", "0.0.19"),
+        ],
+    )
+    def test_should_return_correct_version_from_large_prerelease(self):
+        assert get_current_release_version_by_commits() == "0.0.23"
+
     @mock.patch(
         "semantic_release.history.get_commit_log",
         lambda: [("211", "7.28.0"), ("13", "7.27.0")],
@@ -150,6 +162,32 @@ class TestGetCurrentReleaseVersionByCommits:
     def test_should_return_correct_version_without_prerelease(self):
         assert get_current_release_version_by_commits() == "7.28.0"
 
+    @mock.patch(
+        "semantic_release.history.get_commit_log",
+        lambda: [
+            ("222", "chore(deps): bump random lib to 7.29.0"),
+            ("211", "7.28.0"),
+            ("13", "7.27.0"),
+        ],
+    )
+    def test_should_return_correct_version_ignoring_dependency_bumps(self):
+        assert get_current_release_version_by_commits() == "7.28.0"
+
+    @mock.patch(
+        "semantic_release.history.get_commit_log",
+        lambda: [
+            ("222", "7.29.0"),
+            ("211", "chore(release): 7.28.0"),
+            ("13", "7.27.0"),
+        ],
+    )
+    def test_should_return_correct_version_with_commit_subject(self):
+        with mock.patch(
+            "semantic_release.history.config.get",
+            wrapped_config_get(commit_subject="chore(release): {version}"),
+        ):
+            assert get_current_release_version_by_commits() == "7.28.0"
+
 
 class TestGetNewVersion:
     def test_major_bump(self):
@@ -178,16 +216,22 @@ class TestGetNewVersion:
         assert get_new_version("1.0.1-beta.1", "1.0.0", "major", True) == "2.0.0-beta.1"
         assert get_new_version("1.0.1-beta.1", "1.0.0", "minor", True) == "1.1.0-beta.1"
         assert get_new_version("1.0.1-beta.1", "1.0.0", "patch", True) == "1.0.1-beta.2"
+        assert get_new_version("1.0.1-beta.1", "1.0.0", "patch", True, False) == "1.0.1-beta.2"
+        assert get_new_version("1.0.1-beta.1", "1.0.0", None, True, False) == "1.0.1-beta.1"
 
         assert get_new_version("1.0.0", "1.0.0", None, True) == "1.0.1-beta.1"
         assert get_new_version("1.0.0", "1.0.0", "major", True) == "2.0.0-beta.1"
         assert get_new_version("1.0.0", "1.0.0", "minor", True) == "1.1.0-beta.1"
         assert get_new_version("1.0.0", "1.0.0", "patch", True) == "1.0.1-beta.1"
+        assert get_new_version("1.0.0", "1.0.0", "patch", True, False) == "1.0.1-beta.1"
+        assert get_new_version("1.0.0", "1.0.0", None, True, False) == "1.0.0"
 
         assert get_new_version("0.9.0-beta.1", "1.0.0", None, True) == "1.0.1-beta.1"
         assert get_new_version("0.9.0-beta.1", "1.0.0", "major", True) == "2.0.0-beta.1"
         assert get_new_version("0.9.0-beta.1", "1.0.0", "minor", True) == "1.1.0-beta.1"
         assert get_new_version("0.9.0-beta.1", "1.0.0", "patch", True) == "1.0.1-beta.1"
+        assert get_new_version("0.9.0-beta.1", "1.0.0", "patch", True, False) == "1.0.1-beta.1"
+        assert get_new_version("0.9.0-beta.1", "1.0.0", None, True, False) == "1.0.0"
 
         with pytest.raises(ValueError):
             get_new_version("0.9.0", "1.0.0", None, True)
@@ -449,6 +493,19 @@ class TestVersionPattern:
                 (Path("path2"), r'var2 *[:=] *["\'](\d+\.\d+\.\d+(-beta\.\d+)?)["\']'),
             ],
         ),
+        dict(
+            pyproject="""\
+                        [tool.semantic_release]
+                        version_variable = "path:__version__"
+                        prerelease_tag = "alpha"
+                        """,
+            patterns=[
+                (
+                    Path("path"),
+                    r'__version__ *[:=] *["\'](\d+\.\d+\.\d+(-alpha\.\d+)?)["\']',
+                ),
+            ],
+        ),
         dict(
             pyproject="""\
                         [tool.semantic_release]
diff --git a/tests/test_cli.py b/tests/test_cli.py
index feb4e1b..eea3a77 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -23,6 +23,7 @@ def test_main_should_call_correct_function(mocker, runner):
         post=False,
         force_level=None,
         prerelease=False,
+        prerelease_patch=True,
         retry=False,
         define=(),
     )
@@ -57,7 +58,7 @@ def test_version_by_commit_should_call_correct_functions(mocker):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False, True)
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_commit_new_version.assert_called_once_with("2.0.0")
     mock_tag_new_version.assert_called_once_with("2.0.0")
@@ -96,7 +97,7 @@ def test_version_by_tag_with_commit_version_number_should_call_correct_functions
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False, True)
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_commit_new_version.assert_called_once_with("2.0.0")
     mock_tag_new_version.assert_called_once_with("2.0.0")
@@ -135,7 +136,7 @@ def test_version_by_tag_only_with_commit_version_number_should_call_correct_func
     mock_current_version.assert_called_once_with()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False, True)
     assert not mock_set_new_version.called
     assert not mock_commit_new_version.called
     mock_tag_new_version.assert_called_once_with("2.0.0")
@@ -166,7 +167,7 @@ def test_version_by_tag_should_call_correct_functions(mocker):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False, True)
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_tag_new_version.assert_called_once_with("2.0.0")
 
@@ -196,7 +197,7 @@ def test_version_by_tag_only_should_call_correct_functions(mocker):
     mock_current_version.assert_called_once_with()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False, True)
     assert not mock_set_new_version.called
     mock_tag_new_version.assert_called_once_with("2.0.0")
 
@@ -227,7 +228,7 @@ def test_version_by_commit_without_tag_should_call_correct_functions(mocker):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", "major", False, True)
     mock_set_new_version.assert_called_once_with("2.0.0")
     mock_commit_new_version.assert_called_once_with("2.0.0")
     assert not mock_tag_new_version.called
@@ -241,6 +242,7 @@ def test_force_major(mocker, runner):
         post=False,
         force_level="major",
         prerelease=False,
+        prerelease_patch=True,
         retry=False,
         define=(),
     )
@@ -256,6 +258,7 @@ def test_force_minor(mocker, runner):
         post=False,
         force_level="minor",
         prerelease=False,
+        prerelease_patch=True,
         retry=False,
         define=(),
     )
@@ -271,6 +274,7 @@ def test_force_patch(mocker, runner):
         post=False,
         force_level="patch",
         prerelease=False,
+        prerelease_patch=True,
         retry=False,
         define=(),
     )
@@ -286,11 +290,25 @@ def test_retry(mocker, runner):
         post=False,
         force_level=None,
         prerelease=False,
+        prerelease_patch=True,
         retry=True,
         define=(),
     )
     assert result.exit_code == 0
 
+def test_prerelease_patch(mocker, runner):
+    mock_version = mocker.patch("semantic_release.cli.version")
+    result = runner.invoke(main, ["version", "--no-prerelease-patch"])
+    mock_version.assert_called_once_with(
+        noop=False,
+        post=False,
+        force_level=None,
+        prerelease=False,
+        prerelease_patch=False,
+        retry=False,
+        define=(),
+    )
+    assert result.exit_code == 0
 
 def test_noop_mode(mocker):
     mock_tag_new_version = mocker.patch("semantic_release.cli.tag_new_version")
@@ -312,6 +330,7 @@ def test_cli_print_version(mocker, runner):
         current=False,
         force_level=None,
         prerelease=False,
+        prerelease_patch=True,
         noop=False,
         post=False,
         retry=False,
@@ -327,6 +346,7 @@ def test_cli_print_version_force_major(mocker, runner):
         current=False,
         force_level="major",
         prerelease=False,
+        prerelease_patch=True,
         noop=False,
         post=False,
         retry=False,
@@ -342,6 +362,7 @@ def test_cli_print_version_prerelease(mocker, runner):
         current=False,
         force_level=None,
         prerelease=True,
+        prerelease_patch=True,
         noop=False,
         post=False,
         retry=False,
@@ -357,6 +378,7 @@ def test_cli_print_version_current(mocker, runner):
         current=True,
         force_level=None,
         prerelease=False,
+        prerelease_patch=True,
         noop=False,
         post=False,
         retry=False,
@@ -372,6 +394,7 @@ def test_cli_print_version_next(mocker, runner):
         current=False,
         force_level=None,
         prerelease=False,
+        prerelease_patch=True,
         noop=False,
         post=False,
         retry=False,
@@ -402,7 +425,7 @@ def test_print_version_no_change(mocker, runner, capsys):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", None, False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", None, False, True)
 
 
 def test_print_version_change(mocker, runner, capsys):
@@ -446,6 +469,26 @@ def test_print_version_change_prerelease(mocker, runner, capsys):
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
 
+def test_print_version_change_prerelease_no_patch(mocker, runner, capsys):
+    mock_current_version = mocker.patch(
+        "semantic_release.cli.get_current_version", return_value="1.2.3"
+    )
+    mock_current_release_version = mocker.patch(
+        "semantic_release.cli.get_current_release_version", return_value="1.2.3"
+    )
+    mock_evaluate_bump = mocker.patch(
+        "semantic_release.cli.evaluate_version_bump", return_value=None
+    )
+
+    print_version(prerelease=True, prerelease_patch=False)
+    outerr = capsys.readouterr()
+    assert outerr.out == ""
+    assert outerr.err == "No release will be made.\n"
+
+    mock_current_version.assert_called_once()
+    mock_current_release_version.assert_called_once()
+    mock_evaluate_bump.assert_called_once_with("1.2.3", None)
+
 
 def test_print_version_change_prerelease_bump(mocker, runner, capsys):
     mock_current_version = mocker.patch(
@@ -532,7 +575,7 @@ def test_version_no_change(mocker, runner):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", None)
-    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", None, False)
+    mock_new_version.assert_called_once_with("1.2.3", "1.2.3", None, False, True)
     assert not mock_set_new_version.called
     assert not mock_commit_new_version.called
     assert not mock_tag_new_version.called
@@ -664,7 +707,7 @@ def test_version_retry(mocker):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate_bump.assert_called_once_with("1.2.3", False)
-    mock_get_new.assert_called_once_with("1.2.3", "1.2.3", "patch", False)
+    mock_get_new.assert_called_once_with("1.2.3", "1.2.3", "patch", False, True)
 
 
 def test_publish_should_not_run_pre_commit_by_default(mocker):
@@ -967,7 +1010,12 @@ def test_publish_retry_version_fail(mocker):
     mock_ci_check.assert_called()
     mock_checkout.assert_called_once_with("my_branch")
     mock_should_bump_version.assert_called_once_with(
-        current_version="previous", new_version="current", noop=False, retry=True
+        current_version="previous",
+        new_version="current",
+        current_release_version="1.2.3",
+        prerelease=False,
+        noop=False,
+        retry=True,
     )
 
 
@@ -1017,7 +1065,12 @@ def test_publish_bad_token(mocker):
     mock_ci_check.assert_called()
     mock_checkout.assert_called_once_with("my_branch")
     mock_should_bump_version.assert_called_once_with(
-        current_version="previous", new_version="current", noop=False, retry=True
+        current_version="previous",
+        new_version="current",
+        current_release_version="1.2.3",
+        prerelease=False,
+        noop=False,
+        retry=True,
     )
     mock_get_token.assert_called()
     mock_get_domain.assert_called()
@@ -1091,12 +1144,17 @@ def test_publish_giterror_when_posting(mocker):
     mock_current_version.assert_called_once()
     mock_current_release_version.assert_called_once()
     mock_evaluate.assert_called_once_with("1.2.3", False)
-    mock_get_new.assert_called_once_with("1.2.3", "1.2.3", "patch", False)
+    mock_get_new.assert_called_once_with("1.2.3", "1.2.3", "patch", False, True)
     mock_get_owner_name.assert_called_once_with()
     mock_ci_check.assert_called()
     mock_checkout.assert_called_once_with("my_branch")
     mock_should_bump_version.assert_called_once_with(
-        current_version="1.2.3", new_version="new", noop=False, retry=False
+        current_version="1.2.3",
+        new_version="new",
+        current_release_version="1.2.3",
+        prerelease=False,
+        noop=False,
+        retry=False,
     )
     mock_update_changelog_file.assert_called_once_with("new", "super md changelog")
     mock_bump_version.assert_called_once_with("new", "patch")
@@ -1131,6 +1189,7 @@ def test_changelog_should_call_functions(mocker, runner):
         post=False,
         force_level=None,
         prerelease=False,
+        prerelease_patch=True,
         retry=False,
         unreleased=False,
         define=(),
diff --git a/tests/test_hvcs.py b/tests/test_hvcs.py
index c806173..939a299 100644
--- a/tests/test_hvcs.py
+++ b/tests/test_hvcs.py
@@ -373,6 +373,9 @@ class GithubReleaseTests(TestCase):
         "https://uploads.github.com/repos/relekang/rmoq/releases/1/assets"
         "?name=testupload.md"
     )
+    upload_url = (
+        "https://uploads.github.com/repos/relekang/rmoq/releases/1/assets{?name,label}"
+    )
 
     @responses.activate
     @mock.patch("semantic_release.hvcs.Github.token", return_value="super-token")
@@ -483,6 +486,15 @@ class GithubReleaseTests(TestCase):
         with open(dummy_file_path, "w") as dummy_file:
             dummy_file.write(dummy_content)
 
+        required_payload = {"upload_url": self.upload_url}
+        responses.add(
+            responses.GET,
+            self.edit_url,
+            status=200,
+            body=json.dumps(required_payload),
+            content_type="application/json",
+        )
+
         def request_callback(request):
             self.assertEqual(request.body.decode().replace("\r\n", "\n"), dummy_content)
             self.assertEqual(request.url, self.asset_url_params)
@@ -514,6 +526,15 @@ class GithubReleaseTests(TestCase):
         with open(dummy_file_path, "w") as dummy_file:
             dummy_file.write(dummy_content)
 
+        required_payload = {"upload_url": self.upload_url}
+        responses.add(
+            responses.GET,
+            self.edit_url,
+            status=200,
+            body=json.dumps(required_payload),
+            content_type="application/json",
+        )
+
         def request_callback(request):
             self.assertEqual(request.body.decode().replace("\r\n", "\n"), dummy_content)
             self.assertEqual(request.url, self.asset_no_extension_url_params)
@@ -545,6 +566,15 @@ class GithubReleaseTests(TestCase):
         with open(dummy_file_path, "w") as dummy_file:
             dummy_file.write(dummy_content)
 
+        required_payload = {"upload_url": self.upload_url}
+        responses.add(
+            responses.GET,
+            self.edit_url,
+            status=200,
+            body=json.dumps(required_payload),
+            content_type="application/json",
+        )
+
         def request_callback(request):
             self.assertEqual(request.body.decode().replace("\r\n", "\n"), dummy_content)
             self.assertEqual(request.url, self.dist_asset_url_params)
diff --git a/tests/test_repository.py b/tests/test_repository.py
index 0a4ff43..d48e080 100644
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -10,197 +10,212 @@ from semantic_release.repository import ArtifactRepo
 from tests import wrapped_config_get
 
 
-class RepositoryTests(TestCase):
-    @pytest.fixture(autouse=True)
-    def inject_fixtures(self, caplog):
-        self._caplog = caplog
-
-    def test_upload_enabled(self):
-        self.assertTrue(ArtifactRepo.upload_enabled())
-
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(upload_to_repository=False),
+def test_upload_enabled():
+    assert ArtifactRepo.upload_enabled()
+
+
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(upload_to_repository=False),
+)
+def test_upload_enabled_with_upload_to_repository_false():
+    assert not ArtifactRepo.upload_enabled()
+
+
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(upload_to_pypi=False),
+)
+def test_upload_enabled_with_upload_to_pypi_false():
+    assert not ArtifactRepo.upload_enabled()
+
+
+@mock.patch.dict(
+    "os.environ",
+    {"PYPI_USERNAME": "pypi-username", "PYPI_PASSWORD": "pypi-password"},
+)
+def test_repo_with_pypi_credentials():
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.username == "pypi-username"
+    assert repo.password == "pypi-password"
+
+
+@mock.patch.dict(
+    "os.environ",
+    {
+        "REPOSITORY_USERNAME": "repo-username",
+        "REPOSITORY_PASSWORD": "repo-password",
+    },
+)
+def test_repo_with_repository_credentials():
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.username == "repo-username"
+    assert repo.password == "repo-password"
+
+
+@mock.patch.dict("os.environ", {"REPOSITORY_PASSWORD": "repo-password"})
+def test_repo_with_repository_password_only():
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.username == "__token__"
+    assert repo.password == "repo-password"
+
+
+@mock.patch("semantic_release.repository.Path.exists", return_value=True)
+def test_repo_with_pypirc_file(mock_path_exists):
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.username is None
+    assert repo.password is None
+
+
+@mock.patch("semantic_release.repository.Path.exists", return_value=False)
+def test_repo_without_pypirc_file(mock_path_exists):
+    with pytest.raises(ImproperConfigurationError) as cm:
+        ArtifactRepo(Path("dist"))
+    assert str(cm.value) == "Missing credentials for uploading to artifact repository"
+
+
+@mock.patch.dict(
+    "os.environ",
+    {
+        "PYPI_TOKEN": "pypi-token",
+        "PYPI_USERNAME": "pypi-username",
+        "PYPI_PASSWORD": "pypi-password",
+        "REPOSITORY_USERNAME": "repo-username",
+        "REPOSITORY_PASSWORD": "repo-password",
+    },
+)
+def test_repo_prefers_repository_over_pypi():
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.username == "repo-username"
+    assert repo.password == "repo-password"
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+def test_repo_with_custom_dist_path(mock_handle_creds):
+    repo = ArtifactRepo(Path("custom-dist"))
+    assert repo.dists == [os.path.join("custom-dist", "*")]
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(dist_glob_patterns="*.tar.gz,*.whl"),
+)
+def test_repo_with_custom_dist_globs(mock_handle_creds):
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.dists == [
+        os.path.join("dist", "*.tar.gz"),
+        os.path.join("dist", "*.whl"),
+    ]
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(upload_to_pypi_glob_patterns="*.tar.gz,*.whl"),
+)
+def test_repo_with_custom_pypi_globs(mock_handle_creds):
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.dists == [
+        os.path.join("dist", "*.tar.gz"),
+        os.path.join("dist", "*.whl"),
+    ]
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(repository="testpypi"),
+)
+def test_repo_with_repo_name_testpypi(mock_handle_creds):
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.repository_name == "testpypi"
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(repository="invalid"),
+)
+def test_raises_error_when_repo_name_invalid(mock_handle_creds):
+    repo = ArtifactRepo(Path("dist"))
+    with pytest.raises(
+        ImproperConfigurationError, match="Upload to artifact repository has failed"
+    ):
+        repo.upload(noop=False, verbose=False, skip_existing=False)
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(repository_url="https://custom-repo"),
+)
+def test_repo_with_custom_repo_url(mock_handle_creds):
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.repository_url == "https://custom-repo"
+
+
+@mock.patch.dict("os.environ", {"REPOSITORY_URL": "https://custom-repo"})
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+def test_repo_with_custom_repo_url_from_env(mock_handle_creds):
+    repo = ArtifactRepo(Path("dist"))
+    assert repo.repository_url == "https://custom-repo"
+
+
+@mock.patch("semantic_release.repository.twine_upload")
+@mock.patch(
+    "semantic_release.repository.config.get",
+    wrapped_config_get(
+        dist_glob_patterns="*.tar.gz,*.whl", repository_url="https://custom-repo"
+    ),
+)
+@mock.patch.dict(
+    "os.environ",
+    {
+        "REPOSITORY_USERNAME": "repo-username",
+        "REPOSITORY_PASSWORD": "repo-password",
+    },
+)
+def test_upload_with_custom_settings(mock_upload):
+    repo = ArtifactRepo(Path("custom-dist"))
+    repo.upload(
+        noop=False, verbose=True, skip_existing=True, comment="distribution comment"
     )
-    def test_upload_enabled_with_upload_to_repository_false(self):
-        self.assertFalse(ArtifactRepo.upload_enabled())
-
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(upload_to_pypi=False),
+    settings = mock_upload.call_args[1]["upload_settings"]
+    assert isinstance(settings.auth, auth.Private)
+    assert settings.comment == "distribution comment"
+    assert settings.username == "repo-username"
+    assert settings.password == "repo-password"
+    assert settings.repository_config["repository"] == "https://custom-repo"
+    assert settings.skip_existing
+    assert settings.verbose
+    assert mock_upload.called
+
+
+@mock.patch.object(ArtifactRepo, "_handle_credentials_init")
+@mock.patch("semantic_release.repository.twine_upload")
+def test_upload_with_noop(mock_upload, mock_handle_creds):
+    ArtifactRepo(Path("dist")).upload(noop=True, verbose=False, skip_existing=False)
+    assert not mock_upload.called
+
+
+@mock.patch.dict("os.environ", {"PYPI_TOKEN": "pypi-token"})
+def test_repo_with_token_only(caplog):
+    repo = ArtifactRepo(Path("dist"))
+    assert (
+        "Providing only password or token without username is deprecated"
+        in caplog.messages
     )
-    def test_upload_enabled_with_upload_to_pypi_false(self):
-        self.assertFalse(ArtifactRepo.upload_enabled())
+    assert repo.username == "__token__"
+    assert repo.password == "pypi-token"
 
-    @mock.patch.dict(
-        "os.environ",
-        {"PYPI_USERNAME": "pypi-username", "PYPI_PASSWORD": "pypi-password"},
-    )
-    def test_repo_with_pypi_credentials(self):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(repo.username, "pypi-username")
-        self.assertEqual(repo.password, "pypi-password")
-
-    @mock.patch.dict(
-        "os.environ",
-        {
-            "REPOSITORY_USERNAME": "repo-username",
-            "REPOSITORY_PASSWORD": "repo-password",
-        },
-    )
-    def test_repo_with_repository_credentials(self):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(repo.username, "repo-username")
-        self.assertEqual(repo.password, "repo-password")
-
-    @mock.patch.dict("os.environ", {"PYPI_TOKEN": "pypi-token"})
-    def test_repo_with_token_only(self):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertIn(
-            "Providing only password or token without username is deprecated",
-            self._caplog.messages,
-        )
-        self.assertEqual(repo.username, "__token__")
-        self.assertEqual(repo.password, "pypi-token")
-
-    @mock.patch.dict("os.environ", {"PYPI_PASSWORD": "pypi-password"})
-    def test_repo_with_pypi_password_only(self):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertIn(
-            "Providing only password or token without username is deprecated",
-            self._caplog.messages,
-        )
-        self.assertEqual(repo.username, "__token__")
-        self.assertEqual(repo.password, "pypi-password")
-
-    @mock.patch.dict("os.environ", {"REPOSITORY_PASSWORD": "repo-password"})
-    def test_repo_with_repository_password_only(self):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(repo.username, "__token__")
-        self.assertEqual(repo.password, "repo-password")
-
-    @mock.patch("semantic_release.repository.Path.exists", return_value=True)
-    def test_repo_with_pypirc_file(self, mock_path_exists):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertIsNone(repo.username)
-        self.assertIsNone(repo.password)
-
-    @mock.patch("semantic_release.repository.Path.exists", return_value=False)
-    def test_repo_without_pypirc_file(self, mock_path_exists):
-        with self.assertRaises(ImproperConfigurationError) as cm:
-            ArtifactRepo(Path("dist"))
-        self.assertEqual(
-            str(cm.exception),
-            "Missing credentials for uploading to artifact repository",
-        )
-
-    @mock.patch.dict(
-        "os.environ",
-        {
-            "PYPI_TOKEN": "pypi-token",
-            "PYPI_USERNAME": "pypi-username",
-            "PYPI_PASSWORD": "pypi-password",
-            "REPOSITORY_USERNAME": "repo-username",
-            "REPOSITORY_PASSWORD": "repo-password",
-        },
-    )
-    def test_repo_prefers_repository_over_pypi(self):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(repo.username, "repo-username")
-        self.assertEqual(repo.password, "repo-password")
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    def test_repo_with_custom_dist_path(self, mock_handle_creds):
-        repo = ArtifactRepo(Path("custom-dist"))
-        self.assertEqual(repo.dists, [os.path.join("custom-dist", "*")])
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(dist_glob_patterns="*.tar.gz,*.whl"),
-    )
-    def test_repo_with_custom_dist_globs(self, mock_handle_creds):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(
-            repo.dists,
-            [os.path.join("dist", "*.tar.gz"), os.path.join("dist", "*.whl")],
-        )
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(upload_to_pypi_glob_patterns="*.tar.gz,*.whl"),
-    )
-    def test_repo_with_custom_pypi_globs(self, mock_handle_creds):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(
-            repo.dists,
-            [os.path.join("dist", "*.tar.gz"), os.path.join("dist", "*.whl")],
-        )
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(repository="testpypi"),
-    )
-    def test_repo_with_repo_name_testpypi(self, mock_handle_creds):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(repo.repository_name, "testpypi")
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(repository="invalid"),
-    )
-    def test_raises_error_when_repo_name_invalid(self, mock_handle_creds):
-        repo = ArtifactRepo(Path("dist"))
-        with self.assertRaises(
-            ImproperConfigurationError, msg="Upload to artifact repository has failed"
-        ):
-            repo.upload(noop=False, verbose=False, skip_existing=False)
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(repository_url="https://custom-repo"),
-    )
-    def test_repo_with_custom_repo_url(self, mock_handle_creds):
-        repo = ArtifactRepo(Path("dist"))
-        self.assertEqual(repo.repository_url, "https://custom-repo")
-
-    @mock.patch("semantic_release.repository.twine_upload")
-    @mock.patch(
-        "semantic_release.repository.config.get",
-        wrapped_config_get(
-            dist_glob_patterns="*.tar.gz,*.whl", repository_url="https://custom-repo"
-        ),
-    )
-    @mock.patch.dict(
-        "os.environ",
-        {
-            "REPOSITORY_USERNAME": "repo-username",
-            "REPOSITORY_PASSWORD": "repo-password",
-        },
+
+@mock.patch.dict("os.environ", {"PYPI_PASSWORD": "pypi-password"})
+def test_repo_with_pypi_password_only(caplog):
+    repo = ArtifactRepo(Path("dist"))
+    assert (
+        "Providing only password or token without username is deprecated"
+        in caplog.messages
     )
-    def test_upload_with_custom_settings(self, mock_upload):
-        repo = ArtifactRepo(Path("custom-dist"))
-        repo.upload(
-            noop=False, verbose=True, skip_existing=True, comment="distribution comment"
-        )
-        settings = mock_upload.call_args[1]["upload_settings"]
-        self.assertIsInstance(settings.auth, auth.Private)
-        self.assertEqual(settings.comment, "distribution comment")
-        self.assertEqual(settings.username, "repo-username")
-        self.assertEqual(settings.password, "repo-password")
-        self.assertEqual(
-            settings.repository_config["repository"], "https://custom-repo"
-        )
-        self.assertTrue(settings.skip_existing)
-        self.assertTrue(settings.verbose)
-        self.assertTrue(mock_upload.called)
-
-    @mock.patch.object(ArtifactRepo, "_handle_credentials_init")
-    @mock.patch("semantic_release.repository.twine_upload")
-    def test_upload_with_noop(self, mock_upload, mock_handle_creds):
-        ArtifactRepo(Path("dist")).upload(noop=True, verbose=False, skip_existing=False)
-        self.assertFalse(mock_upload.called)
+    assert repo.username == "__token__"
+    assert repo.password == "pypi-password"
diff --git a/tests/test_vcs_helpers.py b/tests/test_vcs_helpers.py
index 5a583a8..5117b07 100644
--- a/tests/test_vcs_helpers.py
+++ b/tests/test_vcs_helpers.py
@@ -6,7 +6,7 @@ import pytest
 from git import GitCommandError, Repo, TagObject
 
 from semantic_release.errors import GitError, HvcsRepoParseError
-from semantic_release.history import release_version_pattern, version_pattern
+from semantic_release.history import get_release_version_pattern, get_version_pattern
 from semantic_release.vcs_helpers import (
     check_repo,
     checkout,
@@ -359,9 +359,9 @@ def test_checkout_should_checkout_correct_branch(mock_git):
 @pytest.mark.parametrize(
     "pattern, skip_tags,expected_result",
     [
-        ("\d+.\d+.\d+", None, "2.0.0"),
-        ("\d+.\d+.\d+", ["v2.0.0"], "1.1.0"),
-        ("\d+.\d+.\d+", ["v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"], None),
+        ("(\d+.\d+.\d+)", None, "2.0.0"),
+        ("(\d+.\d+.\d+)", ["v2.0.0"], "1.1.0"),
+        ("(\d+.\d+.\d+)", ["v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"], None),
     ],
 )
 def test_get_last_version(pattern, skip_tags, expected_result):
@@ -396,22 +396,22 @@ def test_get_last_version(pattern, skip_tags, expected_result):
 
 
 @pytest.mark.parametrize(
-    "pattern, skip_tags,expected_result",
+    "get_pattern, skip_tags,expected_result",
     [
-        (version_pattern, None, "2.1.0-beta.0"),
-        (version_pattern, ["v2.1.0-beta.0"], "2.0.0"),
-        (version_pattern, ["v2.1.0-beta.0", "v2.0.0-beta.0", "v2.0.0"], "1.1.0"),
+        (get_version_pattern, None, "2.1.0-beta.0"),
+        (get_version_pattern, ["v2.1.0-beta.0"], "2.0.0"),
+        (get_version_pattern, ["v2.1.0-beta.0", "v2.0.0-beta.0", "v2.0.0"], "1.1.0"),
         (
-            version_pattern,
+            get_version_pattern,
             ["v2.1.0-beta.0", "v2.0.0-beta.0", "v0.1.0", "v1.0.0", "v1.1.0", "v2.0.0"],
             None,
         ),
-        (release_version_pattern, None, "2.0.0"),
-        (release_version_pattern, ["v2.0.0"], "1.1.0"),
-        (release_version_pattern, ["v2.0.0", "v1.1.0", "v1.0.0", "v0.1.0"], None),
+        (get_release_version_pattern, None, "2.0.0"),
+        (get_release_version_pattern, ["v2.0.0"], "1.1.0"),
+        (get_release_version_pattern, ["v2.0.0", "v1.1.0", "v1.0.0", "v0.1.0"], None),
     ],
 )
-def test_get_last_version_with_real_pattern(pattern, skip_tags, expected_result):
+def test_get_last_version_with_real_pattern(get_pattern, skip_tags, expected_result):
     # TODO: add some prerelease tags
     class FakeCommit:
         def __init__(self, com_date):
@@ -442,7 +442,7 @@ def test_get_last_version_with_real_pattern(pattern, skip_tags, expected_result)
             FakeTag("v1.0.0", "bbbbbbbbbbbbbbbbbbbb", 2, False),
         ]
     )
-    assert expected_result == get_last_version(pattern, skip_tags)
+    assert expected_result == get_last_version(get_pattern(), skip_tags)
 
 
 def test_update_changelog_file_ok(mock_git, mocker):