66import os
77import pathlib
88import re
9+ import subprocess
10+
11+ from packaging .version import parse as parse_version
12+
13+ _EXCLUDE_PATTERNS = [
14+ "./.git/*" ,
15+ "./.github/*" ,
16+ "./.bazelci/*" ,
17+ "./.bcr/*" ,
18+ "./bazel-*/*" ,
19+ "./CONTRIBUTING.md" ,
20+ "./RELEASING.md" ,
21+ "./tools/private/release/*" ,
22+ "./tests/tools/private/release/*" ,
23+ ]
24+
25+
26+ def _iter_version_placeholder_files ():
27+ for root , dirs , files in os .walk ("." , topdown = True ):
28+ # Filter directories
29+ dirs [:] = [
30+ d
31+ for d in dirs
32+ if not any (
33+ fnmatch .fnmatch (os .path .join (root , d ), pattern )
34+ for pattern in _EXCLUDE_PATTERNS
35+ )
36+ ]
37+
38+ for filename in files :
39+ filepath = os .path .join (root , filename )
40+ if any (fnmatch .fnmatch (filepath , pattern ) for pattern in _EXCLUDE_PATTERNS ):
41+ continue
42+
43+ yield filepath
44+
45+
46+ def _get_git_tags ():
47+ """Runs a git command and returns the output."""
48+ return subprocess .check_output (["git" , "tag" ]).decode ("utf-8" ).splitlines ()
49+
50+
51+ def get_latest_version ():
52+ """Gets the latest version from git tags."""
53+ tags = _get_git_tags ()
54+ # The packaging module can parse PEP440 versions, including RCs.
55+ # It has a good understanding of version precedence.
56+ versions = [
57+ (tag , parse_version (tag ))
58+ for tag in tags
59+ if re .match (r"^\d+\.\d+\.\d+(rc\d+)?$" , tag .strip ())
60+ ]
61+ if not versions :
62+ raise RuntimeError ("No git tags found matching X.Y.Z or X.Y.ZrcN format." )
63+
64+ versions .sort (key = lambda v : v [1 ])
65+ latest_tag , latest_version = versions [- 1 ]
66+
67+ if latest_version .is_prerelease :
68+ raise ValueError (f"The latest version is a pre-release version: { latest_tag } " )
69+
70+ # After all that, we only want to consider stable versions for the release.
71+ stable_versions = [tag for tag , version in versions if not version .is_prerelease ]
72+ if not stable_versions :
73+ raise ValueError ("No stable git tags found matching X.Y.Z format." )
74+
75+ # The versions are already sorted, so the last one is the latest.
76+ return stable_versions [- 1 ]
77+
78+
79+ def should_increment_minor ():
80+ """Checks if the minor version should be incremented."""
81+ for filepath in _iter_version_placeholder_files ():
82+ try :
83+ with open (filepath , "r" ) as f :
84+ content = f .read ()
85+ except (IOError , UnicodeDecodeError ):
86+ # Ignore binary files or files with read errors
87+ continue
88+
89+ if "VERSION_NEXT_FEATURE" in content :
90+ return True
91+ return False
92+
93+
94+ def determine_next_version ():
95+ """Determines the next version based on git tags and placeholders."""
96+ latest_version = get_latest_version ()
97+ major , minor , patch = [int (n ) for n in latest_version .split ("." )]
98+
99+ if should_increment_minor ():
100+ return f"{ major } .{ minor + 1 } .0"
101+ else :
102+ return f"{ major } .{ minor } .{ patch + 1 } "
9103
10104
11105def update_changelog (version , release_date , changelog_path = "CHANGELOG.md" ):
@@ -37,46 +131,19 @@ def update_changelog(version, release_date, changelog_path="CHANGELOG.md"):
37131
38132def replace_version_next (version ):
39133 """Replaces all VERSION_NEXT_* placeholders with the new version."""
40- exclude_patterns = [
41- "./.git/*" ,
42- "./.github/*" ,
43- "./.bazelci/*" ,
44- "./.bcr/*" ,
45- "./bazel-*/*" ,
46- "./CONTRIBUTING.md" ,
47- "./RELEASING.md" ,
48- "./tools/private/release/*" ,
49- "./tests/tools/private/release/*" ,
50- ]
134+ for filepath in _iter_version_placeholder_files ():
135+ try :
136+ with open (filepath , "r" ) as f :
137+ content = f .read ()
138+ except (IOError , UnicodeDecodeError ):
139+ # Ignore binary files or files with read errors
140+ continue
51141
52- for root , dirs , files in os .walk ("." , topdown = True ):
53- # Filter directories
54- dirs [:] = [
55- d
56- for d in dirs
57- if not any (
58- fnmatch .fnmatch (os .path .join (root , d ), pattern )
59- for pattern in exclude_patterns
60- )
61- ]
62-
63- for filename in files :
64- filepath = os .path .join (root , filename )
65- if any (fnmatch .fnmatch (filepath , pattern ) for pattern in exclude_patterns ):
66- continue
67-
68- try :
69- with open (filepath , "r" ) as f :
70- content = f .read ()
71- except (IOError , UnicodeDecodeError ):
72- # Ignore binary files or files with read errors
73- continue
74-
75- if "VERSION_NEXT_FEATURE" in content or "VERSION_NEXT_PATCH" in content :
76- new_content = content .replace ("VERSION_NEXT_FEATURE" , version )
77- new_content = new_content .replace ("VERSION_NEXT_PATCH" , version )
78- with open (filepath , "w" ) as f :
79- f .write (new_content )
142+ if "VERSION_NEXT_FEATURE" in content or "VERSION_NEXT_PATCH" in content :
143+ new_content = content .replace ("VERSION_NEXT_FEATURE" , version )
144+ new_content = new_content .replace ("VERSION_NEXT_PATCH" , version )
145+ with open (filepath , "w" ) as f :
146+ f .write (new_content )
80147
81148
82149def _semver_type (value ):
@@ -94,8 +161,10 @@ def create_parser():
94161 )
95162 parser .add_argument (
96163 "version" ,
97- help = "The new release version (e.g., 0.28.0). " ,
164+ nargs = "? " ,
98165 type = _semver_type ,
166+ help = "The new release version (e.g., 0.28.0). If not provided, "
167+ "it will be determined automatically." ,
99168 )
100169 return parser
101170
@@ -104,21 +173,22 @@ def main():
104173 parser = create_parser ()
105174 args = parser .parse_args ()
106175
107- if not re .match (r"^\d+\.\d+\.\d+(rc\d+)?$" , args .version ):
108- raise ValueError (
109- f"Version '{ args .version } ' is not a valid semantic version (X.Y.Z or X.Y.ZrcN)"
110- )
176+ version = args .version
177+ if version is None :
178+ print ("No version provided, determining next version automatically..." )
179+ version = determine_next_version ()
180+ print (f"Determined next version: { version } " )
111181
112- # Change to the workspace root so the script can be run from anywhere.
182+ # Change to the workspace root so the script can be run using `bazel run`
113183 if "BUILD_WORKSPACE_DIRECTORY" in os .environ :
114184 os .chdir (os .environ ["BUILD_WORKSPACE_DIRECTORY" ])
115185
116186 print ("Updating changelog ..." )
117187 release_date = datetime .date .today ().strftime ("%Y-%m-%d" )
118- update_changelog (args . version , release_date )
188+ update_changelog (version , release_date )
119189
120190 print ("Replacing VERSION_NEXT placeholders ..." )
121- replace_version_next (args . version )
191+ replace_version_next (version )
122192
123193 print ("Done" )
124194
0 commit comments