6
6
import os
7
7
import pathlib
8
8
import 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 } "
9
103
10
104
11
105
def update_changelog (version , release_date , changelog_path = "CHANGELOG.md" ):
@@ -37,46 +131,19 @@ def update_changelog(version, release_date, changelog_path="CHANGELOG.md"):
37
131
38
132
def replace_version_next (version ):
39
133
"""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
51
141
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 )
80
147
81
148
82
149
def _semver_type (value ):
@@ -94,8 +161,10 @@ def create_parser():
94
161
)
95
162
parser .add_argument (
96
163
"version" ,
97
- help = "The new release version (e.g., 0.28.0). " ,
164
+ nargs = "? " ,
98
165
type = _semver_type ,
166
+ help = "The new release version (e.g., 0.28.0). If not provided, "
167
+ "it will be determined automatically." ,
99
168
)
100
169
return parser
101
170
@@ -104,21 +173,22 @@ def main():
104
173
parser = create_parser ()
105
174
args = parser .parse_args ()
106
175
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 } " )
111
181
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`
113
183
if "BUILD_WORKSPACE_DIRECTORY" in os .environ :
114
184
os .chdir (os .environ ["BUILD_WORKSPACE_DIRECTORY" ])
115
185
116
186
print ("Updating changelog ..." )
117
187
release_date = datetime .date .today ().strftime ("%Y-%m-%d" )
118
- update_changelog (args . version , release_date )
188
+ update_changelog (version , release_date )
119
189
120
190
print ("Replacing VERSION_NEXT placeholders ..." )
121
- replace_version_next (args . version )
191
+ replace_version_next (version )
122
192
123
193
print ("Done" )
124
194
0 commit comments