#!/usr/bin/env python3 import os import re import subprocess import click ROOT_DIR = os.path.dirname( os.path.dirname(os.path.abspath(__file__)), ) @click.group() def cli(): """Command line tool for managing releases. To do a chalice release, run these commands:: \b $ NEXT_VERSION=$(jmeslog query next-version) $ scripts/release bump-version --version-number ${NEXT_VERSION} $ git add -A . $ git commit -m "Bumping version to $NEXT_VERSION" $ scripts/release tag-release $ scripts/release build-release $ git push upstream master --tags $ twine upload dist/chalice-* """ pass @cli.command('bump-version') @click.option('--version-number') def bump_version(version_number): """Update necessary files with next version number.""" print(f"Bumping version to: {version_number}") _create_new_changelog_release() for filename, replacer in get_files_to_change().items(): print("Bumping version in %s" % filename) with open(filename, 'r') as f: contents = f.read() if callable(replacer): new_contents = replacer(version_number, contents) else: new_contents = _regex_based_version_bump( version_number, replacer, contents) with open(filename, 'w') as f: f.write(new_contents) def _create_new_changelog_release(): # This takes everything from .changes/next-release/ and creates # a new release entry for them. subprocess.check_call(['jmeslog', 'new-release']) @cli.command('build-release') def build_release(): """Build sdist/whl files.""" original = os.getcwd() os.chdir(ROOT_DIR) try: subprocess.check_call( ['python', 'setup.py', 'sdist', 'bdist_wheel'] ) finally: os.chdir(original) @cli.command('tag-release') def tag_release(): """Create a git tag based on the current version number.""" # We're assuming that setup.py has already been updated # manually or using scripts/release/bump-version so the # current version in setup.py is the version number we should tag. version_number = get_current_version_number() click.echo("Tagging %s release" % version_number) subprocess.check_call( ['git', 'tag', '-a', version_number, '-m', 'Tagging %s release' % version_number], ) @cli.command('get-version') def get_version(): """Print the current version number in setup.py.""" click.echo(get_current_version_number()) def get_files_to_change(): # A mapping of all files that require version bumps. # You can either specify: # * Tuple[str, str] - regex to search, replacement string # * Callable[[str, str], str] - function to handle custom logic files_with_version_numbers = { 'chalice/app.py': ( "__version__: str = '.*'", "__version__: str = '{version}'"), 'CHANGELOG.rst': update_changelog, 'docs/source/conf.py': update_doc_conf, 'setup.py': ("version='(.*)'", "version='{version}'"), } return files_with_version_numbers def _regex_based_version_bump(next_version_number, replacer, contents): regex = replacer[0] replacement = replacer[1].format(version=next_version_number) new_contents = re.sub(regex, replacement, contents) return new_contents def update_changelog(next_version_number, contents): output = subprocess.check_output(['jmeslog', 'render']) return output.decode('utf-8') def update_doc_conf(next_version_number, contents): # For the docs the 'version' is only X.Y # and the release is X.Y.Z version = '.'.join(next_version_number.split('.')[:2]) release = next_version_number new_contents = [] for line in contents.splitlines(): if line.startswith('version ='): new_contents.append("version = u'%s'" % version) elif line.startswith('release = '): new_contents.append("release = u'%s'" % release) else: new_contents.append(line) # Ensure the file ends with a newline. new_contents.append('') return '\n'.join(new_contents) def get_next_version_number(release_type): # Returns a string like '1.0.0'. current = get_current_version_number() # Convert to a list of ints: [1, 0, 0]. version_parts = list(int(i) for i in current.split('.')) # We've already validated that release_type is from a fixed # list of choices so we know it's going to be one of these. # We only support integer version parts, which shouldn't be # a problem now that we're post 1.0. if release_type == 'patch': version_parts[-1] += 1 elif release_type == 'minor': version_parts[1] += 1 version_parts[-1] = 0 return '.'.join(str(i) for i in version_parts) def get_current_version_number(): # We can avoid executing setup.py because we know # specifically how the version is hardcoded in the setup.py file. # This won't work for the general case. regex = re.compile("version='(.*)',") with open(os.path.join(ROOT_DIR, 'setup.py')) as f: for line in f: match = regex.search(line) if match is not None: return match.groups()[0] raise RuntimeError("Could not find version number from setup.py") def main(): return cli() if __name__ == '__main__': main()