11"""
22Description:
3- - Print the calculated new version.
3+ - Print the calculated new release tag.
4+ - Reads all existing git tags, picks the highest PEP440 version as the base,
5+ and computes the next version. Optionally removes invalid (b/rc) pre-release
6+ tags so they cannot poison future runs.
47
58Usage:
6- - python3 update_version .py --current-version <version> (--stage)
9+ - python3 get_new_version .py [--stage] [--cleanup-invalid]
710
811Version Scheme:
912- <year>.<month>.<release count>(a<prerelease count>)
1013
1114example:
1215- case 1:
1316 - given:
14- - current version: 2025.1.1
17+ - latest version: 2025.1.1
1518 - today : YEAR = 2025, MONTH = 1
1619 - then:
1720 - if stage is false:
2023 - new version: 2025.1.2a0
2124- case 2:
2225 - given:
23- - current version: 2025.1.2a0
26+ - latest version: 2025.1.2a0
2427 - today : YEAR = 2025, MONTH = 1
2528 - then:
2629 - if stage is false:
2932 - new version: 2025.1.2a1
3033- case 3:
3134 - given:
32- - current version: 2025.1.1
35+ - latest version: 2025.1.1
3336 - today : YEAR = 2025, MONTH = 2
3437 - then:
3538 - if stage is false:
36- - new version: 2025.2.0
39+ - new version: 2025.2.1
3740 - if stage is true:
38- - new version: 2025.2.0a0
41+ - new version: 2025.2.1a0
42+ - case 4:
43+ - given:
44+ - no tags exist
45+ - today : YEAR = 2025, MONTH = 2
46+ - then:
47+ - if stage is false:
48+ - new version: 2025.2.1
49+ - if stage is true:
50+ - new version: 2025.2.1a0
3951"""
4052
4153import argparse
4254import datetime
55+ import subprocess # nosec: B404 — git CLI 호출 전용, 입력은 PEP440 검증을 통과한 태그 이름뿐
56+ import sys
4357import typing
4458
4559import packaging .version
4862
4963
5064class ArgumentNamespace (argparse .Namespace ):
51- current : str
5265 stage : bool = False
53-
54-
55- def increment_version_count (version : packaging .version .Version , is_stage : bool ) -> str :
56- if (current_pre := version .pre ) and current_pre [0 ] != "a" :
57- raise ValueError (f"Unsupported pre-release version: { current_pre [0 ]} " )
58-
59- # Get the current date
66+ cleanup_invalid : bool = False
67+
68+
69+ def list_tags () -> list [str ]:
70+ out = subprocess .run ( # nosec: B603 B607 — 고정 인자, CI 신뢰 환경
71+ ["git" , "tag" , "-l" ],
72+ check = True ,
73+ capture_output = True ,
74+ text = True ,
75+ ).stdout
76+ return [line .strip () for line in out .splitlines () if line .strip ()]
77+
78+
79+ def collect_versions (tags : list [str ]) -> tuple [list [packaging .version .Version ], list [str ]]:
80+ """Split tags into (a-only PEP440 versions, invalid b/rc tags). Non-PEP440 tags are ignored."""
81+ valid : list [packaging .version .Version ] = []
82+ invalid : list [str ] = []
83+ for tag in tags :
84+ try :
85+ version = packaging .version .parse (tag )
86+ except packaging .version .InvalidVersion :
87+ continue
88+ if version .pre and version .pre [0 ] != "a" :
89+ invalid .append (tag )
90+ continue
91+ valid .append (version )
92+ return valid , invalid
93+
94+
95+ def tag_exists (tag : str ) -> bool :
96+ return (
97+ subprocess .run ( # nosec: B603 B607 — 고정 인자, CI 신뢰 환경
98+ ["git" , "rev-parse" , "--verify" , "--quiet" , f"refs/tags/{ tag } " ],
99+ capture_output = True ,
100+ ).returncode
101+ == 0
102+ )
103+
104+
105+ def delete_tag (tag : str ) -> None :
106+ subprocess .run ( # nosec: B603 B607 — 고정 인자, CI 신뢰 환경
107+ ["git" , "tag" , "-d" , tag ],
108+ check = False ,
109+ capture_output = True ,
110+ )
111+ result = subprocess .run ( # nosec: B603 B607 — 고정 인자, CI 신뢰 환경
112+ ["git" , "push" , "origin" , f":refs/tags/{ tag } " ],
113+ capture_output = True ,
114+ text = True ,
115+ )
116+ if result .returncode != 0 :
117+ print (f"Warning: failed to delete remote tag { tag } : { result .stderr .strip ()} " , file = sys .stderr )
118+
119+
120+ def increment_version_count (version : packaging .version .Version | None , is_stage : bool ) -> str :
60121 today : datetime .date = datetime .date .today ()
122+ current_pre : PreType = typing .cast (PreType , version .pre ) if version else None
61123
62- # Calculate the new version
63- new_count : int = 0
64- if version .major == today .year and version .minor == today .month :
124+ new_count : int
125+ if version and version .major == today .year and version .minor == today .month :
65126 if current_pre :
66- # If the current version is a pre-release, do not increment the count
127+ # Same month with a pre-release: keep the same micro
67128 new_count = version .micro
68129 else :
69130 # Same month, increment the count
70131 new_count = version .micro + 1
71132 else :
72- # Different month, reset the count
133+ # Different month or no prior version: start fresh
73134 new_count = 1
74135 current_pre = None
75136
@@ -78,10 +139,32 @@ def increment_version_count(version: packaging.version.Version, is_stage: bool)
78139 return f"{ today .year } .{ today .month } .{ new_count } { new_pre_str } "
79140
80141
142+ def compute_next_tag (latest : packaging .version .Version | None , is_stage : bool ) -> str :
143+ candidate = increment_version_count (latest , is_stage )
144+ while tag_exists (candidate ):
145+ print (f"Warning: tag { candidate } already exists, retrying" , file = sys .stderr )
146+ candidate = increment_version_count (packaging .version .parse (candidate ), is_stage )
147+ return candidate
148+
149+
81150if __name__ == "__main__" :
82- parser = argparse .ArgumentParser (description = "Update version in files." )
83- parser .add_argument ("--current" , type = str , required = True )
151+ parser = argparse .ArgumentParser (description = "Compute next release tag." )
84152 parser .add_argument ("--stage" , default = False , action = "store_true" )
153+ parser .add_argument (
154+ "--cleanup-invalid" ,
155+ default = False ,
156+ action = "store_true" ,
157+ help = "Delete any b/rc pre-release tags (locally and on origin) before computing the next tag." ,
158+ )
85159
86160 args = parser .parse_args (namespace = ArgumentNamespace ())
87- print (increment_version_count (packaging .version .parse (args .current ), args .stage ))
161+
162+ valid , invalid = collect_versions (list_tags ())
163+
164+ if args .cleanup_invalid :
165+ for tag in invalid :
166+ print (f"Removing invalid pre-release tag: { tag } " , file = sys .stderr )
167+ delete_tag (tag )
168+
169+ latest = max (valid ) if valid else None
170+ print (compute_next_tag (latest , args .stage ))
0 commit comments