Skip to content

Commit

Permalink
Change: Rework release process by using tags for last release version
Browse files Browse the repository at this point in the history
Before this change the current release version was derived from the
project settings. Additionally the last release version was determined
from the git tags in a complicated way keeping the current release
version in mind to support different release series. The last release
version was used only to create the changelog for the release.

With this change getting the current version from the project settings
is dropped and only the last release version is used. Determining the
last release version from the git tags is simplified by just using the
latest tag (sorted by versions) and optionally a release series.

If a release series is provided the last tag fitting to the series is
used as last release version. A release series can be something like
"1.0", "2.0", "20.4" or even just "1".
  • Loading branch information
bjoernricks committed Apr 19, 2023
1 parent e702c68 commit e157846
Show file tree
Hide file tree
Showing 6 changed files with 632 additions and 291 deletions.
12 changes: 12 additions & 0 deletions pontos/release/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ def parse_args(args) -> Tuple[str, str, Namespace]:
),
action=ReleaseVersionAction,
)
release_parser.add_argument(
"--release-series",
help="Create a release for a release series. Setting a release series "
"is required if the latest tag version is newer then the to be "
'released version. Examples: "1.2", "2", "22.4"',
)

release_parser.add_argument(
"--next-version",
Expand Down Expand Up @@ -172,6 +178,12 @@ def parse_args(args) -> Tuple[str, str, Namespace]:
"--release-version",
help="Will release changelog as version. Must be PEP 440 compliant.",
)
sign_parser.add_argument(
"--release-series",
help="Sign release files for a release series. Setting a release "
"series is required if the latest tag version is newer then the to be "
'signed version. Examples: "1.2", "2", "22.4"',
)
sign_parser.add_argument(
"--git-tag-prefix",
default="v",
Expand Down
175 changes: 57 additions & 118 deletions pontos/release/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from argparse import Namespace
from enum import IntEnum, auto
from pathlib import Path
from typing import Iterable, Optional
from typing import Optional

import httpx

Expand All @@ -30,7 +30,7 @@
from pontos.github.api import GitHubAsyncRESTApi
from pontos.terminal import Terminal
from pontos.version import Version, VersionCalculator, VersionError
from pontos.version.helper import get_last_release_versions
from pontos.version.helper import get_last_release_version
from pontos.version.project import Project
from pontos.version.schemes import VersioningScheme

Expand All @@ -45,23 +45,14 @@ class ReleaseReturnValue(IntEnum):
SUCCESS = 0
PROJECT_SETTINGS_NOT_FOUND = auto()
TOKEN_MISSING = auto()
NO_LAST_RELEASE_VERSION = auto()
NO_RELEASE_VERSION = auto()
ALREADY_TAKEN = auto()
CREATE_RELEASE_ERROR = auto()
UPDATE_VERSION_ERROR = auto()
UPDATE_VERSION_AFTER_RELEASE_ERROR = auto()


def _get_version(
release_version: Version, last_release_versions: Iterable[Version]
) -> Optional[Version]:
for version in last_release_versions:
if release_version > version:
return version

return None


class ReleaseCommand:
"""
A CLI command for creating a release
Expand All @@ -74,34 +65,36 @@ def __init__(self, terminal: Terminal) -> None:
self.git = Git()
self.terminal = terminal

def _get_release_version(
def _get_next_release_version(
self,
*,
current_version: Version,
last_release_version: Version,
calculator: VersionCalculator,
release_type: ReleaseType,
release_version: Optional[Version],
) -> Version:
if release_type == ReleaseType.CALENDAR:
return calculator.next_calendar_version(current_version)
return calculator.next_calendar_version(last_release_version)

if release_type == ReleaseType.PATCH:
return calculator.next_patch_version(current_version)
return calculator.next_patch_version(last_release_version)

if release_type == ReleaseType.MINOR:
return calculator.next_minor_version(current_version)
return calculator.next_minor_version(last_release_version)

if release_type == ReleaseType.MAJOR:
return calculator.next_major_version(current_version)
return calculator.next_major_version(last_release_version)

if release_type == ReleaseType.ALPHA:
return calculator.next_alpha_version(current_version)
return calculator.next_alpha_version(last_release_version)

if release_type == ReleaseType.BETA:
return calculator.next_beta_version(current_version)
return calculator.next_beta_version(last_release_version)

if release_type == ReleaseType.RELEASE_CANDIDATE:
return calculator.next_release_candidate_version(current_version)
return calculator.next_release_candidate_version(
last_release_version
)

if not release_version:
raise VersionError(
Expand Down Expand Up @@ -145,74 +138,6 @@ async def _create_release(
prerelease=release_version.is_pre_release,
)

def _get_last_release(
self,
release_type: ReleaseType,
release_version: Version,
calculator: VersionCalculator,
) -> Version:
# try to get last tag for the matching release series
if release_type in [
ReleaseType.PATCH,
ReleaseType.ALPHA,
ReleaseType.BETA,
ReleaseType.RELEASE_CANDIDATE,
]:
tag_name = (
f"{self.git_tag_prefix}"
f"{release_version.major}.{release_version.minor}.*"
)
elif release_type == ReleaseType.MINOR:
tag_name = f"{self.git_tag_prefix}{release_version.major}.*"
else:
tag_name = None

last_release_versions = get_last_release_versions(
calculator.version_from_string,
git_tag_prefix=self.git_tag_prefix,
ignore_pre_releases=not release_version.is_pre_release,
tag_name=tag_name,
)

last_release_version = _get_version(
release_version, last_release_versions
)

if tag_name and not last_release_version:
if release_type in [
ReleaseType.PATCH,
ReleaseType.ALPHA,
ReleaseType.BETA,
ReleaseType.RELEASE_CANDIDATE,
]:
tag_name = f"{self.git_tag_prefix}{release_version.major}.*"
else:
tag_name = None

last_release_versions = get_last_release_versions(
calculator.version_from_string,
git_tag_prefix=self.git_tag_prefix,
ignore_pre_releases=not release_version.is_pre_release,
tag_name=tag_name,
)

last_release_version = _get_version(
release_version, last_release_versions
)

if not last_release_version:
last_release_versions = get_last_release_versions(
calculator.version_from_string,
git_tag_prefix=self.git_tag_prefix,
ignore_pre_releases=not release_version.is_pre_release,
)

last_release_version = _get_version(
release_version, last_release_versions
)

return last_release_version

async def run(
self,
*,
Expand All @@ -228,6 +153,7 @@ async def run(
git_tag_prefix: Optional[str],
cc_config: Optional[Path],
local: Optional[bool] = False,
release_series: Optional[str] = None,
) -> ReleaseReturnValue:
"""
Create a release
Expand All @@ -245,8 +171,9 @@ async def run(
a new CalVer release version. VERSION uses the provided
release_version.
release_version: Optional release version to use. If not set the
current version will be determined from the project.
to be released version will be determined from the project.
next_version: Optional version to set after the release.
If not set the next development version will be set.
git_signing_key: A GPG key ID to use for creating signatures.
git_remote_name: Name of the git remote to use.
git_tag_prefix: An optional prefix to use for creating a git tag
Expand All @@ -255,6 +182,8 @@ async def run(
commits.
local: Only create changes locally and don't push changes to
remote repository. Also don't create a GitHub release.
release_series: Optional release series to use.
For example: "1.2", "2", "23".
"""
git_signing_key = (
git_signing_key
Expand All @@ -267,23 +196,41 @@ async def run(
)
self.space = space

self.terminal.info(
f"Using versioning scheme {versioning_scheme.__class__.__name__}"
)

try:
project = Project(versioning_scheme)
last_release_version = get_last_release_version(
parse_version=versioning_scheme.parse_version,
git_tag_prefix=self.git_tag_prefix,
tag_name=f"{self.git_tag_prefix}{release_series}.*"
if release_series
else None,
)
except PontosError as e:
self.terminal.error(f"Unable to determine project settings. {e}")
return ReleaseReturnValue.PROJECT_SETTINGS_NOT_FOUND
self.terminal.error(
f"Could not determine last release version. {e}"
)
return ReleaseReturnValue.NO_LAST_RELEASE_VERSION

self.terminal.info(f"Using versioning scheme {versioning_scheme}")
if not last_release_version:
if not release_version:
self.terminal.error("Unable to determine last release version.")
return ReleaseReturnValue.NO_LAST_RELEASE_VERSION
else:
self.terminal.info(
f"Creating the initial release {release_version}"
)

else:
self.terminal.info(f"Last release was {last_release_version}")

calculator = versioning_scheme.calculator()

try:
current_version = project.get_current_version()

self.terminal.info(f"Current version is {current_version}")

release_version = self._get_release_version(
current_version=current_version,
release_version = self._get_next_release_version(
last_release_version=last_release_version,
calculator=calculator,
release_type=release_type,
release_version=release_version,
Expand All @@ -292,9 +239,6 @@ async def run(
self.terminal.error(f"Unable to determine release version. {e}")
return ReleaseReturnValue.NO_RELEASE_VERSION

if not release_version:
return ReleaseReturnValue.NO_RELEASE_VERSION

self.terminal.info(f"Preparing the release {release_version}")

git_version = f"{self.git_tag_prefix}{release_version}"
Expand All @@ -303,6 +247,12 @@ async def run(
self.terminal.error(f"Git tag {git_version} already exists.")
return ReleaseReturnValue.ALREADY_TAKEN

try:
project = Project(versioning_scheme)
except PontosError as e:
self.terminal.error(f"Unable to determine project settings. {e}")
return ReleaseReturnValue.PROJECT_SETTINGS_NOT_FOUND

try:
updated = project.update_version(release_version)

Expand All @@ -317,23 +267,11 @@ async def run(
)
return ReleaseReturnValue.UPDATE_VERSION_ERROR

last_release_version = self._get_last_release(
release_type=release_type,
release_version=release_version,
calculator=calculator,
self.terminal.info(
f"Creating changelog for {release_version} since "
f"{last_release_version}"
)

if not last_release_version:
self.terminal.info(
f"No last release found. Creating changelog for the initial "
f"release {release_version}"
)
else:
self.terminal.info(
f"Creating changelog for {release_version} since "
f"{last_release_version}"
)

release_text = self._create_changelog(
release_version, last_release_version, cc_config
)
Expand Down Expand Up @@ -425,5 +363,6 @@ def release(
git_tag_prefix=args.git_tag_prefix,
cc_config=args.cc_config,
local=args.local,
release_series=args.release_series,
)
)
23 changes: 17 additions & 6 deletions pontos/release/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,15 @@ async def run(
self,
*,
token: str,
dry_run: Optional[bool] = False,
space: str,
project: Optional[str],
versioning_scheme: VersioningScheme,
git_tag_prefix: Optional[str],
release_version: Optional[Version],
signing_key: str,
passphrase: str,
dry_run: Optional[bool] = False,
project: Optional[str],
git_tag_prefix: Optional[str],
release_version: Optional[Version],
release_series: Optional[str] = None,
) -> SignReturnValue:
"""
Sign a release
Expand All @@ -209,6 +210,8 @@ async def run(
current version will be determined from the project.
signing_key: A GPG key ID to use for creating signatures.
passphrase: Passphrase for the signing key
release_series: Optional release series to use.
For example: "1.2", "2", "23".
"""
if not token and not dry_run:
# dry run doesn't upload assets. therefore a token MAY NOT be
Expand All @@ -219,12 +222,16 @@ async def run(
)
return SignReturnValue.TOKEN_MISSING

self.terminal.info(
f"Using versioning scheme {versioning_scheme.__class__.__name__}"
)

try:
project = (
project if project is not None else get_git_repository_name()
)
except GitError:
self.terminal.error("Could not determine project. {e}")
except GitError as e:
self.terminal.error(f"Could not determine project. {e}")
return SignReturnValue.NO_PROJECT

try:
Expand All @@ -234,6 +241,9 @@ async def run(
else get_last_release_version(
versioning_scheme.parse_version,
git_tag_prefix=git_tag_prefix,
tag_name=f"{git_tag_prefix}{release_series}.*"
if release_series
else None,
)
)
except PontosError as e:
Expand Down Expand Up @@ -371,5 +381,6 @@ def sign(
release_version=args.release_version,
signing_key=args.signing_key,
passphrase=args.passphrase,
release_series=args.release_series,
)
)
Loading

0 comments on commit e157846

Please sign in to comment.