1""" 2Finalize the repo for a release. Invokes towncrier and bumpversion. 3""" 4 5__requires__ = ['bump2version', 'towncrier'] 6 7 8import subprocess 9import pathlib 10import re 11import sys 12 13 14def release_kind(): 15 """ 16 Determine which release to make based on the files in the 17 changelog. 18 """ 19 # use min here as 'major' < 'minor' < 'patch' 20 return min( 21 'major' if 'breaking' in file.name else 22 'minor' if 'change' in file.name else 23 'patch' 24 for file in pathlib.Path('changelog.d').iterdir() 25 ) 26 27 28bump_version_command = [ 29 sys.executable, 30 '-m', 'bumpversion', 31 release_kind(), 32] 33 34 35def get_version(): 36 cmd = bump_version_command + ['--dry-run', '--verbose'] 37 out = subprocess.check_output(cmd, text=True) 38 return re.search('^new_version=(.*)', out, re.MULTILINE).group(1) 39 40 41def update_changelog(): 42 cmd = [ 43 sys.executable, '-m', 44 'towncrier', 45 'build', 46 '--version', get_version(), 47 '--yes', 48 ] 49 subprocess.check_call(cmd) 50 _repair_changelog() 51 52 53def _repair_changelog(): 54 """ 55 Workaround for #2666 56 """ 57 changelog_fn = pathlib.Path('CHANGES.rst') 58 changelog = changelog_fn.read_text() 59 fixed = re.sub(r'^(v[0-9.]+)v[0-9.]+$', r'\1', changelog, flags=re.M) 60 changelog_fn.write_text(fixed) 61 subprocess.check_output(['git', 'add', changelog_fn]) 62 63 64def bump_version(): 65 cmd = bump_version_command + ['--allow-dirty'] 66 subprocess.check_call(cmd) 67 68 69def ensure_config(): 70 """ 71 Double-check that Git has an e-mail configured. 72 """ 73 subprocess.check_output(['git', 'config', 'user.email']) 74 75 76def check_changes(): 77 """ 78 Verify that all of the files in changelog.d have the appropriate 79 names. 80 """ 81 allowed = 'deprecation', 'breaking', 'change', 'doc', 'misc' 82 except_ = 'README.rst', '.gitignore' 83 news_fragments = ( 84 file 85 for file in pathlib.Path('changelog.d').iterdir() 86 if file.name not in except_ 87 ) 88 unrecognized = [ 89 str(file) 90 for file in news_fragments 91 if not any(f".{key}" in file.suffixes for key in allowed) 92 ] 93 if unrecognized: 94 raise ValueError(f"Some news fragments have invalid names: {unrecognized}") 95 96 97if __name__ == '__main__': 98 print("Cutting release at", get_version()) 99 ensure_config() 100 check_changes() 101 update_changelog() 102 bump_version() 103