Files
Spoolman/spoolman/bump.py
T
Donkie 7e353595c0 bump.py: keep client_v2's version in sync with the release
The bump script has only ever touched client/package.json, so
client_v2 has sat at 0.0.1 since it was created. Nothing reads it --
the footer takes the version from GET /info -- but both clients ship in
every release, and a package that claims 0.0.1 while the release says
0.25.0 is confusing to anyone who looks.

The indent has to be per-client: each tree is formatted by its own
prettier config and client_v2 uses tabs, so writing 2 spaces back would
make `npm run lint` fail on the release commit itself.

Also brings client_v2 up to the current version so the first bump after
this is a normal increment rather than a jump from 0.0.1.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:59:53 +02:00

146 lines
5.2 KiB
Python

"""A python script that bumps the version number of a project."""
# ruff: noqa: PLR2004, T201, S607
import json
import os
import re
import subprocess
import sys
from pathlib import Path
def bump() -> None:
"""Bump the version number of the project."""
project_root = Path(__file__).parent.parent
if len(sys.argv) < 2:
print("Please specify a bump type, e.g. major, minor, micro.")
sys.exit(1)
if subprocess.run(["git", "diff", "--quiet", "pyproject.toml"], cwd=project_root, check=False).returncode != 0:
print("The pyproject.toml file is dirty, please commit your changes before bumping the version number.")
sys.exit(1)
if subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=project_root, check=False).returncode != 0:
print("There are staged changes, please commit them before bumping the version number.")
sys.exit(1)
# Bump the version number by editing pyproject.toml directly.
bump_type = sys.argv[1]
new_version = _bump_pyproject(project_root, bump_type)
# Update the version number in both node projects. The indent differs because each
# client formats its own tree with its own prettier config (client_v2 uses tabs);
# writing the other one back would make `npm run lint` fail on a release commit.
for client_dir, indent in (("client", " "), ("client_v2", "\t")):
_update_node_pkg_version(project_root, client_dir, new_version, indent)
# Run uv lock to update the lock file
subprocess.run(["uv", "lock"], check=True)
# Regenerate requirements.txt from uv so bare-metal / Moonraker update_manager
# installs (which pip install -r requirements.txt) stay in sync with uv.
subprocess.run(
["uv", "export", "--no-dev", "-o", "requirements.txt", "--no-annotate", "--no-hashes"],
cwd=project_root,
check=True,
)
# Stage the changed files
subprocess.run(
[
"git",
"add",
"pyproject.toml",
"uv.lock",
"requirements.txt",
"client/package.json",
"client/package-lock.json",
"client_v2/package.json",
"client_v2/package-lock.json",
],
cwd=project_root,
check=True,
)
# Commit the changes
subprocess.run(["git", "commit", "-m", f"Bump version to {new_version}"], cwd=project_root, check=True)
# Tag the commit, prefix with "v"
subprocess.run(["git", "tag", f"v{new_version}"], cwd=project_root, check=True)
# Notify user that the process is complete
print(f"Bumped version to {new_version}.")
def _update_node_pkg_version(project_root: Path, client_dir: str, new_version: str, indent: str) -> None:
"""Set the version of one of the client packages and refresh its lock file.
Both clients ship in every release, so both track the Spoolman version even
though neither reads it at runtime (the footer takes the version from
``GET /info``). Keeping them in sync stops the packages from drifting into
something confusing to read.
"""
package_json = project_root / client_dir / "package.json"
with package_json.open("r") as f:
node_package = json.load(f)
node_package["version"] = new_version
with package_json.open("w") as f:
json.dump(node_package, f, indent=indent)
f.write("\n") # Ensure file ends with a newline, needed by prettier
# Run npm install to update the lock file with new version
# On windows, shell=True is required for npm to be found
if os.name == "nt":
subprocess.run(["npm", "install"], cwd=project_root / client_dir, check=True, shell=True) # noqa: S602
else:
subprocess.run(["npm", "install"], cwd=project_root / client_dir, check=True)
def _bump_pyproject(project_root: Path, bump_type: str) -> str:
"""Bump the version number in pyproject.toml. Returns the new version."""
pyproject_path = project_root.joinpath("pyproject.toml")
try:
pyproject_text = pyproject_path.read_text()
except OSError:
print("Failed to read pyproject.toml to determine current version.")
sys.exit(1)
# Parse current version expecting MAJOR.MINOR.PATCH (digits only)
version_match = re.search(
r'^\s*version\s*=\s*"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"',
pyproject_text,
re.MULTILINE,
)
if version_match is None:
print("Failed to parse current version from pyproject.toml.")
sys.exit(1)
major = int(version_match.group("major"))
minor = int(version_match.group("minor"))
patch = int(version_match.group("patch"))
if bump_type == "major":
major += 1
minor = 0
patch = 0
elif bump_type == "minor":
minor += 1
patch = 0
elif bump_type in ("micro", "patch"):
patch += 1
else:
print("Unknown bump type. Use 'major', 'minor' or 'micro'.")
sys.exit(1)
new_version = f"{major}.{minor}.{patch}"
# Replace only the first occurrence of the version line in pyproject.toml
pattern = re.compile(r"^\s*version\s*=.*$", re.MULTILINE)
new_line = f'version = "{new_version}"'
new_pyproject_text = pattern.sub(new_line, pyproject_text, count=1)
pyproject_path.write_text(new_pyproject_text)
return new_version