|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import re |
| 6 | + |
5 | 7 | from pathlib import Path |
6 | | -from subprocess import call, check_call |
| 8 | +from subprocess import call, check_call, run |
7 | 9 |
|
8 | 10 | from git import Commit, Remote, Repo, TagReference |
9 | 11 | from packaging.version import Version |
|
15 | 17 | CHANGELOG_FRAGMENTS_DIR = ROOT_SRC_DIR / 'docs' / 'changelog' |
16 | 18 | MAJOR_FRAGMENT_TYPES = frozenset({'removal'}) |
17 | 19 | MINOR_FRAGMENT_TYPES = frozenset({'feature', 'deprecation'}) |
| 20 | +CHECKED_FILE_COUNT = re.compile(r'(\d+) files? was checked') |
18 | 21 |
|
19 | 22 |
|
20 | 23 | def main(version_str: str, *, push: bool) -> None: |
@@ -80,20 +83,37 @@ def create_release_commit(repo: Repo, version: Version) -> Commit: |
80 | 83 | update_version_file(version) |
81 | 84 | print('build changelog from fragments with towncrier') |
82 | 85 | check_call(['towncrier', 'build', '--yes', '--version', version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S603 |
83 | | - # towncrier appends the issue reference past docstrfmt's width budget, so its raw output can run over the |
84 | | - # limit; reflow it here with a pinned docstrfmt instead of trusting the release job's isolated hook env, |
85 | | - # which passed over-long lines into 1.5.1 and left a changelog that failed pre-commit everywhere else. |
86 | | - check_call(['docstrfmt', '--line-length', '120', 'CHANGELOG.rst'], cwd=str(ROOT_SRC_DIR)) |
87 | 86 | call(['pre-commit', 'run', '--all-files'], cwd=str(ROOT_SRC_DIR)) |
88 | 87 | call(['pre-commit', 'run', '--all-files'], cwd=str(ROOT_SRC_DIR)) |
89 | 88 | repo.git.add('src/build/__init__.py', 'CHANGELOG.rst', 'docs/changelog/*') |
90 | 89 | check_call(['pre-commit', 'run', '--all-files', '--show-diff-on-failure'], cwd=str(ROOT_SRC_DIR)) |
91 | 90 | if repo.is_dirty(index=False, working_tree=True, untracked_files=False): |
92 | 91 | msg = 'Pre-commit hooks modified files after final run. This indicates an environment inconsistency.' |
93 | 92 | raise RuntimeError(msg) |
| 93 | + verify_changelog_formatted() |
94 | 94 | return repo.index.commit(f'chore: prepare for {version}') |
95 | 95 |
|
96 | 96 |
|
| 97 | +def verify_changelog_formatted() -> None: |
| 98 | + # 1.6.0 shipped a changelog that every other environment reformats: the release runner's docstrfmt inspected no |
| 99 | + # files and still exited 0, so the hook reported success. Require the check to name a file it actually looked at. |
| 100 | + # The width is the one the docstrfmt hook passes in .pre-commit-config.yaml. |
| 101 | + result = run( |
| 102 | + ['docstrfmt', '--check', '--ignore-cache', '--line-length', '120', 'CHANGELOG.rst'], |
| 103 | + capture_output=True, |
| 104 | + check=False, |
| 105 | + cwd=str(ROOT_SRC_DIR), |
| 106 | + encoding='utf-8', |
| 107 | + ) |
| 108 | + report = (result.stderr or result.stdout).strip() |
| 109 | + if result.returncode: |
| 110 | + msg = f'CHANGELOG.rst is not formatted the way the docstrfmt hook expects:\n{report}' |
| 111 | + raise RuntimeError(msg) |
| 112 | + if (checked := CHECKED_FILE_COUNT.search(report)) is None or not int(checked[1]): |
| 113 | + msg = f'docstrfmt inspected no files, leaving CHANGELOG.rst unverified:\n{report}' |
| 114 | + raise RuntimeError(msg) |
| 115 | + |
| 116 | + |
97 | 117 | def update_version_file(version: Version) -> None: |
98 | 118 | content = VERSION_FILE.read_text(encoding='utf-8') |
99 | 119 | lines = content.splitlines(keepends=True) |
|
0 commit comments