@@ -74,7 +74,7 @@ def git(*args: str) -> str:
7474 return result .stdout .strip ()
7575
7676
77- def tag_exists (tag : str ) -> bool :
77+ def local_tag_exists (tag : str ) -> bool :
7878 result = subprocess .run (
7979 ["git" , "tag" , "-l" , tag ],
8080 capture_output = True ,
@@ -84,6 +84,24 @@ def tag_exists(tag: str) -> bool:
8484 return bool (result .stdout .strip ())
8585
8686
87+ def remote_tag_exists (tag : str ) -> bool :
88+ result = subprocess .run (
89+ ["git" , "ls-remote" , "--tags" , "origin" , f"refs/tags/{ tag } " ],
90+ capture_output = True ,
91+ text = True ,
92+ cwd = CARGO_TOML .parent ,
93+ )
94+ return bool (result .stdout .strip ())
95+
96+
97+ def delete_local_tag (tag : str ) -> None :
98+ subprocess .run (
99+ ["git" , "tag" , "-d" , tag ],
100+ capture_output = True ,
101+ cwd = CARGO_TOML .parent ,
102+ )
103+
104+
87105def main () -> None :
88106 push = "--push" in sys .argv
89107
@@ -103,12 +121,18 @@ def main() -> None:
103121 else :
104122 print ("crates.io version: (not found or not published yet)" )
105123
106- # Check git tag doesn't already exist
107- if tag_exists (tag ):
108- print (f"\n ERROR: Git tag { tag } already exists." , file = sys .stderr )
109- print ("Bump the version in Cargo.toml or delete the existing tag." , file = sys .stderr )
124+ # GitHub is the source of truth for tags.
125+ # If the tag exists on the remote, abort — the release is already out.
126+ # If it only exists locally (stale), clean it up automatically.
127+ if remote_tag_exists (tag ):
128+ print (f"\n ERROR: Git tag { tag } already exists on origin." , file = sys .stderr )
129+ print ("Bump the version in Cargo.toml or delete the remote tag/release first." , file = sys .stderr )
110130 sys .exit (1 )
111131
132+ if local_tag_exists (tag ):
133+ print (f"Local tag { tag } exists but not on origin — deleting stale local tag." )
134+ delete_local_tag (tag )
135+
112136 # Check we're on a clean working tree
113137 status = git ("status" , "--porcelain" )
114138 if status :
0 commit comments