Skip to content

Commit

Permalink
cli: add git-create-release-commit
Browse files Browse the repository at this point in the history
* Closes #367.
  • Loading branch information
Diego Rodriguez committed Aug 12, 2020
1 parent ccb00ee commit 2390565
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 1 deletion.
12 changes: 12 additions & 0 deletions reana/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,15 @@

TIMEOUT = 300
"""Maximum timeout to wait for results when running demo analyses in CI."""

HELM_VERSION_FILE = "Chart.yaml"
"""Helm package version file."""

OPENAPI_VERSION_FILE = "openapi.json"
"""OpenAPI version file."""

JAVASCRIPT_VERSION_FILE = "package.json"
"""JavaScript package version file."""

PYTHON_VERSION_FILE = "version.py"
"""Python package version file."""
97 changes: 96 additions & 1 deletion reana/reana_dev/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import datetime
import os
import sys
import subprocess
import sys

import click

Expand All @@ -21,9 +21,12 @@
REPO_LIST_ALL,
REPO_LIST_SHARED,
)

from reana.reana_dev.utils import (
bump_component_version,
display_message,
fetch_latest_pypi_version,
get_current_component_version_from_source_files,
get_srcdir,
run_command,
select_components,
Expand Down Expand Up @@ -84,6 +87,57 @@ def get_current_commit(srcdir):
)


def git_is_current_version_tagged(component):
"""Determine whether the current version in source code is present as a git tag."""
current_version = get_current_component_version_from_source_files(component)
is_version_tagged = int(
run_command(
f"git tag --list {current_version} | wc -l",
component,
display=False,
return_output=True,
)
)
return bool(is_version_tagged)


def git_create_release_commit(component, next_version=None):
"""Create a release commit for the given component."""
if "release:" in get_current_commit(get_srcdir(component)):
display_message("Nothing to do, last commit is a release commit.", component)
return False

current_version = get_current_component_version_from_source_files(component)
if not current_version:
display_message(
f"Version cannot be autodiscovered from source files.", component
)
sys.exit(1)
elif not git_is_current_version_tagged(component) and not next_version:
display_message(
f"Current version ({current_version}) "
"not present as a git tag, please release it and add a tag.",
component,
)
sys.exit(1)

next_version, modified_files = bump_component_version(
component, current_version, next_version=next_version
)

if (
run_command(
"git branch --show-current", component, display=False, return_output=True,
)
== "master"
):
run_command(f"git checkout -b release-{next_version}", component)

run_command(f"git add {' '.join(modified_files)}", component)
run_command(f"git commit -m 'release: {next_version}'", component)
return True


@click.group()
def git_commands():
"""Git commands group."""
Expand Down Expand Up @@ -820,4 +874,45 @@ def _push_to_origin(components):
_push_to_origin(components)


@click.option(
"--component",
"-c",
required=True,
multiple=True,
help="Which components? [shortname|name|.|CLUSTER|ALL]",
)
@click.option(
"--version", "-v", help="Shall we manually specify component's next version?",
)
@git_commands.command(name="git-create-release-commit")
def git_create_release_commit_command(component, version): # noqa: D301
"""Create a release commit for the specified components.
\b
:param components: The option ``component`` can be repeated. The value may
consist of:
* (1) standard component name such as
'reana-workflow-controller';
* (2) short component name such as 'r-w-controller';
* (3) special value '.' indicating component of the
current working directory;
* (4) special value 'CLUSTER' that will expand to
cover all REANA cluster components [default];
* (5) special value 'CLIENT' that will expand to
cover all REANA client components;
* (6) special value 'DEMO' that will expand
to include several runable REANA demo examples;
* (7) special value 'ALL' that will expand to include
all REANA repositories.
:param version: Manually specifies the version for the component. If not provided,
the last version will be auto-incremented..
:type component: str
:type version: str
"""
components = select_components(component)
for component in components:
if git_create_release_commit(component, next_version=version):
display_message("Release commit created.", component)


git_commands_list = list(git_commands.commands.values())
207 changes: 207 additions & 0 deletions reana/reana_dev/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,24 @@
"""`reana-dev` related utils."""

import datetime
import importlib.util
import json
import os
import subprocess
import sys

import click
import semver
import yaml
from packaging.version import InvalidVersion, Version

from reana.config import (
COMPONENTS_USING_SHARED_MODULE_COMMONS,
COMPONENTS_USING_SHARED_MODULE_DB,
HELM_VERSION_FILE,
JAVASCRIPT_VERSION_FILE,
OPENAPI_VERSION_FILE,
PYTHON_VERSION_FILE,
REPO_LIST_ALL,
REPO_LIST_CLIENT,
REPO_LIST_CLUSTER,
Expand Down Expand Up @@ -458,3 +467,201 @@ def update_module_in_cluster_components(
bold=True,
fg="green",
)


def get_component_version_files(component, abs_path=False):
"""Get a dictionary with all component's version files."""
version_files = {}
for file_ in [
HELM_VERSION_FILE,
OPENAPI_VERSION_FILE,
JAVASCRIPT_VERSION_FILE,
PYTHON_VERSION_FILE,
]:
file_path = run_command(
f"git ls-files | grep -w {file_} || true",
component,
display=False,
return_output=True,
)
if file_path and abs_path:
file_path = os.path.join(get_srcdir(component=component), file_path)

version_files[file_] = file_path

return version_files


def get_current_component_version_from_source_files(component):
"""Get component's current version."""
version_files = get_component_version_files(component, abs_path=True)
version = ""
if version_files.get(HELM_VERSION_FILE):
with open(version_files.get(HELM_VERSION_FILE)) as f:
chart_yaml = yaml.safe_load(f.read())
version = chart_yaml["version"]

elif version_files.get(PYTHON_VERSION_FILE):
spec = importlib.util.spec_from_file_location(
component, version_files.get(PYTHON_VERSION_FILE)
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
version = module.__version__

elif version_files.get(JAVASCRIPT_VERSION_FILE):
with open(version_files.get(JAVASCRIPT_VERSION_FILE)) as f:
package_json = json.loads(f.read())
version = package_json["version"]

return version


def bump_semver2_version(current_version, part=None, pre_version_prefix=None):
"""Bump a semver2 version string.
:param current_version: current version to be bumped
:type current_version: str
:return: String representation of the next version
:rtype: string
"""
if not semver.VersionInfo.isvalid(current_version):
click.echo(
f"Current version {current_version} is not a valid semver2 version. Please amend it"
)

pre_version_prefix = pre_version_prefix or "alpha"
parsed_current_version = semver.VersionInfo.parse(current_version)
next_version = ""
if parsed_current_version.build or part == "build":
next_version = parsed_current_version.bump_build()
elif parsed_current_version.prerelease or part == "prerelease":
next_version = parsed_current_version.bump_prerelease("alpha")
elif parsed_current_version.patch or part == "patch":
next_version = parsed_current_version.next_version("patch")
elif parsed_current_version.minor or part == "minor":
next_version = parsed_current_version.next_version("minor")
elif parsed_current_version.major or part == "major":
next_version = parsed_current_version.next_version("major")

return str(next_version)


def bump_pep440_version(
current_version,
part=None,
dev_version_prefix=None,
post_version_prefix=None,
pre_version_prefix=None,
):
"""Bump a PEP440 version string.
:param current_version: current version to be bumped
:param part: part of the PEP440 version to bump
(one of: [major, minor, micro, dev, post, pre]).
:type current_version: str
:type part: str
:return: String representation of the next version
:rtype: string
"""

def _bump_dev_post_pre(dev_post_pre_number):
"""Bump a dev/post/prerelease depending on its number/date based version."""
try:
dev_post_pre_string = str(dev_post_pre_number)
default_date_format = "%Y%m%d"
extended_date_format = default_date_format + "%H%M%S"

today_ = datetime.datetime.today()
next_prerelease_date = today_.strftime(default_date_format)
prev_prerelease_date = datetime.datetime.strptime(
dev_post_pre_string, default_date_format
)
if today_ < prev_prerelease_date:
raise Exception(
"Current prerelease version is newer than today, please fix it."
)
if dev_post_pre_string == next_prerelease_date:
next_prerelease_date = today_.strftime(extended_date_format)
return next_prerelease_date
except ValueError:
return dev_post_pre_number + 1

try:
version = Version(current_version)
dev_post_pre_default_version_prefixes = {
"dev": dev_version_prefix or "dev",
"post": post_version_prefix or "post",
"pre": pre_version_prefix or "a",
}
next_version = ""
has_dev_post_pre = (
("dev" if version.dev else False)
or ("post" if version.post else False)
or version.pre[0]
)
if (part and part in dev_post_pre_default_version_prefixes.keys()) or (
has_dev_post_pre and not part
):
prefix_part = (
has_dev_post_pre or dev_post_pre_default_version_prefixes[part]
)
version_part = version.dev or version.post or version.pre[1]
version_part = _bump_dev_post_pre(version_part) if version_part else 1
prerelease_part = f"{prefix_part}{version_part}"
next_version = Version(
f"{version.major}.{version.minor}.{version.micro}.{prerelease_part}"
)
elif (part and part == "micro") or (
isinstance(version.micro, int) and not part
):
next_version = Version(f"{version.major}.{version.minor}.{version.micro+1}")
elif (part and part == "minor") or (
isinstance(version.minor, int) and not part
):
next_version = Version(f"{version.major}.{version.minor+1}.0")
elif (part and part == "major") or (
isinstance(version.major, int) and not part
):
next_version = Version(f"{version.major+1}.0.0")

return str(next_version)
except InvalidVersion as e:
click.echo(
f"Current {current_version} is not a valid PEP440 version. Please amend it"
)


def bump_component_version(component, current_version, next_version=None):
"""Bump to next component version."""
try:
version_files = get_component_version_files(component)
files_to_update = []

if version_files.get(HELM_VERSION_FILE):
next_version = next_version or bump_semver2_version(current_version)
files_to_update.append(version_files.get(HELM_VERSION_FILE))
elif version_files.get(PYTHON_VERSION_FILE):
next_version = next_version or bump_pep440_version(current_version)
files_to_update.append(version_files.get(PYTHON_VERSION_FILE))
if version_files.get(OPENAPI_VERSION_FILE):
files_to_update.append(version_files.get(OPENAPI_VERSION_FILE))
elif version_files.get(JAVASCRIPT_VERSION_FILE):
next_version = next_version or bump_semver2_version(current_version)
files_to_update.append(version_files.get(JAVASCRIPT_VERSION_FILE))

for file_ in files_to_update:
replace_string(
file_=file_,
find=current_version,
replace=next_version,
component=component,
)

return next_version, files_to_update
except Exception as e:
display_message(
f"Something went wront while bumping the version: {e}", component
)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"click>=7",
"colorama>=0.3.9",
"PyYAML>=5.1",
"semver>=2.10.2",
]


Expand Down

0 comments on commit 2390565

Please sign in to comment.