From abbb9e67eb181b98207776727f5344c9ac7def68 Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Jun 19 2018 13:44:07 +0000 Subject: Update tag-release script Uses Python instead of Bash. Checks if new version correctly bumps major, minor or patch version number. Updates version in release notes. --- diff --git a/tag-release.py b/tag-release.py new file mode 100755 index 0000000..5f7c57d --- /dev/null +++ b/tag-release.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +""" +Tags new release in current git project. + +Expects release notes sections in following format (unindended). + + Project-Name 1.2.3 + ================== + +1. Verifies if new version follows from the last one found at top of the + release notes. + +2. Replaces section header for next release in release notes with project name + and version and adds release date. + +3. Updates version in spec files (".spec"). + +4. Updates "__version__" variable in "__init__.py" + ("/__init__.py"). + +5. Creates git commit. + +6. Adds GPG-signed tag ("-[prerelease]"). +""" +import argparse +import datetime +import fileinput +import locale +import logging +import re +import subprocess +import sys + +from calendar import month_abbr +from distutils.version import StrictVersion + +log = logging.getLogger(__name__) + +DEFAULT_RELEASE_NOTES_PATH = "docs/release-notes.rst" +DEFAULT_NEXT_RELEASE_TITLE = "Next Release" + + +class TimeLocaleC: + """ + Context manager for settings LC_TIME to C so date/time strings format are + always the same. + """ + oldlocale = None + + def __enter__(self): + self.oldlocale = locale.getlocale(locale.LC_TIME) + locale.setlocale(locale.LC_TIME, 'C') + + def __exit__(self, *args): + locale.setlocale(locale.LC_TIME, self.oldlocale) + + +def get_project_name_and_last_version(release_notes): + """ + Returns project name and last version from last entry in release notes + file. + """ + with open(release_notes, mode='r') as f: + previous_line = "" + for line in f: + if line.startswith("==="): + result = re.match(r'^([A-Za-z]\S*) (\d+\.\d+.*)$', previous_line) + if result: + return result[1], StrictVersion(result[2]) + previous_line = line + + raise RuntimeError( + 'Failed to find project name and last version in "{}"'.format(release_notes)) + + +def git_work_tree_has_modifications(): + """ + Returns True only if git working tree has modifications. + """ + completed_process = subprocess.run( + ["git", "diff-index", "--quiet", "HEAD", "--"], + stdout=subprocess.PIPE) + return completed_process.returncode != 0 + + +def update_release_notes(release_notes, name, version, next_release_title): + """ + Replaces section header for next release in release notes with project name + and version and adds release date. + """ + with fileinput.input(release_notes, inplace=True) as f: + line_iterator = iter(f) + for line in line_iterator: + if line == next_release_title + "\n": + break + else: + sys.stdout.write(line) + + try: + line = next(line_iterator) + assert line.startswith("===") + except StopIteration: + raise RuntimeError( + 'Failed to find "{}" notes in "{}"'.format(next_release_title, f)) + + release_notes_title = "{} {}".format(name, version) + sys.stdout.write(release_notes_title) + sys.stdout.write('\n') + + underline = "=" * len(release_notes_title) + sys.stdout.write(underline) + sys.stdout.write('\n') + + # Add paragrapth with release date (avoids using current locale). + release_date = datetime.date.today() + with TimeLocaleC(): + month = month_abbr[release_date.month] + sys.stdout.write("\nReleased {} {} {}.\n".format( + release_date.day, month, release_date.year)) + + for line in line_iterator: + sys.stdout.write(line) + + +def update_spec_file(spec_file, version, prerelease): + """ + Updates version and release in spec file. + """ + with fileinput.input(spec_file, inplace=True) as f: + for line in f: + if line.startswith('%global upstream_version '): + sys.stdout.write('%global upstream_version {}{}\n'.format( + version, prerelease)) + elif line.startswith('Version:'): + sys.stdout.write('Version: {}\n'.format(version)) + elif line.startswith('Release:'): + if prerelease: + release = '0.' + prerelease + else: + release = '1' + + sys.stdout.write('Release: {}%{{?dist}}\n'.format(release)) + else: + sys.stdout.write(line) + + +def update_init_file(init_file, version, prerelease): + """ + Updates "__version__" variable in "__init__.py" file. + """ + with fileinput.input(init_file, inplace=True) as f: + for line in f: + if line.startswith('__version__ = '): + sys.stdout.write("__version__ = '{}{}'\n".format( + version, prerelease)) + else: + sys.stdout.write(line) + + +def is_version_valid(version, last_version): + """ + Returns True only if new version follows properly the last version. + """ + for i in range(3): + if version.version[i] != last_version.version[i]: + return version.version[i] == last_version.version[i] + 1 and \ + all(version.version[j] == 0 for j in range(i + 1, 3)) + + return False + + +def test_is_version_valid(): + assert is_version_valid(StrictVersion("0.0.2"), StrictVersion("0.0.1")) + assert not is_version_valid(StrictVersion("0.0.1"), StrictVersion("0.0.1")) + assert not is_version_valid(StrictVersion("0.0.1"), StrictVersion("0.0.2")) + + assert is_version_valid(StrictVersion("0.1.0"), StrictVersion("0.0.1")) + assert is_version_valid(StrictVersion("0.2.0"), StrictVersion("0.1.0")) + assert not is_version_valid(StrictVersion("0.1.0"), StrictVersion("0.1.0")) + assert not is_version_valid(StrictVersion("0.1.0"), StrictVersion("1.0.0")) + assert not is_version_valid(StrictVersion("0.1.1"), StrictVersion("0.0.1")) + + assert is_version_valid(StrictVersion("1.0.0"), StrictVersion("0.0.1")) + assert not is_version_valid(StrictVersion("1.0.0"), StrictVersion("1.0.0")) + assert not is_version_valid(StrictVersion("1.0.0"), StrictVersion("2.0.0")) + assert not is_version_valid(StrictVersion("1.0.1"), StrictVersion("0.0.1")) + assert not is_version_valid(StrictVersion("1.1.0"), StrictVersion("0.0.1")) + assert not is_version_valid(StrictVersion("1.1.1"), StrictVersion("0.0.1")) + + +def main(): + parser = argparse.ArgumentParser( + description="Tag new release in current git project.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('version', help='new version, e.g. 1.2.3') + parser.add_argument( + 'prerelease', default='', nargs='?', + help='prerelease tag ') + parser.add_argument( + '--release-notes', default=DEFAULT_RELEASE_NOTES_PATH, + help='path to release notes') + parser.add_argument( + '--next-release-title', default=DEFAULT_NEXT_RELEASE_TITLE, + help='section name for the new release in release notes') + args = parser.parse_args() + + if git_work_tree_has_modifications(): + log.error("Work tree has modifications, stash or add before tagging") + sys.exit(1) + + name, last_version = get_project_name_and_last_version(args.release_notes) + version = StrictVersion(args.version) + + if not is_version_valid(version, last_version): + log.error( + "New version %s doesn't follow the last one %s", + version, last_version) + sys.exit(1) + + log.info("Updating to %s (previous was %s)", version, last_version) + + update_release_notes( + release_notes=args.release_notes, + name=name, + version=version, + next_release_title=args.next_release_title) + + spec_file = '{}.spec'.format(name.lower()) + update_spec_file(spec_file, version, args.prerelease) + + init_file = '{}/__init__.py'.format(name.lower()) + update_init_file(init_file, version, args.prerelease) + + subprocess.run( + ["git", "add", "--", args.release_notes, spec_file, init_file], check=True) + + commit_message = "Automatic commit of release {}{}".format( + version, args.prerelease) + subprocess.run(["git", "commit", "-m", commit_message], check=True) + + tag = "{}-{}{}".format(name.lower(), version, args.prerelease) + tag_message = "Tagging release {}{}".format(version, args.prerelease) + subprocess.run(["git", "tag", "-s", tag, "-m", tag_message], check=True) + + +if __name__ == "__main__": + main() diff --git a/tag-release.sh b/tag-release.sh deleted file mode 100755 index d177820..0000000 --- a/tag-release.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# SPDX-License-Identifier: GPL-2.0+ - -set -e - -version="$1" -prerelease="$2" - -if [ -z "$version" ] ; then - echo "Usage: $0 []" >&2 - echo "Example: $0 0.1 rc1" >&2 - exit 1 -fi - -if git status --porcelain | grep -q '^.M' ; then - echo "Work tree has modifications, stash or add before tagging" >&2 - exit 1 -fi - -sed -i -e "/%global upstream_version /c\%global upstream_version ${version}${prerelease}" greenwave.spec -sed -i -e "/^Version:/c\Version: $version" greenwave.spec -if [ -n "$prerelease" ] ; then - sed -i -e "/^Release:/c\Release: 0.$prerelease%{?dist}" greenwave.spec -else - sed -i -e "/^Release:/c\Release: 1%{?dist}" greenwave.spec -fi -sed -i -e "/^__version__ = /c\\__version__ = '$version$prerelease'" greenwave/__init__.py -git add greenwave.spec greenwave/__init__.py -git commit -m "Automatic commit of release $version$prerelease" -git tag -s "greenwave-$version$prerelease" -m "Tagging release $version$prerelease"