Repository: relekang/python-semantic-release
Pull request #462 (2022-06-30 19:25:19)

feat: add prerelease-patch and no-prerelease-patch flags for whether …
…to auto-bump prereleases with a default of at least a patch


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c561480..061aeb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,34 @@
 
 <!--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))
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/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/semantic_release/__init__.py b/semantic_release/__init__.py
index a028d18..dfbd397 100644
--- a/semantic_release/__init__.py
+++ b/semantic_release/__init__.py
@@ -1,6 +1,6 @@
 """Semantic Release
 """
-__version__ = "7.29.4"
+__version__ = "7.30.2"
 
 
 from .errors import UnknownCommitMessageStyleError  # noqa; noqa
diff --git a/semantic_release/cli.py b/semantic_release/cli.py
index fcdf614..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,7 +116,7 @@ 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,
@@ -130,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.
 
@@ -154,7 +155,7 @@ 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(
@@ -275,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()
@@ -294,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()
diff --git a/semantic_release/history/__init__.py b/semantic_release/history/__init__.py
index 4a47c8b..fa0613d 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
 
@@ -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.
@@ -297,12 +317,17 @@ def get_new_version(
     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]
 
@@ -333,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}")
@@ -342,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()
@@ -360,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}")
@@ -369,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()
@@ -386,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/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": [


Test patch:
diff --git a/tests/history/test_version.py b/tests/history/test_version.py
index f62b2d5..463ce04 100644
--- a/tests/history/test_version.py
+++ b/tests/history/test_version.py
@@ -162,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):
@@ -190,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)
@@ -461,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 b71a49f..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):
@@ -1101,7 +1144,7 @@ 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")
@@ -1146,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_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):