From aa9e94c26e36f00747273d6709e22e9af1bb0aeb Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 4 Feb 2020 12:20:17 +0100 Subject: [PATCH 1/3] Add spec version- and release mgmt instructions - Add gitflow-based version- and release management instructions to the Versioning section of the README. - Add links to the "latest stable", "current draft", "new changes since latest stable" and "release history" of the specification to the header of the README document. Note that the version number of the latest stable version of the specification is no longer shown in the README, which eliminates the need to update the README with every new release. --- README.rst | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 31fc544..aeb1a8f 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,10 @@ The Update Framework specification ---------------------------------- -Latest: `version 1.0.0 `_ +- `latest stable `_ +- `current draft `_ +- `new changes since latest stable `_ +- `release history `_ Contact @@ -31,7 +34,33 @@ Versioning ---------- The TUF specification uses `Semantic Versioning 2.0.0 `_ -for its version numbers. +(semver) for its version numbers, and a gitflow-based release management: + +- The 'master' branch of this repository always points to the latest stable + version of the specification. +- The 'draft' branch of this repository always points to the latest development + version of the specification and must always be based off of the latest + 'master' branch. +- Contributors must submit changes as pull requests against these branches, + depending on the type of the change (see semver rules). +- For patch-type changes, pull requests may be submitted directly against the + 'master' branch. +- For major- and minor-type changes, pull requests must be submitted against + the 'draft' branch. +- Maintainers may, from time to time, decide that the 'draft' branch is ready + for a new major or minor release, and submit a pull request from 'draft' + against 'master'. +- Before merging a branch with 'master' the 'last modified date' and 'version' + in the specification header must be bumped. +- Merges with 'master' that originate from the 'draft' branch must bump either + the major or minor version number. +- Merges with 'master' that originate from any other branch must bump the patch + version number. +- Merges with 'master' must be followed by a git tag for the new version + number. +- Merges with 'master' must be followed by a rebase of 'draft' onto 'master'. + + Acknowledgements ---------------- From 19c0ebce462e6bc5b455a67d1a2386fc6662e38f Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 4 Feb 2020 12:26:36 +0100 Subject: [PATCH 2/3] Add check release script Add script to run on Travis upon a GitHub pull request, that checks that versioning- and release management rules are followed. --- check_release.py | 160 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 check_release.py diff --git a/check_release.py b/check_release.py new file mode 100644 index 0000000..af75b0a --- /dev/null +++ b/check_release.py @@ -0,0 +1,160 @@ +""" + + check_release.py + + + Lukas Puehringer + + + Jan 3, 2020 + + + See LICENSE for licensing information. + + + Check that specification updates are performed according to the versioning + requirements in README.rst. + + Expects Travis environment variables: + - TRAVIS_BRANCH + - TRAVIS_PULL_REQUEST_BRANCH + (see https://docs.travis-ci.com/user/environment-variables/) + +""" +import os +import re +import sys +import shlex +import datetime +import subprocess + +SPEC_NAME = "tuf-spec.md" + +LAST_MODIFIED_PATTERN = "Last modified: **%d %B %Y**\n" +LAST_MODIFIED_LINENO = 3 + +VERSION_PATTERN = r"^Version: \*\*(\d*)\.(\d*)\.(\d*)\*\*$" +VERSION_LINENO = 5 + +class SpecError(Exception): + """Common error message part. """ + def __init__(self, msg): + super().__init__( + msg + " please see 'Versioning' section in README.rst for details.") + + +def get_spec_head(): + """Return the lines (as list) at the head of the file that contain last date + modified and version. """ + with open(SPEC_NAME) as spec_file: + spec_head = [next(spec_file) + for x in range(max(VERSION_LINENO, LAST_MODIFIED_LINENO))] + + return spec_head + + +def get_date(spec_head): + """Parse date from spec head and return datetime object. """ + last_modified_line = spec_head[LAST_MODIFIED_LINENO - 1] + + try: + date = datetime.datetime.strptime(last_modified_line, + LAST_MODIFIED_PATTERN) + + except ValueError as e: + raise SpecError("expected to match '{}' (datetime format) in line {}, but" + " got '{}': {}.".format(LAST_MODIFIED_PATTERN, LAST_MODIFIED_LINENO, + last_modified_line, e)) + + return date + + +def get_version(spec_head): + """Parse version from spec head and return (major, minor, patch) tuple. """ + version_line = spec_head[VERSION_LINENO - 1] + + version_match = re.search(VERSION_PATTERN, version_line) + if not version_match: + raise SpecError("expected to match '{}' (regex) in line {}, but got '{}'." + .format(VERSION_PATTERN, VERSION_LINENO, version_line)) + + return version_match.groups() + + +def main(): + """Check that the current branch is based off of the master branch and that + the last modified date and version number in the specification document + header are higher than in the master branch, i.e. were bumped. """ + + # Skip version and date comparison on push builds ... + # As per https://docs.travis-ci.com/user/environment-variables/ + # if the current job is a push build, this [env] variable is empty ("") + if not os.environ.get("TRAVIS_PULL_REQUEST_BRANCH"): + print("skipping version and date check for non pr builds ...") + sys.exit(0) + + # ... also skip on PRs that don't target the master branch + # As per https://docs.travis-ci.com/user/environment-variables/: + # for builds triggered by a pull request this [env variable] is the name of + # the branch targeted by the pull request + if not os.environ.get("TRAVIS_BRANCH") == "master": + print("skipping version and date for builds that don't target master ...") + sys.exit(0) + + # Check that the current branch is based off of the master branch + try: + subprocess.run( + shlex.split("git merge-base --is-ancestor master {}".format( + os.environ["TRAVIS_PULL_REQUEST_BRANCH"])), check=True) + + except subprocess.CalledProcessError as e: + raise SpecError("make sure the current branch is based off of master") + + # Read the first few lines from the updated specification document and + # extract date and version from spec file header in the current branch + spec_head = get_spec_head() + date_new = get_date(spec_head) + version_new = get_version(spec_head) + + # Checkout master branch + subprocess.run(shlex.split("git checkout -q master"), check=True) + + # Read the first few lines from the previous specification document and + # extract date and version from spec file header in the master branch + spec_head = get_spec_head() + date_prev = get_date(spec_head) + version_prev = get_version(spec_head) + + # Assert date update + if not date_new > date_prev: + raise SpecError("new 'last modified date' ({:%d %B %Y}) must be greater" + " than the previous one ({:%d %B %Y})".format(date_new, date_prev)) + + # Assert version bump type depending on the PR originating branch + # - if the originating branch is 'draft', it must be a major (x)or minor bump + # - otherwise, it must be a patch bump + if os.environ["TRAVIS_PULL_REQUEST_BRANCH"] == "draft": + if not (((version_new[0] > version_prev[0]) != + (version_new[1] > version_prev[1])) and + (version_new[2] == version_prev[2])): + raise SpecError("new version ({}) must have exactly one of a greater" + " major or a greater minor version number than the previous one ({})," + " if the PR originates from the 'draft' branch.".format(version_new, + version_prev)) + + else: + if not (version_new[:2] == version_prev[:2] and + version_new[2] > version_prev[2]): + raise SpecError("new version ({}) must have exactly a greater patch" + " version number than the previous one ({}), if the PR originates" + " from a feature branch (i.e. not 'draft')".format(version_new, + version_prev)) + + print("*"*68) + print("thanks for correctly bumping version and last modified date. :)") + print("don't forget to tag the release and to sync 'draft' with master!! :P") + print("*"*68) + + +if __name__ == '__main__': + main() \ No newline at end of file From 8c2af51f7b85dafc690e8424bb15931f4e75fa86 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 4 Feb 2020 13:51:35 +0100 Subject: [PATCH 3/3] Configure travis to run check_release.py --- .travis.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1037e43 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: python + +# Disable auto-cloning and ... +git: + clone: false + +# ... instead manually fetch and checkout the pull request source branch, which +# is expected by check_release.py +# https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/checking-out-pull-requests-locally +install: + - git clone --depth=50 https://github.com/${TRAVIS_REPO_SLUG}.git ${TRAVIS_REPO_SLUG} + - cd ${TRAVIS_REPO_SLUG} + - git fetch origin pull/${TRAVIS_PULL_REQUEST}/head:${TRAVIS_PULL_REQUEST_BRANCH} + - git checkout -qf ${TRAVIS_PULL_REQUEST_BRANCH} + +script: + - python check_release.py