1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "semver>=2.13.0",
# "typer>=0.4.1",
# ]
# ///
import datetime
import os
import tomllib
from enum import Enum
import typer
from click.types import Choice
from semver import VersionInfo
class BumpType(str, Enum):
major = "major"
minor = "minor"
patch = "patch"
def get_current_version() -> VersionInfo:
with open("./pyproject.toml", "rb") as f:
pyproject = tomllib.load(f)
version = pyproject["project"]["version"]
return VersionInfo.parse(version)
def get_new_version(current_version: VersionInfo, bump_type: BumpType) -> VersionInfo:
if bump_type == BumpType.major:
return current_version.bump_major()
if bump_type == BumpType.minor:
return current_version.bump_minor()
if bump_type == BumpType.patch:
return current_version.bump_patch()
raise NotImplementedError("Unhandled bump type")
PYPROJECT_PATH = "./pyproject.toml"
def update_pyproject(current_version: VersionInfo, new_version: VersionInfo) -> None:
with open(PYPROJECT_PATH) as f:
content = f.read()
new_content = content.replace(
f'version = "{current_version}"', f'version = "{new_version}"'
)
if content == new_content:
typer.secho("Couldn't bump version in pyproject.toml", fg=typer.colors.RED)
raise typer.Exit(1)
with open(PYPROJECT_PATH, "w") as f:
f.write(new_content)
typer.secho("Version updated with success", fg=typer.colors.GREEN)
RELEASE_NOTE_PATH = "./__release_notes__.md"
def get_release_notes() -> str:
with open("./CHANGELOG.md", "r") as f:
while not f.readline().strip() == "## [Unreleased]":
pass
content = ""
while not (line := f.readline().strip()).startswith("## "):
content = content + line + "\n"
return content
def save_release_notes(release_notes: str) -> None:
if os.path.exists(RELEASE_NOTE_PATH):
typer.secho(
f"Release note file {RELEASE_NOTE_PATH} already exists", fg=typer.colors.RED
)
raise typer.Exit(1)
with open(RELEASE_NOTE_PATH, "w") as f:
f.write(release_notes)
typer.secho("Release note file generated with success", fg=typer.colors.GREEN)
CHANGELOG_PATH = "./CHANGELOG.md"
def update_changelog(current_version: VersionInfo, new_version: VersionInfo) -> None:
today = datetime.date.today()
date_str = f"{today.year}-{today.month:02d}-{today.day:02d}"
with open(CHANGELOG_PATH, "r") as f:
content = f.read()
# Add version header
content = content.replace(
"## [Unreleased]", ("## [Unreleased]\n\n" f"## [{new_version}] - {date_str}")
)
# Add version links
content = content.replace(
f"[unreleased]: https://github.com/art049/odmantic/compare/v{current_version}...HEAD",
(
f"[{new_version}]: https://github.com/art049/odmantic/compare/v{current_version}...v{new_version}\n"
f"[unreleased]: https://github.com/art049/odmantic/compare/v{new_version}...HEAD"
),
)
with open(CHANGELOG_PATH, "w") as f:
f.write(content)
typer.secho("Changelog updated with success", fg=typer.colors.GREEN)
VERSION_FILE_PATH = "__version__.txt"
def create_version_file(new_version: VersionInfo) -> None:
if os.path.exists(VERSION_FILE_PATH):
typer.secho(
f"Version file {VERSION_FILE_PATH} already exists", fg=typer.colors.RED
)
raise typer.Exit(1)
with open(VERSION_FILE_PATH, "w") as f:
f.write(str(new_version))
def summarize(
current_version: VersionInfo,
new_version: VersionInfo,
bump_type: BumpType,
release_notes: str,
) -> None:
typer.secho("Release summary:", fg=typer.colors.BLUE, bold=True)
typer.secho(f" Version bump: {bump_type.upper()}", bold=True)
typer.secho(f" Version change: {current_version} -> {new_version}", bold=True)
typer.confirm("Continue to release notes preview ?", abort=True, default=True)
release_header = typer.style(
f"RELEASE NOTE {new_version}\n\n", fg=typer.colors.BLUE, bold=True
)
typer.echo_via_pager(release_header + release_notes)
typer.confirm("Continue ?", abort=True, default=True)
def main() -> None:
current_version = get_current_version()
typer.secho(f"Current version: {current_version}", bold=True)
bump_type: BumpType = typer.prompt(
typer.style("Release type ?", fg=typer.colors.BLUE, bold=True),
type=Choice(list(BumpType.__members__)),
default=BumpType.patch.value,
show_choices=True,
)
new_version = get_new_version(current_version, bump_type)
release_notes = get_release_notes()
summarize(current_version, new_version, bump_type, release_notes)
save_release_notes(release_notes)
update_pyproject(current_version, new_version)
update_changelog(current_version, new_version)
create_version_file(new_version)
typer.confirm("Additionnal release commit files staged ?", abort=True, default=True)
if __name__ == "__main__":
typer.run(main)
|