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/socketsecurity/json"
15+ STABLE_VERSION_PATTERN = re .compile (r"^\d+\.\d+\.\d+$" )
16+ PYPI_PROD_API = "https://pypi.org/pypi/socketsecurity/json"
17+ PYPI_TEST_API = "https://test.pypi.org/pypi/socketsecurity/json"
1618
1719def read_version_from_init (path : pathlib .Path ) -> str :
1820 content = path .read_text ()
@@ -39,24 +41,59 @@ 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+
5175def find_next_available_dev_version (base_version : str ) -> str :
52- existing_versions = fetch_existing_versions ()
76+ existing_versions = fetch_existing_versions (PYPI_TEST_API )
5377 for i in range (1 , 100 ):
5478 candidate = f"{ base_version } .dev{ i } "
5579 if candidate not in existing_versions :
5680 return candidate
5781 print ("❌ Could not find available .devN slot after 100 attempts." )
5882 sys .exit (1 )
5983
84+
85+ def find_next_stable_patch_version (current_version : str ) -> str :
86+ current_stable = current_version .split (".dev" )[0 ] if ".dev" in current_version else current_version
87+ current_parts = parse_stable_version (current_stable )
88+ if current_parts is None :
89+ print (f"❌ Unsupported version format for stable bump: { current_version } " )
90+ sys .exit (1 )
91+
92+ latest_pypi_parts = fetch_latest_stable_pypi_version ()
93+ base_parts = max ([current_parts , latest_pypi_parts ] if latest_pypi_parts else [current_parts ])
94+ next_parts = (base_parts [0 ], base_parts [1 ], base_parts [2 ] + 1 )
95+ return format_stable_version (next_parts )
96+
6097def inject_version (version : str ):
6198 print (f"🔁 Updating version to: { version } " )
6299
@@ -105,13 +142,25 @@ def main():
105142 print (f"⚠️ Version was unchanged — auto-bumped. Please git add{ lock_hint } + commit again." )
106143 sys .exit (0 )
107144 else :
108- new_version = bump_patch_version (current_version )
145+ new_version = find_next_stable_patch_version (current_version )
109146 inject_version (new_version )
110147 uv_lock_changed = run_uv_lock ()
111148 lock_hint = " and uv.lock" if uv_lock_changed else ""
112- print (f"⚠️ Version was unchanged — auto-bumped. Please git add{ lock_hint } + commit again." )
149+ print (f"⚠️ Version was unchanged — auto-bumped to { new_version } . Please git add{ lock_hint } + commit again." )
113150 sys .exit (1 )
114151 else :
152+ if not dev_mode :
153+ current_parts = parse_stable_version (current_version )
154+ latest_pypi_parts = fetch_latest_stable_pypi_version ()
155+ if current_parts is not None and latest_pypi_parts is not None and current_parts <= latest_pypi_parts :
156+ next_parts = (latest_pypi_parts [0 ], latest_pypi_parts [1 ], latest_pypi_parts [2 ] + 1 )
157+ new_version = format_stable_version (next_parts )
158+ inject_version (new_version )
159+ uv_lock_changed = run_uv_lock ()
160+ lock_hint = " and uv.lock" if uv_lock_changed else ""
161+ print (f"⚠️ Version { current_version } is already published on PyPI — auto-bumped to { new_version } . Please git add{ lock_hint } + commit again." )
162+ sys .exit (1 )
163+
115164 uv_lock_changed = run_uv_lock ()
116165 if uv_lock_changed :
117166 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