From 444c4dd45af1da09e47c9a87a3f785082ed7ac9d Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 29 Apr 2022 09:59:55 -0400 Subject: [PATCH] contributing: Update VERSION file using commands (#2331) The script provides an action as sub-command and updates the VERSION file based on the input and the current state of the VERSION file and gives an error when the operation does not make sense with the given VERSION file content. * Supports switch to release, rc, dev versions and updates of major, minor, and micro numbers. * Can increase minor version by two with --dev for odd versions. * Suggest commit message based on the current release procedure. * General instructions on using the script included separately. * Applied in the release procedure. * Add Bash eval output for convenience in the release procedure. --- doc/howto_release.md | 55 ++++---- utils/update_version.md | 99 +++++++++++++ utils/update_version.py | 301 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 427 insertions(+), 28 deletions(-) create mode 100644 utils/update_version.md create mode 100755 utils/update_version.py diff --git a/doc/howto_release.md b/doc/howto_release.md index 746bf52946b..cc07d3aa4b1 100644 --- a/doc/howto_release.md +++ b/doc/howto_release.md @@ -11,21 +11,24 @@ ### Update VERSION file to release version number -Directly edit VERSION file in GH interface: +Modify the VERSION file use the dedicated script, for RC1, e.g.: - +```bash +./utils/update_version.py status +./utils/update_version.py rc 1 +``` -Example: +The script will compute the correct version string and print a message containing it into the terminal (e.g., "version: GRASS GIS 8.2.0RC1"). + +Commit with a commit message suggested by the script, e.g.: ```bash -8 -2 -0RC1 -2022 +git diff +git commit -m "version: GRASS GIS 8.2.0RC1" include/VERSION +git show +git push upstream ``` -Commit with version message, e.g. "GRASS GIS 8.2.0RC1". - ### Create release tag (For background, see ) @@ -43,22 +46,17 @@ git fetch --all --prune && git checkout releasebranch_8_2 && \ git merge upstream/releasebranch_8_2 && git push origin releasebranch_8_2 # create version env var for convenience: -MAJOR=`cat include/VERSION | head -1 | tail -1` -MINOR=`cat include/VERSION | head -2 | tail -1` -RELEASE=`cat include/VERSION | head -3 | tail -1` -VERSION=${MAJOR}.${MINOR}.${RELEASE} -echo $VERSION - -# RELEASETAG variable not really needed any more: -TODAY=`date +"%Y%m%d"` -RELEASETAG=release_${TODAY}_grass_${MAJOR}_${MINOR}_${RELEASE} -echo $RELEASETAG +# Get VERSION and TAG as variables. +eval `./update_version.py status --bash` ``` #### Tag release (on GitHub) +Version and tag are the same for all releases: + ```bash echo "$VERSION" +echo "$TAG" ``` To be done in GH interface: @@ -111,22 +109,23 @@ head ChangeLog_$VERSION gzip ChangeLog_$VERSION ``` -### Reset include/VERSION file to git version - -Directly edit VERSION file in GH interface: +### Reset include/VERSION file to git development version - +Use a dedicated script to edit the VERSION file, for RC1, e.g.: Example: ```bash -8 -0 -1dev -2021 +./utils/update_version.py dev ``` -Commit as "back to dev" +Commit with the suggested commit message and push, e.g.: + +```bash +git show +git commit include/VERSION -m "version: Back to 8.2.0dev" +git push upstream +``` Reset local copy to GH: diff --git a/utils/update_version.md b/utils/update_version.md new file mode 100644 index 00000000000..8f755a7e9c0 --- /dev/null +++ b/utils/update_version.md @@ -0,0 +1,99 @@ +# Updating Version File + +Version file (`include/VERSION`) can be updated using the _update_version.md_ script. + +The script captures the logic of updating the version file incorporating +the common actions and workflow checks. + +## Usage + +Use `--help` to get the information about actions available as sub-commands: + +```sh +./utils/update_version.py --help +``` + +Some sub-commands have short documentation on their own: + +```sh +./utils/update_version.py minor --help +``` + +All commands return YAML output on success and return non-zero return code on failure. + +## Examples + +### Checking Current Status + +The _status_ command prints content of the version file as YAML +and adds today's date and constructs a version string: + +```sh +./utils/update_version.py status +``` + +Example output: + +```yaml +today: 2022-04-27 +year: 2022 +major: 3 +minor: 2 +micro: 0dev +version: 3.2.0dev +``` + +Naturally, this also checks that the version if is accessible and fails otherwise. + +The _status_ command prints input for Bash _eval_ with `--bash`: + +```bash +eval `./utils/update_version.py status --bash` +echo $VERSION +``` + +### Updating Minor Version + +Let's say we are at development-only version 3.1.dev and just created +a new branch for 3.2 release, so we want to update the minor version +to the next minor version: + +```sh +./utils/update_version.py minor +``` + +Separately, or as part of other changes, now is the time to commit, +so the script suggests a commit message: + +```yaml +message: Use the provided title as a commit message +title: 'version: Start 3.2.0dev' +``` + +### Error Handling + +The commands detect invalid states and report error messages. +Continuing in the previous example, an attempt to increase +the micro version will fail: + +```sh +./utils/update_version.py micro +``` + +The error message explains the reason, a micro version should be increased +only after the release: + +```text +Already dev with micro '0dev'. Release first before update. +``` + +### Updating Development-only Version + +Development-only versions have odd minor version numbers and are never actually +released. Given the branching model, all these versions are on the _main_ branch, +so there the minor version is increased by two. This can be done by running +the _minor_ command twice or by using the `minor --dev`: + +```sh +./utils/update_version.py minor --dev +``` diff --git a/utils/update_version.py b/utils/update_version.py new file mode 100755 index 00000000000..db07c861a39 --- /dev/null +++ b/utils/update_version.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 + +"""Update VERSION file to release and development versions""" + +import sys +import datetime +from types import SimpleNamespace + +import argparse + + +def read_version_file(): + """Return version file content as object instance with attributes""" + with open("include/VERSION", encoding="utf-8") as file: + lines = file.read().splitlines() + return SimpleNamespace( + major=lines[0], minor=lines[1], micro=lines[2], year=lines[3] + ) + + +def write_version_file(major, minor, micro, year): + """Write version file content from object instance with attributes""" + with open("include/VERSION", "w", encoding="utf-8") as file: + file.write(f"{major}\n") + file.write(f"{minor}\n") + file.write(f"{micro}\n") + file.write(f"{year}\n") + + +def is_int(value): + """Return True if the *value* represents an integer, False otherwise""" + try: + int(value) + return True + except ValueError: + return False + + +def this_year(): + """Return current year""" + return datetime.date.today().year + + +def construct_version(version_info): + """Construct version string from version info""" + return f"{version_info.major}.{version_info.minor}.{version_info.micro}" + + +def suggest_commit_from_version_file(action): + """Using information in the version file, suggest a commit message""" + version_file = read_version_file() + suggest_commit(action, construct_version(version_file)) + + +def suggest_commit(action, version): + """Suggest a commit message for action and version""" + print("message: Use the provided title as a commit message") + print(f"title: 'version: {action} {version}'") + + +def release_candidate(args): + """Switch to RC""" + version_file = read_version_file() + micro = version_file.micro + if micro.endswith("dev"): + micro = micro[:-3] + if not micro: + sys.exit("Creating RC from a dev micro without number is not possible") + micro = f"{micro}RC{args.number}" + else: + sys.exit( + "Creating RC from a non-dev VERSION file " + f"with micro '{micro}' is not possible" + ) + write_version_file( + major=version_file.major, + minor=version_file.minor, + micro=micro, + year=this_year(), + ) + suggest_commit_from_version_file("GRASS GIS") + + +def release(_unused): + """Switch to release version""" + version_file = read_version_file() + micro = version_file.micro + if micro.endswith("dev"): + micro = micro[:-3] + if not micro: + micro = 0 + micro = f"{micro}" + else: + sys.exit("Creating a release from a non-dev VERSION file is not possible") + write_version_file( + major=version_file.major, + minor=version_file.minor, + micro=micro, + year=this_year(), + ) + suggest_commit_from_version_file("GRASS GIS") + + +def update_micro(_unused): + """Update to next micro version""" + version_file = read_version_file() + micro = version_file.micro + if micro == "dev": + sys.exit("The micro version does not increase with development-only versions.") + # We could also add micro version when not present, but requested with: + # micro = "0dev" + elif micro.endswith("dev"): + sys.exit(f"Already dev with micro '{micro}'. Release first before update.") + elif is_int(micro): + micro = int(version_file.micro) + 1 + micro = f"{micro}dev" + else: + if "RC" in micro: + sys.exit( + f"Updating micro for RC '{micro}' is not possible. " + "Release first before update." + ) + sys.exit(f"Unknown micro version in VERSION file: '{micro}'") + write_version_file( + major=version_file.major, + minor=version_file.minor, + micro=micro, + year=this_year(), + ) + suggest_commit_from_version_file("Start") + + +def update_minor(args): + """Update to next minor version""" + version_file = read_version_file() + micro = version_file.micro + minor = int(version_file.minor) + if args.dev: + if not minor % 2: + sys.exit( + "Updating to a development-only version " + f"from an even minor version '{minor}' is not possible" + ) + minor += 2 + else: + minor += 1 + if micro.endswith("dev"): + if minor % 2: + # Odd is development-only, never released and without micro version. + micro = "dev" + else: + # Even will be released, so adding micro version. + micro = "0dev" + else: + sys.exit("Updating version from a non-dev VERSION file is not possible") + write_version_file( + major=version_file.major, minor=minor, micro=micro, year=this_year() + ) + suggest_commit_from_version_file("Start") + + +def update_major(_unused): + """Update to next major version""" + version_file = read_version_file() + + micro = version_file.micro + if micro.endswith("dev"): + micro = "0dev" + else: + sys.exit("Updating version from a non-dev VERSION file is not possible") + minor = 0 + major = int(version_file.major) + 1 + write_version_file(major=major, minor=minor, micro=micro, year=this_year()) + suggest_commit_from_version_file("Start") + + +def back_to_dev(_unused): + """Switch version to development state""" + version_file = read_version_file() + micro = version_file.micro + if "RC" in micro: + micro = micro.split("RC")[0] + micro = f"{micro}dev" + action = "Back to" + elif is_int(micro): + micro = int(micro) + 1 + micro = f"{micro}dev" + action = "Start" + else: + if micro.endswith("dev"): + sys.exit(f"Already dev with micro '{micro}'") + sys.exit( + "Can switch to dev only from release or RC VERSION file, " + f"not from micro '{micro}'" + ) + write_version_file( + major=version_file.major, + minor=version_file.minor, + micro=micro, + year=this_year(), + ) + suggest_commit_from_version_file(action) + + +def status_as_yaml(version_info, today, version, tag): + """Print VERSION file and today's date as YAML""" + print(f"today: {today}") + print(f"year: {version_info.year}") + print(f"major: {version_info.major}") + print(f"minor: {version_info.minor}") + print(f"micro: {version_info.micro}") + print(f"version: {version}") + if tag: + print(f"tag: {version}") + + +def status_as_bash(version_info, today, version, tag): + """Print VERSION file and today's date as Bash eval variables""" + print(f"TODAY={today}") + print(f"YEAR={version_info.year}") + print(f"MAJOR={version_info.major}") + print(f"MINOR={version_info.minor}") + print(f"MICRO={version_info.micro}") + print(f"VERSION={version}") + if tag: + print(f"TAG={version}") + + +def status(args): + """Print VERSION file and today's date""" + version_info = read_version_file() + today = datetime.date.today().isoformat() + version = construct_version(version_info) + if not version_info.micro.endswith("dev"): + tag = version + else: + tag = None + if args.bash: + status_as_bash(version_info=version_info, today=today, version=version, tag=tag) + else: + status_as_yaml(version_info=version_info, today=today, version=version, tag=tag) + + +def main(): + """Translate sub-commands to function calls""" + parser = argparse.ArgumentParser( + description="Update VERSION file using the specified action.", + epilog="Run in the root directory to access the VERSION file.", + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + subparser = subparsers.add_parser( + "rc", help="switch to release candidate (no dev suffix)" + ) + subparser.add_argument( + "number", type=int, help="RC number (number sequence not checked)" + ) + subparser.set_defaults(func=release_candidate) + + subparser = subparsers.add_parser( + "dev", help="switch to development state (attaches dev suffix)" + ) + subparser.set_defaults(func=back_to_dev) + + subparser = subparsers.add_parser( + "release", help="switch to release version (no dev suffix)" + ) + subparser.set_defaults(func=release) + + subparser = subparsers.add_parser( + "major", help="increase major (X.y.z) version (attaches dev suffix)" + ) + subparser.set_defaults(func=update_major) + + subparser = subparsers.add_parser( + "minor", help="increase minor (x.Y.z) version (uses dev in micro)" + ) + subparser.add_argument( + "--dev", action="store_true", help="increase development-only version" + ) + subparser.set_defaults(func=update_minor) + + subparser = subparsers.add_parser( + "micro", help="increase micro (x.y.Z, aka patch) version (attaches dev suffix)" + ) + subparser.set_defaults(func=update_micro) + + subparser = subparsers.add_parser( + "status", help="show status of VERSION file (as YAML by default)" + ) + subparser.add_argument( + "--bash", action="store_true", help="format as Bash variables for eval" + ) + subparser.set_defaults(func=status) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main()