Skip to content

Feature/prerelease #413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 7, 2022
6 changes: 6 additions & 0 deletions docs/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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 @@ -40,3 +40,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
Loading