#!/usr/bin/env python from distutils.util import strtobool from git import Repo import contoml import semantic_version import shutil import fabric from fabric.api import local, task from fabric.decorators import runs_once from deploy_common import * from build import * RELEASE_TYPE_SNAPSHOT = "snapshot" RELEASE_TYPE_FINAL = "final" RELEASE_TYPE_TEST_FINAL = "testfinal" # used to test final releases without committing to master. RELEASE_TYPES = [RELEASE_TYPE_SNAPSHOT, RELEASE_TYPE_FINAL, RELEASE_TYPE_TEST_FINAL] SNAPSHOT = "SNAPSHOT" BRANCH_DEVELOP = "develop" BRANCH_MASTER = "master" BRANCH_TEST_MASTER = "testmaster" BRANCH_TEST_DEVELOP = "testdevelop" @runs_once @task def release(release_context): if not release_context.dry_run: print("*** You are about to do a release. This is not a dry run. ***") resp = _prep_bool_arg(raw_input('Confirm [Y/N]: ')) if not resp: fabric.utils.abort("Release aborted.") if release_context.release_type not in RELEASE_TYPES: fabric.utils.abort("You must specify the release type: [snapshot xor final xor testfinal]") if not release_context.disable_checks and release_context.repo_active_branch().lower() != BRANCH_DEVELOP: fabric.utils.abort("You must be on the develop branch in order to do a release. You are on branch {}".format( release_context.repo_active_branch())) if not release_context.disable_checks and release_context.repo_is_dirty(): fabric.utils.abort("There are uncommitted changes on the develop branch.") if release_context.is_test_final_release(): release_context.checkout_test_develop() starting_version, package_name = read_cargo_file(release_context) print("starting version = {sv}, package name = {pn}".format(sv=starting_version, pn=package_name)) release_version = confirm_version(release_context, semantic_version.Version(starting_version)) print "Releasing {} v{}".format(package_name, str(release_version)) update_version_in_files(release_context, release_version) if release_context.run_build: build_and_test(PROJECT_ROOT) print "Build and tests completed successfully" if not release_context.dry_run: release_context.commit_release("Release commit for {}.".format(str(release_version))) print "Committed release v{} to {}.".format(str(release_version), release_context.repo_active_branch()) tag = "{}-{}".format(package_name, str(release_version)) if not release_context.dry_run: release_context.tag_release(tag, tag) print "Tagged release v{} to {}.".format(str(release_version), release_context.repo_active_branch()) if not release_context.dry_run: print "Pushing release to origin." release_context.push_to_origin() @runs_once @task def bump_version(release_context): starting_version, package_name = read_cargo_file(release_context) starting_sem_ver = semantic_version.Version(starting_version) if release_context.is_snapshot_release(): snapshot_version = to_snapshot_version(starting_sem_ver) update_version_in_files(release_context, snapshot_version) print("Updated files with SNAPSHOT specifier.") if not release_context.dry_run: release_context.commit_release("Rewrite version to SNAPSHOT.") if release_context.is_test_final_release(): release_context.checkout_test_master() release_context.merge_test_develop() release_context.checkout_test_develop() next_version = to_next_patch_snapshot_version(starting_sem_ver) update_version_in_files(release_context, next_version) print("Updated files with SNAPSHOT specifier.") if not release_context.dry_run: release_context.commit_release("Bumped version to {}.".format(next_version)) if release_context.is_final_release(): release_context.checkout_master() release_context.merge_develop() release_context.checkout_develop() next_version = to_next_patch_snapshot_version(starting_sem_ver) update_version_in_files(release_context, next_version) print("Updated files with SNAPSHOT specifier.") if not release_context.dry_run: release_context.commit_release("Bumped version to {}.".format(next_version)) if not release_context.dry_run: print "Pushing release to origin." release_context.push_to_origin() class ReleaseContext: def __init__( self, repo_path, release_type, cargo_file, version_file, readme_file, disable_checks, dry_run, run_build ): # Either final or snapshot self.release_type = release_type.lower() # This should be the path to the Cargo.toml file. self.cargo_file = cargo_file # This should be the path to the version.txt file. self.version_file = version_file # This should be the path to the README.md file. self.readme_file = readme_file # disable_checks is useful for testing of the release script. # It should not be used normally. self.disable_checks = disable_checks # Do everything non destructively. That is, the script will run with # output but nothing will actually be committed. self.dry_run = dry_run # The git repo. self._repo = Repo(repo_path) # Run the build and test processes? self.run_build = run_build def repo_active_branch(self): return self._repo.active_branch.name def repo_is_dirty(self): return self._repo.is_dirty() def commit_release(self, message): self._repo.git.add(update=True) self._repo.index.commit(message) def tag_release(self, tag, tag_message): self._repo.create_tag(tag, message=tag_message) def push_to_origin(self): self._repo.remotes.origin.push('refs/heads/*:refs/heads/*', tags=True) def is_snapshot_release(self): return self.release_type == RELEASE_TYPE_SNAPSHOT def is_final_release(self): return self.release_type == RELEASE_TYPE_FINAL def is_test_final_release(self): return self.release_type == RELEASE_TYPE_TEST_FINAL def checkout_master(self): self._repo.heads.master.checkout() def checkout_test_master(self): self.checkout_test_branch(BRANCH_TEST_MASTER) def checkout_test_develop(self): self.checkout_test_branch(BRANCH_TEST_DEVELOP) def checkout_test_branch(self, branch_name): if branch_name in self._repo.heads: self._repo.delete_head(branch_name, ["-D"]) self._repo.create_head(branch_name) self._repo.heads[branch_name].checkout() def checkout_develop(self): self._repo.heads.develop.checkout() def merge_develop(self): self._repo.git.merge(BRANCH_DEVELOP) def merge_test_develop(self): self._repo.git.merge(BRANCH_TEST_DEVELOP) def build_and_test(project_root): try: build(project_root) test(project_root) except BuildException as e: fabric.utils.abort("{}".format(e.value)) except TestsFailedException as e: fabric.utils.abort("{}".format(e.value)) def read_cargo_file(release_context): with open(release_context.cargo_file) as cargo_file: cargo_content = contoml.loads(cargo_file.read()) return cargo_content['package']['version'], cargo_content['package']['name'] def confirm_version(release_context, current_version): confirmed_version = None presentation_version = to_presentation_version(release_context, current_version) while confirmed_version is None: # We confirm current_version if the user does not specify a version # because current_version may not be valid for the type of release the # user specified. input_version = raw_input('Set version [{}]: '.format(presentation_version)) or str(presentation_version) confirmed_version = is_valid_proposed_version(release_context, input_version) if confirmed_version is None: print("{} does not fit the semantic versioning spec or is not valid given the specified release type of {}." .format(input_version, release_context.release_type)) if release_context.is_snapshot_release(): return to_snapshot_release_version(confirmed_version) elif release_context.is_test_final_release(): return to_test_final_release_version(confirmed_version) else: return confirmed_version def to_presentation_version(release_context, version): if release_context.is_snapshot_release(): return to_snapshot_version(version) else: return to_final_release_version(version) def is_valid_proposed_version(release_context, proposed_version): valid_version = False sv = None if semantic_version.validate(proposed_version): sv = semantic_version.Version(proposed_version) if release_context.is_snapshot_release(): valid_version = sv.prerelease and sv.prerelease[0].upper() == SNAPSHOT else: valid_version = not sv.prerelease if valid_version: return sv else: return None def to_next_patch_snapshot_version(original_version): return semantic_version.Version( '{}.{}.{}-{}'.format( original_version.major, original_version.minor, original_version.patch + 1, SNAPSHOT ) ) def to_snapshot_version(original_version): return semantic_version.Version( '{}.{}.{}-{}'.format( original_version.major, original_version.minor, original_version.patch, SNAPSHOT ) ) def to_snapshot_release_version(original_version): return semantic_version.Version( '{}.{}.{}-{}'.format( original_version.major, original_version.minor, original_version.patch, local("git rev-parse --short HEAD", capture=True) ) ) def to_test_final_release_version(original_version): return semantic_version.Version( '{}.{}.{}-{}'.format( original_version.major, original_version.minor, original_version.patch, 'TESTFINALRELEASE' ) ) def to_final_release_version(original_version): return semantic_version.Version( '{}.{}.{}'.format( original_version.major, original_version.minor, original_version.patch ) ) def update_version_in_files(release_context, version): version_string = str(version) update_cargo_file_version(release_context, version_string) print 'Updated {} with the release version.'.format(release_context.cargo_file) def update_cargo_file_version(release_context, version): with open(release_context.cargo_file, 'r+') as cargo_file: cargo_content = contoml.loads(cargo_file.read()) cargo_content['package']['version'] = version cargo_content.dump(release_context.cargo_file) def _prep_bool_arg(arg): return bool(strtobool(str(arg)))