def bump(latest): merge_request_id = extract_merge_request_id_from_commit() labels = retrieve_labels_from_merge_request(merge_request_id) new_version = None print('MR Labels', labels) if "bump-major" in labels: print('Bump major') new_version = semver.bump_major(latest) elif "bump-minor" in labels: print('Bump minor') new_version = semver.bump_minor(latest) elif "bump-patch" in labels: print('Bump patch') new_version = semver.bump_patch(latest) elif "finalize-rc" in labels: print('Finalize rc') new_version = semver.finalize_version(latest) elif "bump-build": print('Bump build') new_version = semver.bump_build(new_version if new_version else latest) if "bump-rc" in labels and not "finalize-rc" in labels: print('Bump rc') new_version = semver.bump_prerelease( new_version if new_version else latest) new_version = semver.bump_build(new_version) return new_version if new_version else latest
def update_version() -> str: """ Retrieves the current version from github, bumps the version, and updates the values in service_config.json before committing to the dst branch :return: The new version. """ cur_version = get_current_version(args.stage) if args.stage == "prod": prv_version = get_current_version(stage='staging') new_version = semver.finalize_version(prv_version) elif args.stage == "staging": prv_version = get_current_version(stage='integration') assert '-integration' in prv_version new_version = prv_version.replace( '-integration', '-rc') # don't bump the version number else: new_version = getattr(semver, f'bump_{args.release}')(str(cur_version)) new_version = new_version if semver.parse_version_info(new_version).prerelease \ else semver.bump_prerelease(new_version, token='integration') if cur_version == new_version: print("Nothing to promote") exit(0) else: print(f"Upgrading: {cur_version} -> {new_version}") return new_version
def test_from_ancestor_version(self): bumped = "4.5.7-dev.1" old, new, updates = self.call( persist_from=[Constants.FROM_VCS_PREVIOUS_VERSION]) self.assertEqual( updates, { "VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": semver.finalize_version(bumped), }, )
def test_from_latest_of_all_time_release(self): bumped = "4.5.6" old, new, updates = self.call( persist_from=[Constants.FROM_VCS_LATEST_RELEASE]) self.assertEqual( updates, { "VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": semver.finalize_version(bumped), }, )
def get_final_version_string(release_mode, version): """Generates update dictionary entries for the version string""" production_version = semver.finalize_version(version) updates = {} if release_mode: updates[Constants.RELEASE_FIELD] = config.RELEASED_VALUE updates[Constants.VERSION_FIELD] = production_version updates[Constants.VERSION_STRICT_FIELD] = production_version else: updates[Constants.VERSION_FIELD] = version updates[Constants.VERSION_STRICT_FIELD] = production_version return updates
def test_should_finalize_version(): assert finalize_version('1.2.3') == '1.2.3' assert finalize_version('1.2.3-rc.5') == '1.2.3' assert finalize_version('1.2.3+build.2') == '1.2.3' assert finalize_version('1.2.3-rc.1+build.5') == '1.2.3' assert finalize_version('1.2.3-alpha') == '1.2.3' assert finalize_version('1.2.0') == '1.2.0'
def get_release_version(cls, version_file, next_git_tag_version): file_version = cls.read_version_file(version_file) if not file_version: return next_git_tag_version finalized_file_version = semver.finalize_version(file_version) result = semver.max_ver(finalized_file_version, next_git_tag_version) logging.info( 'Release version is %s (max of git-version `%s` and ' 'file-version `%s`)', result, next_git_tag_version, finalized_file_version) return result
def bump(self, what): if self.bumped: debug('version already bumped, don\'t do it twice') return if '=' in what: what, what_name = what.split('=') else: what, what_name = what, None old = semver.format_version(*self.version) types = ['major', 'minor', 'patch', 'prerelease', 'build'] if what == "last": # count how many non None parts there are parts = len([k for k in self.version if k is not None]) new = semver_bump[parts - 1](old) elif what == 'finalize': new = semver.finalize_version(old) elif what in types: if what_name: if what == 'prerelease': # to enable x.y.z-beta.1 -> x.y.z->rc.0 old_final = semver.finalize_version(old) new = semver_bump[types.index(what)](old_final, what_name) elif what in ['major', 'minor', 'patch']: # to go from 0.1.0 -> 0.2.0-beta1 new = semver_bump[types.index(what)](old) new = semver.bump_prerelease(new, what_name) else: new = semver_bump[types.index(what)](old, what_name) else: new = semver_bump[types.index(what)](old) else: error("unknown what: {}", what) info("version was {}, is now {}", old, new) self.version = [ k for k in semver.parse_version_info(new) if k is not None ] self.bumped = True
def next_semver(major, minor, prerelease=None, versions=None): # type: (int, int, str, list) -> str major = int(major) minor = int(minor) def stuff_we_care_about(v): """ we only care about stuff that: - has our same major+minor version - is a release OR has our same prerelease token """ if v.major != major: return False if v.minor != minor: return False if not prerelease: return True if v.prerelease is None: return True prefix = prerelease + "." if v.prerelease.startswith(prefix): # also needs to end in an int for us to care about it return bool(re.match("^\d+$", v.prerelease[len(prefix):])) return False semvers = [ VersionInfo.parse(v[1:] if v.startswith("v") else v) for v in versions or [] ] semvers = filter(stuff_we_care_about, semvers) if semvers: latest = sorted(semvers)[-1] version = str(latest) # do we bump patch? if not latest.prerelease: version = semver.bump_patch(version) # is this a final version? if prerelease: version = semver.bump_prerelease(version, prerelease) else: version = semver.finalize_version(version) else: # nothing exists on the current minor version so create a new one version = "%s.%s.0" % (major, minor) if prerelease: version += "-%s.1" % prerelease return "v" + version
def get_semver_from_source(data): """Given a dictionary of all version data available, determine the current version""" # get the not-none values from data known = { key: data.get(alias) for key, alias in config._forward_aliases.items() if data.get(alias) is not None } _LOG.debug("valid, mapped keys: %r", known) # prefer the non-strict field, if available, because it retains more information potentials = [ known.get(Constants.VERSION_FIELD, None), known.get(Constants.VERSION_STRICT_FIELD, None), ] # build from components, if they're defined from_components = {k: known.get(k) for k in SemVerSigFig if k in known} try: potentials.append(str(semver.VersionInfo(**from_components))) except TypeError: # we didn't have enough components pass versions = [ potential for potential in potentials if from_text_or_none(potential) ] release_versions = { semver.finalize_version(version) for version in versions } if len(release_versions) > 1: raise ValueError( "conflicting versions within project: %s\nkeys were: %r" % (release_versions, known)) if not versions: _LOG.debug("key pairs found: \n%r", known) raise ValueError("could not find existing semver") result = None if versions: result = versions[0] _LOG.info("latest version found in source: %r", result) return result
def postrelease(ctx, version): """ Finalise the release Running this task will: - bump the version to the next dev version - push changes to main """ clean_version = semver.finalize_version(version) if clean_version == version: # last release was full release, bump patch new_version = semver.bump_patch(version) + "-dev" else: # last release was prerelease, revert to dev version new_version = clean_version + "-dev" info(f"Bumping version numbers to {new_version} and committing") set_source_version(new_version)
def test_to_tag(self): """writes a tag in to git """ bumped = "5.0.0-dev.1" old, new, updates = self.call( persist_from=[Constants.FROM_VCS_LATEST_VERSION], persist_to=[Constants.TO_VCS], bump="major", ) self.addCleanup(subprocess.check_call, shlex.split("git tag --delete release/5.0.0-dev.1")) self.assertEqual( updates, { "VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": semver.finalize_version(bumped), }, ) version = auto_version_tool.get_dvcs_repo_latest_version_semver() self.assertEqual( dict(version._asdict()), dict(major=5, minor=0, patch=0, build=None, prerelease="dev.1"), )
def update_version(repo, src, dst) -> str: """ Retrieves the current version from github, bumps the version, and updates the values in service_config.json before committing to the dst branch :return: The new version. """ cur_version = get_current_version(repo, cmd_args.stage) if src == "prod": prv_version = get_current_version(repo, _stage=dst) _new_version = semver.finalize_version(prv_version) else: _new_version = getattr(semver, f"bump_{cmd_args.release}")(str(cur_version)) _new_version = (_new_version if semver.parse_version_info(_new_version).prerelease else semver.bump_prerelease(_new_version, token="rc")) if cur_version == _new_version: print("Nothing to promote") exit(0) else: print(f"Upgrading: {cur_version} -> {_new_version}") return _new_version
def test_should_finalize_version(version, expected): assert finalize_version(version) == expected
def main(set_to=None, commit_count_as=None, release=None, bump=None, lock=None, enable_file_triggers=None, incr_from_release=None, config_path=None, persist_from=None, persist_to=None, dry_run=None, **extra_updates): """Main workflow. Load config from cli and file Detect "bump triggers" - things that cause a version increment Find the current version Create a new version Write out new version and any other requested variables :param set_to: explicitly set semver to this version string :param commit_count_as: uses the commit count for the specified sigfig :param release: marks with a production flag just sets a single flag as per config :param bump: string indicating major/minor/patch more significant bumps will zero the less significant ones :param lock: locks the version string for the next call to autoversion lock only removed if a version bump would have occurred :param enable_file_triggers: whether to enable bumping based on file triggers bumping occurs once if any file(s) exist that match the config :param incr_from_release: dynamically generates the bump by comparing the proposed triggers for the current version, with the significance of the previous release to ensure e.g. adding new major changes to a prerelease should probably trigger a new major version specifically, the bump is: if (max trigger sigfig) > (max sigfig since release): (max trigger sigfig) else (min trigger sigfig) :param config_path: path to config file :param extra_updates: :return: """ updates = {} persist_to = persist_to or [Constants.TO_SOURCE] persist_from = persist_from or [Constants.FROM_SOURCE] load_config(config_path) all_data = {} last_release_semver = None if incr_from_release: if (Constants.FROM_VCS_PREVIOUS_VERSION in persist_from) or (Constants.FROM_VCS_PREVIOUS_RELEASE in persist_from): last_release_semver = get_dvcs_previous_release_semver() else: last_release_semver = get_dvcs_repo_latest_release_semver() _LOG.debug("found previous full release: %s", last_release_semver) current_semver = get_current_version(persist_from) release_commit = get_dvcs_commit_for_version(current_semver, persist_from) triggers = get_all_triggers(bump, enable_file_triggers, release_commit) updates.update(get_lock_behaviour(triggers, all_data, lock)) updates.update(get_dvcs_info()) new_version = current_semver if set_to: _LOG.debug("setting version directly: %s", set_to) # parse it - validation failure will raise a ValueError new_version = semver.parse_version_info(set_to) if not lock: warnings.warn( "After setting version manually, does it need locking for a CI flow, to avoid an extraneous increment?", UserWarning, ) elif triggers: # use triggers if the version is not set directly _LOG.debug("auto-incrementing version (triggers: %s)", triggers) overrides = get_overrides(updates, commit_count_as) new_version = utils.make_new_semver(current_semver, last_release_semver, triggers, **overrides) release_string = semver.finalize_version(str(new_version)) release_version = semver.parse_version_info(release_string) if release: new_version = release_version updates[Constants.RELEASE_FIELD] = config.RELEASED_VALUE updates[Constants.VERSION_FIELD] = release_string updates[Constants.VERSION_STRICT_FIELD] = release_string else: updates[Constants.VERSION_FIELD] = str(new_version) updates[Constants.VERSION_STRICT_FIELD] = release_string # write out the individual parts of the version updates.update(new_version._asdict()) # only rewrite a field that the user has specified in the configuration source_file_updates = { native: updates[key] for native, key in config.key_aliases.items() if key in updates } # finally, add in commandline overrides source_file_updates.update(extra_updates) if not dry_run: if Constants.TO_SOURCE in persist_to: write_targets(config.targets, **source_file_updates) if Constants.TO_VCS in persist_to: add_dvcs_tag(updates[Constants.VERSION_FIELD]) else: _LOG.warning("dry run: no changes were made") return str(current_semver), str(new_version), source_file_updates
def finalize(self): return semver.finalize_version(str(self))
#!/usr/bin/python3 import semver import functools import subprocess def isValid(tag): if not tag.startswith('v'): return False tag = tag[1:] try: return semver.match(tag, ">=0.0.0") except: return False tags = subprocess.run(["git", "tag", "-l"], capture_output=True, text=True).stdout.split() tags = map(lambda tag: tag[1:], filter(lambda tag: isValid(tag), tags)) tags = sorted(tags, key=functools.cmp_to_key(semver.compare)) stable_tags = list( filter(lambda tag: tag == semver.finalize_version(tag), tags)) print(stable_tags[-1]) print(tags[-1])
def make(ctx, check, version, initial_release, skip_sign, sign_only): """Perform a set of operations needed to release a single check: \b * update the version in __about__.py * update the changelog * update the requirements-agent-release.txt file * update in-toto metadata * commit the above changes You can release everything at once by setting the check to `all`. \b If you run into issues signing: \b - Ensure you did `gpg --import <YOUR_KEY_ID>.gpg.pub` """ # Import lazily since in-toto runs a subprocess to check for gpg2 on load from ..signing import update_link_metadata root = get_root() releasing_all = check == 'all' valid_checks = get_valid_checks() if not releasing_all and check not in valid_checks: abort('Check `{}` is not an Agent-based Integration'.format(check)) # don't run the task on the master branch if get_current_branch() == 'master': abort( 'This task will commit, you do not want to add commits to master directly' ) if releasing_all: if version: abort('You cannot bump every check to the same version') checks = sorted(valid_checks) else: checks = [check] if initial_release: version = '1.0.0' for check in checks: if sign_only: break elif initial_release and check in BETA_PACKAGES: continue # Initial releases will only bump if not already 1.0.0 so no need to always output if not initial_release: echo_success('Check `{}`'.format(check)) if version: # sanity check on the version provided cur_version = get_version_string(check) if version == 'final': # Remove any pre-release metadata version = finalize_version(cur_version) else: # Keep track of intermediate version bumps prev_version = cur_version for method in version.split(','): # Apply any supported version bumping methods. Chaining is required for going # from mainline releases to development releases since e.g. x.y.z > x.y.z-rc.A. # So for an initial bug fix dev release you can do `fix,rc`. if method in VERSION_BUMP: version = VERSION_BUMP[method](prev_version) prev_version = version p_version = parse_version_info(version) p_current = parse_version_info(cur_version) if p_version <= p_current: if initial_release: continue else: abort('Current version is {}, cannot bump to {}'.format( cur_version, version)) else: cur_version, changelog_types = ctx.invoke(changes, check=check, dry_run=True) if not changelog_types: echo_warning('No changes for {}, skipping...'.format(check)) continue bump_function = get_bump_function(changelog_types) version = bump_function(cur_version) if initial_release: echo_success('Check `{}`'.format(check)) # update the version number echo_info('Current version of check {}: {}'.format(check, cur_version)) echo_waiting('Bumping to {}... '.format(version), nl=False) update_version_module(check, cur_version, version) echo_success('success!') # update the CHANGELOG echo_waiting('Updating the changelog... ', nl=False) # TODO: Avoid double GitHub API calls when bumping all checks at once ctx.invoke(changelog, check=check, version=version, old_version=cur_version, initial=initial_release, quiet=True, dry_run=False) echo_success('success!') commit_targets = [check] # update the global requirements file if check not in NOT_CHECKS: commit_targets.append(AGENT_REQ_FILE) req_file = os.path.join(root, AGENT_REQ_FILE) echo_waiting('Updating the Agent requirements file... ', nl=False) update_agent_requirements( req_file, check, get_agent_requirement_line(check, version)) echo_success('success!') echo_waiting('Committing files...') # commit the changes. # do not use [ci skip] so releases get built https://docs.gitlab.com/ee/ci/yaml/#skipping-jobs msg = '[Release] Bumped {} version to {}'.format(check, version) git_commit(commit_targets, msg) if not initial_release: # Reset version version = None if sign_only or not skip_sign: echo_waiting('Updating release metadata...') echo_info( 'Please touch your Yubikey immediately after entering your PIN!') commit_targets = update_link_metadata(checks) git_commit(commit_targets, '[Release] Update metadata', force=True) # done echo_success( 'All done, remember to push to origin and open a PR to merge these changes on master' )
def make(ctx, checks, version, initial_release, skip_sign, sign_only, exclude, allow_master): """Perform a set of operations needed to release checks: \b * update the version in `__about__.py` * update the changelog * update the `requirements-agent-release.txt` file * update in-toto metadata * commit the above changes You can release everything at once by setting the check to `all`. \b If you run into issues signing: \b - Ensure you did `gpg --import <YOUR_KEY_ID>.gpg.pub` """ # Import lazily since in-toto runs a subprocess to check for gpg2 on load from ...signing import update_link_metadata, YubikeyException releasing_all = 'all' in checks valid_checks = get_valid_checks() if not releasing_all: for check in checks: if check not in valid_checks: abort(f'Check `{check}` is not an Agent-based Integration') if releasing_all: if version: abort('You cannot bump every check to the same version') checks = sorted(valid_checks) else: checks = sorted(checks) if exclude: for check in exclude.split(','): with suppress(ValueError): checks.remove(check) if initial_release: version = '1.0.0' repo_choice = ctx.obj['repo_choice'] core_workflow = repo_choice == 'core' if get_current_branch() == 'master' and (core_workflow or not allow_master): abort( 'Please create a release branch, you do not want to commit to master directly.' ) # Signing is done by a pipeline in a separate commit if not core_workflow and not sign_only: skip_sign = True # Keep track of the list of checks that have been updated. updated_checks = [] for check in checks: if sign_only: updated_checks.append(check) continue elif initial_release and check in BETA_PACKAGES: continue # Initial releases will only bump if not already 1.0.0 so no need to always output if not initial_release: echo_success(f'Check `{check}`') if version: # sanity check on the version provided cur_version = get_version_string(check) if version == 'final': # Remove any pre-release metadata version = finalize_version(cur_version) else: # Keep track of intermediate version bumps prev_version = cur_version for method in version.split(','): # Apply any supported version bumping methods. Chaining is required for going # from mainline releases to development releases since e.g. x.y.z > x.y.z-rc.A. # So for an initial bug fix dev release you can do `fix,rc`. if method in VERSION_BUMP: version = VERSION_BUMP[method](prev_version) prev_version = version p_version = parse_version_info(version) p_current = parse_version_info(cur_version) if p_version <= p_current: if initial_release: continue else: abort( f'Current version is {cur_version}, cannot bump to {version}' ) else: cur_version, changelog_types = ctx.invoke(changes, check=check, dry_run=True) if not changelog_types: echo_warning(f'No changes for {check}, skipping...') continue bump_function = get_bump_function(changelog_types) version = bump_function(cur_version) if initial_release: echo_success(f'Check `{check}`') # update the version number echo_info(f'Current version of check {check}: {cur_version}') echo_waiting(f'Bumping to {version}... ', nl=False) update_version_module(check, cur_version, version) echo_success('success!') # update the CHANGELOG echo_waiting('Updating the changelog... ', nl=False) # TODO: Avoid double GitHub API calls when bumping all checks at once ctx.invoke( changelog, check=check, version=version, old_version=cur_version, initial=initial_release, quiet=True, dry_run=False, ) echo_success('success!') commit_targets = [check] updated_checks.append(check) # update the list of integrations to be shipped with the Agent if repo_choice == 'core' and check not in NOT_CHECKS: req_file = get_agent_release_requirements() commit_targets.append(os.path.basename(req_file)) echo_waiting('Updating the Agent requirements file... ', nl=False) update_agent_requirements( req_file, check, get_agent_requirement_line(check, version)) echo_success('success!') echo_waiting('Committing files...') # commit the changes. # do not use [ci skip] so releases get built https://docs.gitlab.com/ee/ci/yaml/#skipping-jobs msg = f'[Release] Bumped {check} version to {version}' git_commit(commit_targets, msg, update=True) if not initial_release: # Reset version version = None if sign_only or not skip_sign: if not updated_checks: abort('There are no new checks to sign and release!') echo_waiting('Updating release metadata...') if core_workflow: echo_info( 'Please touch your Yubikey immediately after entering your PIN!' ) try: commit_targets = update_link_metadata(updated_checks, core_workflow=core_workflow) git_commit(commit_targets, '[Release] Update metadata', force=True) except YubikeyException as e: abort(f'A problem occurred while signing metadata: {e}') # done echo_success( 'All done, remember to push to origin and open a PR to merge these changes on master' )