Skip to content

Commit

Permalink
feat: add prerelease functionality (#413)
Browse files Browse the repository at this point in the history
* chore: add initial todos
* feat: add prerelease tag option
* feat: add prerelease cli flag
* feat: omit_pattern for previouse and current version getters
* feat: print_version with prerelease bump
* feat: make print_version prerelease ready
* feat: move prerelease determination to get_new_version
* test: improve get_last_version test
* docs: added basic infos about prereleases
* feat: add prerelease flag to version and publish
* feat: remove leftover todos

Co-authored-by: Mario Jäckle <m.jaeckle@careerpartner.eu>
  • Loading branch information
jacksbox and Mario Jäckle committed Mar 7, 2022
1 parent be7708d commit 7064265
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 84 deletions.
6 changes: 6 additions & 0 deletions docs/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ Force a minor release, ignoring the version bump determined from commit messages

Force a major release, ignoring the version bump determined from commit messages.

``--prerelease``
...........

Makes the next release a prerelease, version bumps are still determined or can be forced,
but the `prerelease_tag` (see :ref:`config-prerelease_tag`) will be appended to version number.

``--noop``
..........

Expand Down
27 changes: 19 additions & 8 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ The file and variable name of where the version number is stored, for example::

semantic_release/__init__.py:__version__

You can specify multiple version variables (i.e. in different files) by
You can specify multiple version variables (i.e. in different files) by
providing comma-separated list of such strings::

semantic_release/__init__.py:__version__,docs/conf.py:version

In ``pyproject.toml`` specifically, you can also use the TOML list syntax to
In ``pyproject.toml`` specifically, you can also use the TOML list syntax to
specify multiple versions:

.. code-block:: toml
Expand Down Expand Up @@ -67,14 +67,14 @@ identified using an arbitrary regular expression::

README.rst:VERSION (\d+\.\d+\.\d+)

The regular expression must contain a parenthesized group that matches the
version number itself. Anything outside that group is just context. For
example, the above specifies that there is a version number in ``README.rst``
The regular expression must contain a parenthesized group that matches the
version number itself. Anything outside that group is just context. For
example, the above specifies that there is a version number in ``README.rst``
preceded by the string "VERSION".

If the pattern contains the string ``{version}``, it will be replaced with the
regular expression used internally by ``python-semantic-release`` to match
semantic version numbers. So the above example would probably be better
If the pattern contains the string ``{version}``, it will be replaced with the
regular expression used internally by ``python-semantic-release`` to match
semantic version numbers. So the above example would probably be better
written as::

README.rst:VERSION {version}
Expand All @@ -95,6 +95,17 @@ The way we get and set the new version. Can be `commit` or `tag`.

Default: `commit`

.. _config-prerelease_tag:

``prerelease_tag``
------------------
Defined the prerelease marker appended to the version when doing a prerelease.

- The format of a prerelease version will be `{tag_format}-{prerelease_tag}.<prerelease_number>`,
e.g. `1.0.0-beta.0` or `1.1.0-beta.1`

Default: `beta`

.. _config-tag_commit:

``tag_commit``
Expand Down
26 changes: 16 additions & 10 deletions semantic_release/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
get_current_version,
get_new_version,
get_previous_version,
set_new_version,
set_new_version
)
from .history.logs import generate_changelog
from .hvcs import (
Expand Down Expand Up @@ -66,8 +66,12 @@
click.option(
"--patch", "force_level", flag_value="patch", help="Force patch version."
),
click.option(
"--prerelease", is_flag=True, help="Creates a prerelease version."
),
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("--retry", is_flag=True,
help="Retry the same release, do not bump."),
click.option(
"--noop",
is_flag=True,
Expand All @@ -92,7 +96,7 @@ def common_options(func):
return func


def print_version(*, current=False, force_level=None, **kwargs):
def print_version(*, current=False, force_level=None, prerelease=False, **kwargs):
"""
Print the current or new version to standard output.
"""
Expand All @@ -107,7 +111,7 @@ def print_version(*, current=False, force_level=None, **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, level_bump)
new_version = get_new_version(current_version, level_bump, prerelease)
if should_bump_version(current_version=current_version, new_version=new_version):
print(new_version, end="")
return True
Expand All @@ -116,7 +120,7 @@ def print_version(*, current=False, force_level=None, **kwargs):
return False


def version(*, retry=False, noop=False, force_level=None, **kwargs):
def version(*, retry=False, noop=False, force_level=None, prerelease=False, **kwargs):
"""
Detect the new version according to git log and semver.
Expand All @@ -136,7 +140,7 @@ def version(*, retry=False, noop=False, force_level=None, **kwargs):
return False
# Find what the new version number should be
level_bump = evaluate_version_bump(current_version, force_level)
new_version = get_new_version(current_version, level_bump)
new_version = get_new_version(current_version, level_bump, prerelease)

if not should_bump_version(
current_version=current_version, new_version=new_version, retry=retry, noop=noop
Expand Down Expand Up @@ -228,13 +232,14 @@ def changelog(*, unreleased=False, noop=False, post=False, **kwargs):
owner,
name,
current_version,
markdown_changelog(owner, name, current_version, log, header=False),
markdown_changelog(
owner, name, current_version, log, header=False),
)
else:
logger.error("Missing token: cannot post changelog to HVCS")


def publish(retry: bool = False, noop: bool = False, **kwargs):
def publish(retry: bool = False, noop: bool = False, prerelease=False, **kwargs):
"""Run the version task, then push to git and upload to an artifact repository / GitHub Releases."""
current_version = get_current_version()

Expand All @@ -248,8 +253,9 @@ def publish(retry: bool = False, noop: bool = False, **kwargs):
current_version = get_previous_version(current_version)
else:
# Calculate the new version
level_bump = evaluate_version_bump(current_version, kwargs.get("force_level"))
new_version = get_new_version(current_version, level_bump)
level_bump = evaluate_version_bump(
current_version, kwargs.get("force_level"))
new_version = get_new_version(current_version, level_bump, prerelease)

owner, name = get_repository_owner_and_name()

Expand Down
1 change: 1 addition & 0 deletions semantic_release/defaults.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ upload_to_repository=true
upload_to_pypi=true
upload_to_release=true
version_source=commit
prerelease_tag=beta
50 changes: 38 additions & 12 deletions semantic_release/history/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,18 @@ def swap_version(m):
self.path.write_text(new_content)


def get_prerelease_pattern() -> str:
return "-" + config.get("prerelease_tag") + "."


@LoggedFunction(logger)
def get_current_version_by_tag() -> str:
def get_current_version_by_tag(omit_pattern=None) -> str:
"""
Find the current version of the package in the current working directory using git tags.
:return: A string with the version number or 0.0.0 on failure.
"""
version = get_last_version()
version = get_last_version(omit_pattern=omit_pattern)
if version:
return version

Expand All @@ -192,7 +196,7 @@ def get_current_version_by_tag() -> str:


@LoggedFunction(logger)
def get_current_version_by_config_file() -> str:
def get_current_version_by_config_file(omit_pattern=None) -> str:
"""
Get current version from the version variable defined in the configuration.
Expand All @@ -216,35 +220,55 @@ def get_current_version_by_config_file() -> str:
return version


def get_current_version() -> str:
def get_current_version(prerelease_version: bool = False) -> str:
"""
Get current version from tag or version variable, depending on configuration.
This will not return prerelease versions.
:return: A string with the current version number
"""
omit_pattern = None if prerelease_version else get_prerelease_pattern()
if config.get("version_source") == "tag":
return get_current_version_by_tag()
return get_current_version_by_config_file()
return get_current_version_by_tag(omit_pattern)
current_version = get_current_version_by_config_file(omit_pattern)
if omit_pattern and omit_pattern in current_version:
return get_previous_version(current_version)
return current_version


@LoggedFunction(logger)
def get_new_version(current_version: str, level_bump: str) -> str:
def get_new_version(current_version: str, level_bump: str, prerelease: bool = False) -> str:
"""
Calculate the next version based on the given bump level with semver.
:param current_version: The version the package has now.
:param level_bump: The level of the version number that should be bumped.
Should be `'major'`, `'minor'` or `'patch'`.
:param prerelease: Should the version bump be marked as a prerelease
:return: A string with the next version number.
"""
if not level_bump:
logger.debug("No bump requested, returning input version")
return current_version
return str(semver.VersionInfo.parse(current_version).next_version(part=level_bump))
logger.debug("No bump requested, using input version")
new_version = current_version
else:
new_version = str(semver.VersionInfo.parse(current_version).next_version(part=level_bump))

if prerelease:
logger.debug("Prerelease requested")
potentialy_prereleased_current_version = get_current_version(prerelease_version=True)
if get_prerelease_pattern() in potentialy_prereleased_current_version:
logger.debug("Previouse prerelease detected, increment prerelease version")
prerelease_num = int(potentialy_prereleased_current_version.split(".")[-1]) + 1
else:
logger.debug("No previouse prerelease detected, starting from 0")
prerelease_num = 0
new_version = new_version + get_prerelease_pattern() + str(prerelease_num)

return new_version


@LoggedFunction(logger)
def get_previous_version(version: str) -> Optional[str]:
def get_previous_version(version: str, omit_pattern: str = None) -> Optional[str]:
"""
Return the version prior to the given version.
Expand All @@ -260,12 +284,14 @@ def get_previous_version(version: str) -> Optional[str]:
continue

if found_version:
if omit_pattern and omit_pattern in commit_message:
continue
matches = re.match(r"v?(\d+.\d+.\d+)", commit_message)
if matches:
logger.debug(f"Version matches regex {commit_message}")
return matches.group(1).strip()

return get_last_version([version, get_formatted_tag(version)])
return get_last_version([version, get_formatted_tag(version)], omit_pattern=omit_pattern)


@LoggedFunction(logger)
Expand Down
10 changes: 7 additions & 3 deletions semantic_release/vcs_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def get_commit_log(from_rev=None):

@check_repo
@LoggedFunction(logger)
def get_last_version(skip_tags=None) -> Optional[str]:
def get_last_version(skip_tags=None, omit_pattern=None) -> Optional[str]:
"""
Find the latest version using repo tags.
Expand All @@ -78,8 +78,12 @@ def version_finder(tag):

for i in sorted(repo.tags, reverse=True, key=version_finder):
match = re.search(r"\d+\.\d+\.\d+", i.name)
if match and i.name not in skip_tags:
return match.group(0) # Return only numeric vesion like 1.2.3
if match:
# check if the omit pattern is present in the tag (e.g. -beta for pre-release tags)
if omit_pattern and omit_pattern in i.name:
continue
if i.name not in skip_tags:
return match.group(0) # Return only numeric vesion like 1.2.3

return None

Expand Down
36 changes: 32 additions & 4 deletions tests/history/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ def test_should_return_correct_version(self):
def test_should_return_correct_version_with_v(self):
assert get_previous_version("0.10.0") == "0.9.0"

@mock.patch(
"semantic_release.history.get_commit_log",
lambda: [("211", "0.10.0-beta"), ("13", "0.9.0")],
)
def test_should_return_correct_version_from_prerelease(self):
assert get_previous_version("0.10.0-beta") == "0.9.0"

@mock.patch(
"semantic_release.history.get_commit_log",
lambda: [("211", "0.10.0"), ("13", "0.10.0-beta"), ("13", "0.9.0")],
)
def test_should_return_correct_version_skip_prerelease(self):
assert get_previous_version(
"0.10.0-beta", omit_pattern="-beta") == "0.9.0"


class TestGetNewVersion:
def test_major_bump(self):
Expand All @@ -88,6 +103,19 @@ def test_patch_bump(self):
def test_none_bump(self):
assert get_new_version("1.0.0", None) == "1.0.0"

def test_prerelease(self):
assert get_new_version("1.0.0", None, True) == "1.0.0-beta.0"
assert get_new_version("1.0.0", "major", True) == "2.0.0-beta.0"
assert get_new_version("1.0.0", "minor", True) == "1.1.0-beta.0"
assert get_new_version("1.0.0", "patch", True) == "1.0.1-beta.0"

def test_prerelease_bump(self, mocker):
mocker.patch(
"semantic_release.history.get_current_version",
return_value="1.0.0-beta.0"
)
assert get_new_version("1.0.0", None, True) == "1.0.0-beta.1"


@mock.patch(
"semantic_release.history.config.get",
Expand Down Expand Up @@ -253,11 +281,11 @@ def test_toml_parse(self, tmp_path, key, content, hits):
name = "my-package"
version = "0.1.0"
description = "A super package"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.semantic_release]
version_toml = "pyproject.toml:tool.poetry.version"
"""
Expand All @@ -268,11 +296,11 @@ def test_toml_parse(self, tmp_path, key, content, hits):
name = "my-package"
version = "-"
description = "A super package"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.semantic_release]
version_toml = "pyproject.toml:tool.poetry.version"
"""
Expand Down

0 comments on commit 7064265

Please sign in to comment.