1212
1313VERSION_PATTERN = re .compile (r"__version__\s*=\s*['\"]([^'\"]+)['\"]" )
1414PYPROJECT_PATTERN = re .compile (r'^version\s*=\s*".*"$' , re .MULTILINE )
15- PYPI_API = "https://test.pypi.org/pypi/socketdev/json"
15+ STABLE_VERSION_PATTERN = re .compile (r"^\d+\.\d+\.\d+$" )
16+ PYPI_PROD_API = "https://pypi.org/pypi/socketdev/json"
17+ PYPI_TEST_API = "https://test.pypi.org/pypi/socketdev/json"
1618
1719def read_version_from_version_file (path : pathlib .Path ) -> str :
1820 content = path .read_text ()
@@ -39,24 +41,61 @@ def bump_patch_version(version: str) -> str:
3941 parts [- 1 ] = str (int (parts [- 1 ]) + 1 )
4042 return "." .join (parts )
4143
42- def fetch_existing_versions () -> set :
44+ def parse_stable_version (version : str ):
45+ if not STABLE_VERSION_PATTERN .fullmatch (version ):
46+ return None
47+ return tuple (int (part ) for part in version .split ("." ))
48+
49+
50+ def format_stable_version (version_parts ) -> str :
51+ return "." .join (str (part ) for part in version_parts )
52+
53+
54+ def fetch_existing_versions (api_url : str ) -> set :
4355 try :
44- with urllib .request .urlopen (PYPI_API ) as response :
56+ with urllib .request .urlopen (api_url ) as response :
4557 data = json .load (response )
4658 return set (data .get ("releases" , {}).keys ())
4759 except Exception as e :
48- print (f"⚠️ Warning: Failed to fetch existing versions from Test PyPI : { e } " )
60+ print (f"⚠️ Warning: Failed to fetch versions from { api_url } : { e } " )
4961 return set ()
5062
63+
64+ def fetch_latest_stable_pypi_version ():
65+ versions = fetch_existing_versions (PYPI_PROD_API )
66+ stable_versions = []
67+ for ver in versions :
68+ parsed = parse_stable_version (ver )
69+ if parsed is not None :
70+ stable_versions .append (parsed )
71+ if not stable_versions :
72+ return None
73+ return max (stable_versions )
74+
75+
5176def find_next_available_dev_version (base_version : str ) -> str :
52- existing_versions = fetch_existing_versions ()
77+ existing_versions = fetch_existing_versions (PYPI_TEST_API )
5378 for i in range (1 , 100 ):
5479 candidate = f"{ base_version } .dev{ i } "
5580 if candidate not in existing_versions :
5681 return candidate
5782 print ("❌ Could not find available .devN slot after 100 attempts." )
5883 sys .exit (1 )
5984
85+
86+ def find_next_stable_patch_version (current_version : str ) -> str :
87+ current_stable = current_version .split (".dev" )[0 ] if ".dev" in current_version else current_version
88+ current_parts = parse_stable_version (current_stable )
89+ if current_parts is None :
90+ print (f"❌ Unsupported version format for stable bump: { current_version } " )
91+ sys .exit (1 )
92+
93+ latest_pypi_parts = fetch_latest_stable_pypi_version ()
94+ base_parts = max ([current_parts , latest_pypi_parts ] if latest_pypi_parts else [current_parts ])
95+ next_parts = (base_parts [0 ], base_parts [1 ], base_parts [2 ] + 1 )
96+ return format_stable_version (next_parts )
97+
98+
6099def inject_version (version : str ):
61100 print (f"🔁 Updating version to: { version } " )
62101
@@ -102,13 +141,25 @@ def main():
102141 print (f"⚠️ Version was unchanged — auto-bumped. Please git add{ lock_hint } + commit again." )
103142 sys .exit (0 )
104143 else :
105- new_version = bump_patch_version (current_version )
144+ new_version = find_next_stable_patch_version (current_version )
106145 inject_version (new_version )
107146 uv_lock_changed = run_uv_lock ()
108147 lock_hint = " and uv.lock" if uv_lock_changed else ""
109- print (f"⚠️ Version was unchanged — auto-bumped. Please git add{ lock_hint } + commit again." )
148+ print (f"⚠️ Version was unchanged — auto-bumped to { new_version } . Please git add{ lock_hint } + commit again." )
110149 sys .exit (1 )
111150 else :
151+ if not dev_mode :
152+ current_parts = parse_stable_version (current_version )
153+ latest_pypi_parts = fetch_latest_stable_pypi_version ()
154+ if current_parts is not None and latest_pypi_parts is not None and current_parts <= latest_pypi_parts :
155+ next_parts = (latest_pypi_parts [0 ], latest_pypi_parts [1 ], latest_pypi_parts [2 ] + 1 )
156+ new_version = format_stable_version (next_parts )
157+ inject_version (new_version )
158+ uv_lock_changed = run_uv_lock ()
159+ lock_hint = " and uv.lock" if uv_lock_changed else ""
160+ print (f"⚠️ Version { current_version } is already published on PyPI — auto-bumped to { new_version } . Please git add{ lock_hint } + commit again." )
161+ sys .exit (1 )
162+
112163 uv_lock_changed = run_uv_lock ()
113164 if uv_lock_changed :
114165 print ("⚠️ Version already bumped, but uv.lock was out of date and has been updated. Please git add uv.lock + commit again." )
0 commit comments