Skip to content

Fix/Refactor: Prerelease #435

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:

steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
Expand Down
27 changes: 17 additions & 10 deletions semantic_release/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
from .history import (
evaluate_version_bump,
get_current_version,
get_current_release_version,
get_new_version,
get_previous_version,
get_previous_release_version,
set_new_version,
)
from .history.logs import generate_changelog
Expand Down Expand Up @@ -98,7 +100,9 @@ def print_version(*, current=False, force_level=None, prerelease=False, **kwargs
Print the current or new version to standard output.
"""
try:
current_version = get_current_version(prerelease)
current_version = get_current_version()
current_release_version = get_current_release_version()
logger.info(f"Current version: {current_version}, Current release version: {current_release_version}")
except GitError as e:
print(str(e), file=sys.stderr)
return False
Expand All @@ -108,7 +112,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, level_bump, prerelease)
new_version = get_new_version(current_version, current_release_version, level_bump, prerelease)
if should_bump_version(current_version=current_version, new_version=new_version):
print(new_version, end="")
return True
Expand All @@ -130,14 +134,15 @@ def version(*, retry=False, noop=False, force_level=None, prerelease=False, **kw

# Get the current version number
try:
current_version = get_current_version(prerelease)
logger.info(f"Current version: {current_version}")
current_version = get_current_version()
current_release_version = get_current_release_version()
logger.info(f"Current version: {current_version}, Current release version: {current_release_version}")
except GitError as e:
logger.error(str(e))
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, prerelease)
level_bump = evaluate_version_bump(current_release_version, force_level)
new_version = get_new_version(current_version, current_release_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 @@ -207,7 +212,7 @@ def changelog(*, unreleased=False, noop=False, post=False, prerelease=False, **k

:raises ImproperConfigurationError: if there is no current version
"""
current_version = get_current_version(prerelease)
current_version = get_current_version()
if current_version is None:
raise ImproperConfigurationError(
"Unable to get the current version. "
Expand Down Expand Up @@ -245,7 +250,9 @@ def publish(
retry: bool = False, noop: bool = False, prerelease: bool = False, **kwargs
):
"""Run the version task, then push to git and upload to an artifact repository / GitHub Releases."""
current_version = get_current_version(prerelease)
current_version = get_current_version()
current_release_version = get_current_release_version()
logger.info(f"Current version: {current_version}, Current release version: {current_release_version}")

verbose = logger.isEnabledFor(logging.DEBUG)
if retry:
Expand All @@ -254,11 +261,11 @@ def publish(
# "current" version will be the previous version.
level_bump = None
new_version = current_version
current_version = get_previous_version(current_version)
current_version = get_previous_release_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, prerelease)
new_version = get_new_version(current_version, current_release_version, level_bump, prerelease)

owner, name = get_repository_owner_and_name()

Expand Down
171 changes: 125 additions & 46 deletions semantic_release/history/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
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}))"

release_version_regex = rf"{release_version_pattern}"
version_regex = rf"{version_pattern}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this variables as single source of truth for regex version matching, for ease of use once as string and once as regex.



class VersionDeclaration(ABC):
def __init__(self, path: Union[str, Path]):
self.path = Path(path)
Expand All @@ -46,7 +54,7 @@ def from_variable(config_str: str):
"""
path, variable = config_str.split(":", 1)
pattern = (
rf'{variable} *[:=] *["\']{PatternVersionDeclaration.version_regex}["\']'
rf'{variable} *[:=] *["\']{version_regex}["\']'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced to use a single source of truth

)
return PatternVersionDeclaration(path, pattern)

Expand All @@ -57,7 +65,7 @@ def from_pattern(config_str: str):
regular expression matching the version number.
"""
path, pattern = config_str.split(":", 1)
pattern = pattern.format(version=PatternVersionDeclaration.version_regex)
pattern = pattern.format(version=version_regex)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced to use a single source of truth

return PatternVersionDeclaration(path, pattern)

@abstractmethod
Expand Down Expand Up @@ -116,8 +124,6 @@ class PatternVersionDeclaration(VersionDeclaration):
function to create the version patterns specified in the config files.
"""

version_regex = r"(\d+\.\d+(?:\.\d+)?)"

# The pattern should be a regular expression with a single group,
# containing the version to replace.
def __init__(self, path: str, pattern: str):
Expand Down Expand Up @@ -176,18 +182,29 @@ 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:
"""
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(pattern=version_pattern)
if version:
return version

logger.debug("no version found, returning default of v0.0.0")
return "0.0.0"


@LoggedFunction(logger)
def get_current_version_by_tag(omit_pattern=None) -> str:
def get_current_release_version_by_tag() -> 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(omit_pattern=omit_pattern)
version = get_last_version(pattern=release_version_pattern)
if version:
return version

Expand All @@ -196,7 +213,7 @@ def get_current_version_by_tag(omit_pattern=None) -> str:


@LoggedFunction(logger)
def get_current_version_by_config_file(omit_pattern=None) -> str:
def get_current_version_by_config_file() -> str:
"""
Get current version from the version variable defined in the configuration.

Expand All @@ -220,25 +237,36 @@ def get_current_version_by_config_file(omit_pattern=None) -> str:
return version


def get_current_version(prerelease_version: bool = False) -> str:
def get_current_version() -> str:
"""
Get current version from tag or version variable, depending on configuration.
This will not return prerelease versions.
This can be either a release or prerelease version

:return: A string with the current version number
"""
omit_pattern = None if prerelease_version else get_prerelease_pattern()
if config.get("version_source") in ["tag", "tag_only"]:
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
return get_current_version_by_tag()
else:
return get_current_version_by_config_file()


def get_current_release_version() -> str:
"""
Get current release version from tag or commit message (no going back in config file),
depending on configuration.
This will return the current release version (NOT prerelease), instead of just the current version

:return: A string with the current version number
"""
if config.get("version_source") == "tag":
return get_current_release_version_by_tag()
else:
return get_current_release_version_by_commits()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_current_release_version should give us the last not-prerelease version.
As the config file does not give us any history, we have to analyse commits here instead of just looking into the config file.



@LoggedFunction(logger)
def get_new_version(
current_version: str, level_bump: str, prerelease: bool = False
current_version: str, current_release_version: str, level_bump: str, prerelease: bool = False
) -> str:
"""
Calculate the next version based on the given bump level with semver.
Expand All @@ -249,34 +277,46 @@ def get_new_version(
: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, using input version")
new_version = current_version
# pre or release version
current_version_info = semver.VersionInfo.parse(current_version)
# release version
current_release_version_info = semver.VersionInfo.parse(current_release_version)

# sanity check
# if the current version is no prerelease, than
# current_version and current_release_version must be the same
if not current_version_info.prerelease and current_version_info.compare(current_release_version_info) != 0:
raise ValueError()

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")
else:
new_version = str(
semver.VersionInfo.parse(current_version).next_version(part=level_bump)
)
next_version_info = current_release_version_info

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
)
next_raw_version = next_version_info.to_tuple()[:3]
current_raw_version = current_version_info.to_tuple()[:3]

if current_version_info.prerelease and next_raw_version == current_raw_version:
# next version (based on commits) matches current prerelease version
# bump prerelase
next_prerelease_version_info = current_version_info.bump_prerelease(config.get("prerelease_tag"))
else:
logger.debug("No previouse prerelease detected, starting from 0")
prerelease_num = 0
new_version = new_version + get_prerelease_pattern() + str(prerelease_num)
# next version (based on commits) higher than current prerelease version
# new prerelease based on next version
next_prerelease_version_info = next_version_info.bump_prerelease(config.get("prerelease_tag"))

return new_version
return str(next_prerelease_version_info)
else:
# normal version bump
return str(next_version_info)


@LoggedFunction(logger)
def get_previous_version(version: str, omit_pattern: str = None) -> Optional[str]:
def get_previous_version(version: str) -> Optional[str]:
"""
Return the version prior to the given version.

Expand All @@ -292,16 +332,55 @@ def get_previous_version(version: str, omit_pattern: str = None) -> 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:
match = re.search(rf"{version_pattern}", commit_message)
if match:
logger.debug(f"Version matches regex {commit_message}")
return matches.group(1).strip()
return match.group(1).strip()

return get_last_version(pattern=version_pattern, skip_tags=[version, get_formatted_tag(version)])


return get_last_version(
[version, get_formatted_tag(version)], omit_pattern=omit_pattern
)
@LoggedFunction(logger)
def get_previous_release_version(version: str) -> Optional[str]:
"""
Return the version prior to the given version.

:param version: A string with the version number.
:return: A string with the previous version number.
"""
found_version = False
for commit_hash, commit_message in get_commit_log():
logger.debug(f"Checking commit {commit_hash}")
if version in commit_message:
found_version = True
logger.debug(f'Found version in commit "{commit_message}"')
continue

if found_version:
match = re.search(rf"{release_version_pattern}", commit_message)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced to use a single source of truth

if match:
logger.debug(f"Version matches regex {commit_message}")
return match.group(1).strip()

return get_last_version(pattern=release_version_pattern, skip_tags=[version, get_formatted_tag(version)])


@LoggedFunction(logger)
def get_current_release_version_by_commits() -> str:
"""
Return the current release version (NOT prerelease) version.

:return: A string with the current version number.
"""
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)
if match:
logger.debug(f"Version matches regex {commit_message}")
return match.group(1).strip()

# nothing found, return the initial version
return "0.0.0"


@LoggedFunction(logger)
Expand Down
Loading