Skip to content

Commit

Permalink
DEV: Automatically create release message / tag message (#2127)
Browse files Browse the repository at this point in the history
See #2125
  • Loading branch information
MartinThoma authored Aug 28, 2023
1 parent 63b8fee commit fe2dfaf
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 139 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ ignore = E203,E501,E741,W503,W604,N817,N814,VNE001,VNE002,VNE003,N802,SIM105,P10
exclude = build,sample-files,dist,.benchmarks,.git,.github,.mypy_cache,.pytest_cache,.tox
per-file-ignores =
tests/*: ASS001,PT011,B011,T001,T201
make_changelog.py:T001,T201
make_release.py:T001,T201
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ docs/meta/CONTRIBUTORS.md
extracted-images/

RELEASE_COMMIT_MSG.md
RELEASE_TAG_MSG.md
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ maint:
pyenv local 3.7.9
pip-compile -U requirements/docs.in

changelog:
python make_changelog.py
release:
python make_release.py
git commit -eF RELEASE_COMMIT_MSG.md

upload:
make clean
Expand Down
232 changes: 116 additions & 116 deletions docs/_static/releasing.drawio

Large diffs are not rendered by default.

Binary file modified docs/_static/releasing.drawio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/dev/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ you `git commit`.

Having a clean commit message helps people to quickly understand what the commit
was about, without actually looking at the changes. The first line of the
commit message is used to [auto-generate the CHANGELOG](https://github.com/py-pdf/pypdf/blob/main/make_changelog.py). For this reason, the format should be:
commit message is used to [auto-generate the CHANGELOG](https://github.com/py-pdf/pypdf/blob/main/make_release.py).
For this reason, the format should be:

```
PREFIX: DESCRIPTION
Expand Down
22 changes: 5 additions & 17 deletions docs/dev/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,16 @@ release.

The release contains the following steps:

1. Create the changelog with `python make_changelog.py` and adjust the `_version.py`
2. Create a release commit
3. Tag that commit
4. Push both
1. Update the CHANGELOG.md and the _version.py via `python make_release.py`.
This also prepares the release commit message
2. Create a release commit: `git commit -eF RELEASE_COMMIT_MSG.md`.
3. Tag that commit: `git tag -s {{version}} -eF RELEASE_TAG_MSG.md`.
4. Push both: `git push && git push --tags`
5. CI now builds a source and a wheels package which it pushes to PyPI. It also
creates a GitHub release.

![](../_static/releasing.drawio.png)

### The Release Commit

The release commit is used to create the GitHub release page. The structure of
it should be:

```
REL: {{ version }}
## What's new
{{ CHANGELOG }}
```

### The Release Tag

* Use the release version as the tag name. No need for a leading "v".
Expand Down
68 changes: 68 additions & 0 deletions make_changelog.py → make_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from datetime import datetime, timezone
from typing import List

from rich.prompt import Prompt


@dataclass(frozen=True)
class Change:
Expand All @@ -30,13 +32,17 @@ def main(changelog_path: str) -> None:
return

new_version = version_bump(git_tag)
new_version = get_version_interactive(new_version, changes)
adjust_version_py(new_version)

today = datetime.now(tz=timezone.utc)
header = f"## Version {new_version}, {today:%Y-%m-%d}\n"
url = f"https://github.com/py-pdf/pypdf/compare/{git_tag}...{new_version}"
trailer = f"\n[Full Changelog]({url})\n\n"
new_entry = header + changes + trailer
print(new_entry)
write_commit_msg_file(new_version, changes + trailer)
write_release_msg_file(new_version, changes + trailer, today)

# Make the script idempotent by checking if the new entry is already in the changelog
if new_entry in changelog:
Expand All @@ -45,6 +51,52 @@ def main(changelog_path: str) -> None:

new_changelog = "# CHANGELOG\n\n" + new_entry + strip_header(changelog)
write_changelog(new_changelog, changelog_path)
print_instructions(new_version)


def print_instructions(new_version: str) -> None:
"""Print release instructions."""
print("=" * 80)
print(f"☑ _version.py was adjusted to '{new_version}'")
print("☑ CHANGELOG.md was adjusted")
print("")
print("Now run:")
print(" git commit -eF RELEASE_COMMIT_MSG.md")
print(f" git tag -s {new_version} -eF RELEASE_TAG_MSG.md")
print(" git push")
print(" git push --tags")


def adjust_version_py(version: str) -> None:
"""Adjust the __version__ string."""
with open("pypdf/_version.py", "w") as fp:
fp.write(f'__version__ = "{version}"\n')


def get_version_interactive(new_version: str, changes: str) -> str:
"""Get the new __version__ interactively."""
print("The changes are:")
print(changes)
orig = new_version
new_version = Prompt.ask("New semantic version", default=orig)
while not is_semantic_version(new_version):
new_version = Prompt.ask(
"That was not a semantic version. Please enter a semantic version",
default=orig,
)
return new_version


def is_semantic_version(version: str) -> bool:
"""Check if the given version is a semantic version."""
# It's not so important to cover the edge-cases like pre-releases
# This is meant for pypdf only and we don't make pre-releases
if version.count(".") != 2:
return False
try:
return bool([int(part) for part in version.split(".")])
except Exception:
return False


def write_commit_msg_file(new_version: str, commit_changes: str) -> None:
Expand All @@ -61,6 +113,22 @@ def write_commit_msg_file(new_version: str, commit_changes: str) -> None:
fp.write(commit_changes)


def write_release_msg_file(
new_version: str, commit_changes: str, today: datetime
) -> None:
"""
Write a file that can be used as a git tag message.
Like this:
git tag -eF RELEASE_TAG_MSG.md && git push
"""
with open("RELEASE_TAG_MSG.md", "w") as fp:
fp.write(f"Version {new_version}, {today:%Y-%m-%d}\n\n")
fp.write("## What's new\n")
fp.write(commit_changes)


def strip_header(md: str) -> str:
"""Remove the 'CHANGELOG' header."""
return md.lstrip("# CHANGELOG").lstrip() # noqa
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ tests_dir = "tests/"
package = "./pypdf"

[tool.flit.sdist]
exclude = [".github/*", "docs/*", "resources/*", "sample-files/*", "sample-files/.github/*", "sample-files/.gitignore", "sample-files/.pre-commit-config.yaml", "requirements/*", "tests/*", ".flake8", ".gitignore", ".gitmodules", ".pylintrc", "tox.ini", "make_changelog.py", "mutmut-test.sh", ".pre-commit-config.yaml", ".gitblame-ignore-revs", "Makefile", "mutmut_config.py"]
exclude = [".github/*", "docs/*", "resources/*", "sample-files/*", "sample-files/.github/*", "sample-files/.gitignore", "sample-files/.pre-commit-config.yaml", "requirements/*", "tests/*", ".flake8", ".gitignore", ".gitmodules", ".pylintrc", "tox.ini", "make_release.py", "mutmut-test.sh", ".pre-commit-config.yaml", ".gitblame-ignore-revs", "Makefile", "mutmut_config.py"]

[tool.pytest.ini_options]
addopts = "--disable-socket"
Expand Down Expand Up @@ -201,7 +201,7 @@ max-complexity = 54 # Recommended: 10
"_writer.py" = ["S324"]
"docs/conf.py" = ["PTH", "INP001"]
"json_consistency.py" = ["T201"]
"make_changelog.py" = ["T201", "S603", "S607"]
"make_release.py" = ["T201", "S603", "S607"]
"pypdf/*" = ["N802", "N803"] # We first need to deprecate old stuff:
"sample-files/*" = ["D100", "INP001"]
"tests/*" = ["S101", "ANN001", "ANN201","D104", "S105", "S106", "D103", "B018", "B017"]
Expand Down

0 comments on commit fe2dfaf

Please sign in to comment.