Browse Source

Add release validation and tagging script release.py

Signed-off-by: Ulysses Souza <[email protected]>
Ulysses Souza 5 years ago
parent
commit
b5c4f4fc0f
5 changed files with 194 additions and 3 deletions
  1. 2 0
      requirements-dev.txt
  2. 12 3
      script/release/README.md
  3. 7 0
      script/release/const.py
  4. 126 0
      script/release/release.py
  5. 47 0
      script/release/utils.py

+ 2 - 0
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'

+ 12 - 3
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 <version>`
+
+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 <version_branch>`
+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.

+ 7 - 0
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__), '..', '..')

+ 126 - 0
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
+
+
[email protected]()
+def cli():
+    pass
+
+
[email protected]()
[email protected]('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()))
+
+
[email protected]()
[email protected]('version')
+def push_latest(version):
+    """
+    TODO Pushes the latest tag pointing to a certain GA version
+    """
+    raise NotImplementedError
+
+
[email protected]()
[email protected]('version')
+def ghtemplate(version):
+    """
+    TODO Generates the github release page content
+    """
+    version = Version(version)
+    raise NotImplementedError
+
+
+if __name__ == '__main__':
+    cli()

+ 47 - 0
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