Dan Willemsen | adad21e | 2022-03-25 17:22:05 -0700 | [diff] [blame] | 1 | """ |
| 2 | Finalize the repo for a release. Invokes towncrier and bumpversion. |
| 3 | """ |
| 4 | |
| 5 | __requires__ = ['bump2version', 'towncrier'] |
| 6 | |
| 7 | |
| 8 | import subprocess |
| 9 | import pathlib |
| 10 | import re |
| 11 | import sys |
| 12 | |
| 13 | |
| 14 | def 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 | |
| 28 | bump_version_command = [ |
| 29 | sys.executable, |
| 30 | '-m', 'bumpversion', |
| 31 | release_kind(), |
| 32 | ] |
| 33 | |
| 34 | |
| 35 | def 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 | |
| 41 | def 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 | |
| 53 | def _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 | |
| 64 | def bump_version(): |
| 65 | cmd = bump_version_command + ['--allow-dirty'] |
| 66 | subprocess.check_call(cmd) |
| 67 | |
| 68 | |
| 69 | def ensure_config(): |
| 70 | """ |
| 71 | Double-check that Git has an e-mail configured. |
| 72 | """ |
| 73 | subprocess.check_output(['git', 'config', 'user.email']) |
| 74 | |
| 75 | |
| 76 | def 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 | |
| 97 | if __name__ == '__main__': |
| 98 | print("Cutting release at", get_version()) |
| 99 | ensure_config() |
| 100 | check_changes() |
| 101 | update_changelog() |
| 102 | bump_version() |