1+ """
2+ <Program Name>
3+ check_release.py
4+
5+ <Author>
6+ Lukas Puehringer <lukas.puehringer@nyu.edu>
7+
8+ <Started>
9+ Jan 3, 2020
10+
11+ <Copyright>
12+ See LICENSE for licensing information.
13+
14+ <Purpose>
15+ Check that specification updates are performed according to the versioning
16+ requirements in README.rst.
17+
18+ Expects Travis environment variables:
19+ - TRAVIS_BRANCH
20+ - TRAVIS_PULL_REQUEST_BRANCH
21+ (see https://docs.travis-ci.com/user/environment-variables/)
22+
23+ """
24+ import os
25+ import re
26+ import sys
27+ import shlex
28+ import datetime
29+ import subprocess
30+
31+ SPEC_NAME = "tuf-spec.md"
32+
33+ LAST_MODIFIED_PATTERN = "Last modified: **%d %B %Y**\n "
34+ LAST_MODIFIED_LINENO = 3
35+
36+ VERSION_PATTERN = r"^Version: \*\*(\d*)\.(\d*)\.(\d*)\*\*$"
37+ VERSION_LINENO = 5
38+
39+ class SpecError (Exception ):
40+ """Common error message part. """
41+ def __init__ (self , msg ):
42+ super ().__init__ (
43+ msg + " please see 'Versioning' section in README.rst for details." )
44+
45+
46+ def get_spec_head ():
47+ """Return the lines (as list) at the head of the file that contain last date
48+ modified and version. """
49+ with open (SPEC_NAME ) as spec_file :
50+ spec_head = [next (spec_file )
51+ for x in range (max (VERSION_LINENO , LAST_MODIFIED_LINENO ))]
52+
53+ return spec_head
54+
55+
56+ def get_date (spec_head ):
57+ """Parse date from spec head and return datetime object. """
58+ last_modified_line = spec_head [LAST_MODIFIED_LINENO - 1 ]
59+
60+ try :
61+ date = datetime .datetime .strptime (last_modified_line ,
62+ LAST_MODIFIED_PATTERN )
63+
64+ except ValueError as e :
65+ raise SpecError ("expected to match '{}' (datetime format) in line {}, but"
66+ " got '{}': {}." .format (LAST_MODIFIED_PATTERN , LAST_MODIFIED_LINENO ,
67+ last_modified_line , e ))
68+
69+ return date
70+
71+
72+ def get_version (spec_head ):
73+ """Parse version from spec head and return (major, minor, patch) tuple. """
74+ version_line = spec_head [VERSION_LINENO - 1 ]
75+
76+ version_match = re .search (VERSION_PATTERN , version_line )
77+ if not version_match :
78+ raise SpecError ("expected to match '{}' (regex) in line {}, but got '{}'."
79+ .format (VERSION_PATTERN , VERSION_LINENO , version_line ))
80+
81+ return version_match .groups ()
82+
83+
84+ def main ():
85+ """Check that the current branch is based off of the master branch and that
86+ the last modified date and version number in the specification document
87+ header are higher than in the master branch, i.e. were bumped. """
88+
89+ # Skip version and date comparison on push builds ...
90+ # As per https://docs.travis-ci.com/user/environment-variables/
91+ # if the current job is a push build, this [env] variable is empty ("")
92+ if not os .environ .get ("TRAVIS_PULL_REQUEST_BRANCH" ):
93+ print ("skipping version and date check for non pr builds ..." )
94+ sys .exit (0 )
95+
96+ # ... also skip on PRs that don't target the master branch
97+ # As per https://docs.travis-ci.com/user/environment-variables/:
98+ # for builds triggered by a pull request this [env variable] is the name of
99+ # the branch targeted by the pull request
100+ if not os .environ .get ("TRAVIS_BRANCH" ) == "master" :
101+ print ("skipping version and date for builds that don't target master ..." )
102+ sys .exit (0 )
103+
104+ # Check that the current branch is based off of the master branch
105+ try :
106+ subprocess .run (
107+ shlex .split ("git merge-base --is-ancestor master {}" .format (
108+ os .environ ["TRAVIS_PULL_REQUEST_BRANCH" ])), check = True )
109+
110+ except subprocess .CalledProcessError as e :
111+ raise SpecError ("make sure the current branch is based off of master" )
112+
113+ # Read the first few lines from the updated specification document and
114+ # extract date and version from spec file header in the current branch
115+ spec_head = get_spec_head ()
116+ date_new = get_date (spec_head )
117+ version_new = get_version (spec_head )
118+
119+ # Checkout master branch
120+ subprocess .run (shlex .split ("git checkout -q master" ), check = True )
121+
122+ # Read the first few lines from the previous specification document and
123+ # extract date and version from spec file header in the master branch
124+ spec_head = get_spec_head ()
125+ date_prev = get_date (spec_head )
126+ version_prev = get_version (spec_head )
127+
128+ # Assert date update
129+ if not date_new > date_prev :
130+ raise SpecError ("new 'last modified date' ({:%d %B %Y}) must be greater"
131+ " than the previous one ({:%d %B %Y})" .format (date_new , date_prev ))
132+
133+ # Assert version bump type depending on the PR originating branch
134+ # - if the originating branch is 'draft', it must be a major (x)or minor bump
135+ # - otherwise, it must be a patch bump
136+ if os .environ ["TRAVIS_PULL_REQUEST_BRANCH" ] == "draft" :
137+ if not (((version_new [0 ] > version_prev [0 ]) !=
138+ (version_new [1 ] > version_prev [1 ])) and
139+ (version_new [2 ] == version_prev [2 ])):
140+ raise SpecError ("new version ({}) must have exactly one of a greater"
141+ " major or a greater minor version number than the previous one ({}),"
142+ " if the PR originates from the 'draft' branch." .format (version_new ,
143+ version_prev ))
144+
145+ else :
146+ if not (version_new [:2 ] == version_prev [:2 ] and
147+ version_new [2 ] > version_prev [2 ]):
148+ raise SpecError ("new version ({}) must have exactly a greater patch"
149+ " version number than the previous one ({}), if the PR originates"
150+ " from a feature branch (i.e. not 'draft')" .format (version_new ,
151+ version_prev ))
152+
153+ print ("*" * 68 )
154+ print ("thanks for correctly bumping version and last modified date. :)" )
155+ print ("don't forget to tag the release and to sync 'draft' with master!! :P" )
156+ print ("*" * 68 )
157+
158+
159+ if __name__ == '__main__' :
160+ main ()
0 commit comments