diff --git a/requirements-dev.txt b/requirements-dev.txt index 0fea3b6d3..d723b3705 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,8 @@ +Click==7.0 coverage==5.0.3 ddt==1.2.2 flake8==3.7.9 +gitpython==2.1.14 mock==3.0.5 pytest==5.3.4; python_version >= '3.5' pytest==4.6.5; python_version < '3.5' diff --git a/script/release/README.md b/script/release/README.md index 022d35417..8c961e3ad 100644 --- a/script/release/README.md +++ b/script/release/README.md @@ -4,6 +4,15 @@ The release process is fully automated by `Release.Jenkinsfile`. ## Usage -1. edit `compose/__init__.py` to set release version number -1. commit and tag as `{major}.{minor}.{patch}` -1. edit `compose/__init__.py` again to set next development version number +1. In the appropriate branch, run `./scripts/release/release tag ` + +By appropriate, we mean for a version `1.26.0` or `1.26.0-rc1` you should run the script in the `1.26.x` branch. + +The script should check the above then ask for changelog modifications. + +After the executions, you should have a commit with the proper bumps for `docker-compose version` and `run.sh` + +2. Run `git push --tags upstream ` +This should trigger a new CI build on the new tag. When the CI finishes with the tests and builds a new draft release would be available on github's releases page. + +3. Check and confirm the release on github's release page. diff --git a/script/release/const.py b/script/release/const.py new file mode 100644 index 000000000..77a32332a --- /dev/null +++ b/script/release/const.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os + + +REPO_ROOT = os.path.join(os.path.dirname(__file__), '..', '..') diff --git a/script/release/release.py b/script/release/release.py new file mode 100755 index 000000000..f53d1f3c1 --- /dev/null +++ b/script/release/release.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +from __future__ import absolute_import +from __future__ import unicode_literals + +import re + +import click +from git import Repo +from utils import update_init_py_version +from utils import update_run_sh_version +from utils import yesno + +VALID_VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(-rc\d+)?$") + + +class Version(str): + def matching_groups(self): + match = VALID_VERSION_PATTERN.match(self) + if not match: + return False + + return match.groups() + + def is_ga_version(self): + groups = self.matching_groups() + if not groups: + return False + + rc_suffix = groups[1] + return not rc_suffix + + def validate(self): + return len(self.matching_groups()) > 0 + + def branch_name(self): + if not self.validate(): + return None + + rc_part = self.matching_groups()[0] + ver = self + if rc_part: + ver = ver[:-len(rc_part)] + + tokens = ver.split(".") + tokens[-1] = 'x' + + return ".".join(tokens) + + +def create_bump_commit(repository, version): + print('Creating bump commit...') + repository.commit('-a', '-s', '-m "Bump {}"'.format(version), '--no-verify') + + +def validate_environment(version, repository): + if not version.validate(): + print('Version "{}" has an invalid format. This should follow D+.D+.D+(-rcD+). ' + 'Like: 1.26.0 or 1.26.0-rc1'.format(version)) + return False + + expected_branch = version.branch_name() + if str(repository.active_branch) != expected_branch: + print('Cannot tag in this branch with version "{}". ' + 'Please checkout "{}" to tag'.format(version, version.branch_name())) + return False + return True + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.argument('version') +def tag(version): + """ + Updates the version related files and tag + """ + repo = Repo(".") + version = Version(version) + if not validate_environment(version, repo): + return + + update_init_py_version(version) + update_run_sh_version(version) + + input('Please add the release notes to the CHANGELOG.md file, then press Enter to continue.') + proceed = False + while not proceed: + print(repo.git.diff()) + proceed = yesno('Are these changes ok? y/N ', default=False) + + if repo.git.diff(): + create_bump_commit(repo.git, version) + else: + print('No changes to commit. Exiting...') + return + + repo.create_tag(version) + + print('Please, check the changes. If everything is OK, you just need to push with:\n' + '$ git push --tags upstream {}'.format(version.branch_name())) + + +@cli.command() +@click.argument('version') +def push_latest(version): + """ + TODO Pushes the latest tag pointing to a certain GA version + """ + raise NotImplementedError + + +@cli.command() +@click.argument('version') +def ghtemplate(version): + """ + TODO Generates the github release page content + """ + version = Version(version) + raise NotImplementedError + + +if __name__ == '__main__': + cli() diff --git a/script/release/utils.py b/script/release/utils.py new file mode 100644 index 000000000..4f5770487 --- /dev/null +++ b/script/release/utils.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import os +import re + +from const import REPO_ROOT + + +def update_init_py_version(version): + path = os.path.join(REPO_ROOT, 'compose', '__init__.py') + with open(path, 'r') as f: + contents = f.read() + contents = re.sub(r"__version__ = '[0-9a-z.-]+'", "__version__ = '{}'".format(version), contents) + with open(path, 'w') as f: + f.write(contents) + + +def update_run_sh_version(version): + path = os.path.join(REPO_ROOT, 'script', 'run', 'run.sh') + with open(path, 'r') as f: + contents = f.read() + contents = re.sub(r'VERSION="[0-9a-z.-]+"', 'VERSION="{}"'.format(version), contents) + with open(path, 'w') as f: + f.write(contents) + + +def yesno(prompt, default=None): + """ + Prompt the user for a yes or no. + + Can optionally specify a default value, which will only be + used if they enter a blank line. + + Unrecognised input (anything other than "y", "n", "yes", + "no" or "") will return None. + """ + answer = input(prompt).strip().lower() + + if answer == "y" or answer == "yes": + return True + elif answer == "n" or answer == "no": + return False + elif answer == "": + return default + else: + return None