Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.0.19
current_version = 1.1.10
commit = True
tag = True
tag_name = v{new_version}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ jobs:
- name: Commit version bump
run: |
git add etsy_python/_version.py
# bump_version.py also syncs .bumpversion.cfg; stage it so the two
# versions stay consistent in git (not just on the runner's disk).
git add .bumpversion.cfg
# Always skip CI for version bump commits to prevent infinite loops
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} [skip ci]"

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ cp specs/latest.json specs/baseline.json

**Single source of truth**: `etsy_python/_version.py`

`setup.py` reads version dynamically from `_version.py`. The `.bumpversion.cfg` file tracks version for the bump tool but can drift out of sync -- always trust `_version.py`.
`setup.py` reads version dynamically from `_version.py`, which remains the single source of truth -- always trust `_version.py`. The `.bumpversion.cfg` file tracks version for the `bump2version` tool (which CI does not run); `scripts/bump_version.py` now keeps its `current_version` synced automatically on every bump, and the publish workflow commits both files together, so the two should no longer drift.

A pre-commit hook (`.pre-commit-config.yaml`) runs `scripts/check_version_consistency.py` to validate `_version.py` and `.bumpversion.cfg` agree.

Expand Down
38 changes: 35 additions & 3 deletions scripts/bump_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,36 @@ def write_version(version_file, new_version):
content = f'''"""Version information for etsy-python package."""

__version__ = "{new_version}"'''

with open(version_file, 'w') as f:
f.write(content)


def sync_bumpversion_cfg(cfg_file, new_version):
"""Update ``current_version`` in .bumpversion.cfg to match _version.py.

_version.py is the source of truth (see CLAUDE.md); .bumpversion.cfg is only
read by the bump2version tool, which CI does not run. Keeping the two in sync
here stops check_version_consistency.py from flapping after every auto-bump.

Returns True if the file was updated, False if it is absent (nothing to do).
"""
if not cfg_file.exists():
return False

content = cfg_file.read_text()
new_content, count = re.subn(
r'(?m)^(current_version\s*=\s*).*$',
rf'\g<1>{new_version}',
content,
)
if count == 0:
raise ValueError(f"Could not find current_version in {cfg_file}")

cfg_file.write_text(new_content)
return True


def get_latest_commit_message():
"""Get the latest commit message to determine bump type."""
import subprocess
Expand Down Expand Up @@ -126,7 +151,8 @@ def main():
script_dir = Path(__file__).parent
project_root = script_dir.parent
version_file = project_root / 'etsy_python' / '_version.py'

cfg_file = project_root / '.bumpversion.cfg'

if not version_file.exists():
print(f"Error: Version file not found at {version_file}")
sys.exit(1)
Expand Down Expand Up @@ -156,11 +182,17 @@ def main():
# Write new version
write_version(version_file, new_version)
print(f"Version updated to {new_version}")


# Keep .bumpversion.cfg in sync so the consistency check doesn't drift
if sync_bumpversion_cfg(cfg_file, new_version):
print(f".bumpversion.cfg synced to {new_version}")

# Output new version for GitHub Actions
print(f"::set-output name=version::{new_version}")
else:
print("Dry run - no changes made")
if cfg_file.exists():
print(f"Dry run - would sync .bumpversion.cfg to {new_version}")

return 0

Expand Down
104 changes: 104 additions & 0 deletions tests/test_bump_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Tests for scripts/bump_version.py — version arithmetic and the
.bumpversion.cfg sync that keeps it consistent with _version.py.
"""

import sys
from pathlib import Path

import pytest

# scripts/ is not a package; put it on the path so we can import the tool.
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))

import bump_version # noqa: E402


# --------------------------------------------------------------------------- #
# bump_version — arithmetic
# --------------------------------------------------------------------------- #
class TestBumpVersion:
@pytest.mark.parametrize(
"current,bump_type,expected",
[
("1.1.10", "patch", "1.1.11"),
("1.1.10", "minor", "1.2.0"),
("1.1.10", "major", "2.0.0"),
("0.0.0", "patch", "0.0.1"),
("9.9.9", "major", "10.0.0"),
],
)
def test_bump(self, current, bump_type, expected):
assert bump_version.bump_version(current, bump_type) == expected

def test_invalid_bump_type_raises(self):
with pytest.raises(ValueError):
bump_version.bump_version("1.0.0", "sideways")

def test_malformed_version_raises(self):
with pytest.raises(ValueError):
bump_version.parse_version("1.0")


# --------------------------------------------------------------------------- #
# sync_bumpversion_cfg
# --------------------------------------------------------------------------- #
class TestSyncBumpversionCfg:
def _cfg(self, tmp_path, current="1.0.19"):
p = tmp_path / ".bumpversion.cfg"
p.write_text(
f"[bumpversion]\n"
f"current_version = {current}\n"
f"commit = True\n\n"
f"[bumpversion:file:etsy_python/_version.py]\n"
f"search = __version__ = \"{{current_version}}\"\n"
)
return p

def test_updates_current_version(self, tmp_path):
cfg = self._cfg(tmp_path, "1.0.19")
assert bump_version.sync_bumpversion_cfg(cfg, "1.1.10") is True
assert "current_version = 1.1.10" in cfg.read_text()
assert "1.0.19" not in cfg.read_text()

def test_only_touches_current_version_line(self, tmp_path):
# The bumpversion:file section also references the version via a
# template; that line must be left alone.
cfg = self._cfg(tmp_path, "1.0.19")
bump_version.sync_bumpversion_cfg(cfg, "1.1.10")
content = cfg.read_text()
assert 'search = __version__ = "{current_version}"' in content
assert content.count("1.1.10") == 1 # only the current_version line

def test_missing_file_returns_false(self, tmp_path):
assert bump_version.sync_bumpversion_cfg(tmp_path / "nope.cfg", "1.1.10") is False

def test_no_current_version_line_raises(self, tmp_path):
p = tmp_path / ".bumpversion.cfg"
p.write_text("[bumpversion]\ncommit = True\n")
with pytest.raises(ValueError):
bump_version.sync_bumpversion_cfg(p, "1.1.10")

def test_idempotent(self, tmp_path):
cfg = self._cfg(tmp_path, "1.1.10")
bump_version.sync_bumpversion_cfg(cfg, "1.1.10")
assert "current_version = 1.1.10" in cfg.read_text()


# --------------------------------------------------------------------------- #
# Integration: write_version + sync leave the repo consistent
# --------------------------------------------------------------------------- #
class TestWriteAndSyncConsistency:
def test_both_files_end_on_same_version(self, tmp_path):
vfile = tmp_path / "_version.py"
vfile.write_text('__version__ = "1.1.10"')
cfg = tmp_path / ".bumpversion.cfg"
cfg.write_text("[bumpversion]\ncurrent_version = 1.1.10\n")

new = bump_version.bump_version(bump_version.read_version(vfile), "patch")
bump_version.write_version(vfile, new)
bump_version.sync_bumpversion_cfg(cfg, new)

assert bump_version.read_version(vfile) == "1.1.11"
assert f"current_version = {new}" in cfg.read_text()
Loading