36
36
# Gradient colors for the banner
37
37
GRADIENT_COLORS = [
38
38
(138 , 43 , 226 ), # BlueViolet
39
- (75 , 0 , 130 ), # Indigo
40
- (0 , 191 , 255 ), # DeepSkyBlue
39
+ (75 , 0 , 130 ), # Indigo
40
+ (0 , 191 , 255 ), # DeepSkyBlue
41
41
(30 , 144 , 255 ), # DodgerBlue
42
42
(138 , 43 , 226 ), # BlueViolet
43
- (75 , 0 , 130 ), # Indigo
44
- (0 , 191 , 255 ), # DeepSkyBlue
43
+ (75 , 0 , 130 ), # Indigo
44
+ (0 , 191 , 255 ), # DeepSkyBlue
45
45
]
46
46
47
+
47
48
def print_colored (message : str , color : str ) -> None :
48
49
"""Print a message with a specific color."""
49
50
print (f"{ color } { message } { COLOR_RESET } " )
50
51
52
+
51
53
def print_step (step : str ) -> None :
52
54
"""Print a step in the process with a specific color."""
53
55
print_colored (f"\n ✨ { step } " , COLOR_STEP )
54
56
57
+
55
58
def print_error (message : str ) -> None :
56
59
"""Print an error message with a specific color."""
57
60
print_colored (f"❌ Error: { message } " , COLOR_ERROR )
58
61
62
+
59
63
def print_success (message : str ) -> None :
60
64
"""Print a success message with a specific color."""
61
65
print_colored (f"✅ { message } " , COLOR_SUCCESS )
62
66
67
+
63
68
def print_warning (message : str ) -> None :
64
69
"""Print a warning message with a specific color."""
65
70
print_colored (f"⚠️ { message } " , COLOR_WARNING )
66
71
72
+
67
73
def generate_gradient (colors : List [Tuple [int , int , int ]], steps : int ) -> List [str ]:
68
74
"""Generate a list of color codes for a smooth multi-color gradient."""
69
75
gradient = []
@@ -82,28 +88,33 @@ def generate_gradient(colors: List[Tuple[int, int, int]], steps: int) -> List[st
82
88
83
89
return gradient
84
90
91
+
85
92
def strip_ansi (text : str ) -> str :
86
93
"""Remove ANSI color codes from a string."""
87
94
ansi_escape = re .compile (r"\x1B[@-_][0-?]*[ -/]*[@-~]" )
88
95
return ansi_escape .sub ("" , text )
89
96
97
+
90
98
def apply_gradient (text : str , gradient : List [str ], line_number : int ) -> str :
91
99
"""Apply gradient colors diagonally to text."""
92
100
return "" .join (
93
101
f"{ gradient [(i + line_number ) % len (gradient )]} { char } "
94
102
for i , char in enumerate (text )
95
103
)
96
104
105
+
97
106
def center_text (text : str , width : int ) -> str :
98
107
"""Center text, accounting for ANSI color codes and Unicode widths."""
99
108
visible_length = wcswidth (strip_ansi (text ))
100
109
padding = (width - visible_length ) // 2
101
110
return f"{ ' ' * padding } { text } { ' ' * (width - padding - visible_length )} "
102
111
112
+
103
113
def center_block (block : List [str ], width : int ) -> List [str ]:
104
114
"""Center a block of text within a given width."""
105
115
return [center_text (line , width ) for line in block ]
106
116
117
+
107
118
def create_banner () -> str :
108
119
"""Create a beautiful cosmic-themed banner with diagonal gradient."""
109
120
banner_width = 80
@@ -133,57 +144,84 @@ def create_banner() -> str:
133
144
134
145
release_manager_text = COLOR_STEP + "Release Manager"
135
146
136
- banner .extend ([
137
- f"{ COLOR_BORDER } ╰{ '─' * (banner_width - 2 )} ╯" ,
138
- center_text (f"{ COLOR_STAR } ∴。 ・゚*。☆ { release_manager_text } { COLOR_STAR } ☆。*゚・ 。∴" , banner_width ),
139
- center_text (f"{ COLOR_STAR } ・ 。 ☆ ∴。 ・゚*。★・ ∴。 ・゚*。☆ ・ 。 ☆ ∴。" , banner_width ),
140
- ])
147
+ banner .extend (
148
+ [
149
+ f"{ COLOR_BORDER } ╰{ '─' * (banner_width - 2 )} ╯" ,
150
+ center_text (
151
+ f"{ COLOR_STAR } ∴。 ・゚*。☆ { release_manager_text } { COLOR_STAR } ☆。*゚・ 。∴" ,
152
+ banner_width ,
153
+ ),
154
+ center_text (
155
+ f"{ COLOR_STAR } ・ 。 ☆ ∴。 ・゚*。★・ ∴。 ・゚*。☆ ・ 。 ☆ ∴。" , banner_width
156
+ ),
157
+ ]
158
+ )
141
159
142
160
return "\n " .join (banner )
143
161
162
+
144
163
def print_logo () -> None :
145
164
"""Print the banner/logo for the release manager."""
146
165
print (create_banner ())
147
166
167
+
148
168
def check_tool_installed (tool_name : str ) -> None :
149
169
"""Check if a tool is installed."""
150
170
if shutil .which (tool_name ) is None :
151
171
print_error (f"{ tool_name } is not installed. Please install it and try again." )
152
172
sys .exit (1 )
153
173
174
+
154
175
def check_branch () -> None :
155
176
"""Ensure we're on the main branch."""
156
- current_branch = subprocess .check_output (["git" , "rev-parse" , "--abbrev-ref" , "HEAD" ]).decode ().strip ()
177
+ current_branch = (
178
+ subprocess .check_output (["git" , "rev-parse" , "--abbrev-ref" , "HEAD" ])
179
+ .decode ()
180
+ .strip ()
181
+ )
157
182
if current_branch != "main" :
158
183
print_error ("You must be on the main branch to release." )
159
184
sys .exit (1 )
160
185
186
+
161
187
def check_uncommitted_changes () -> None :
162
188
"""Check for uncommitted changes."""
163
- result = subprocess .run (["git" , "diff-index" , "--quiet" , "HEAD" , "--" ], capture_output = True )
189
+ result = subprocess .run (
190
+ ["git" , "diff-index" , "--quiet" , "HEAD" , "--" ], capture_output = True , check = False
191
+ )
164
192
if result .returncode != 0 :
165
- print_error ("You have uncommitted changes. Please commit or stash them before releasing." )
193
+ print_error (
194
+ "You have uncommitted changes. Please commit or stash them before releasing."
195
+ )
166
196
sys .exit (1 )
167
197
198
+
168
199
def get_current_version () -> str :
169
200
"""Get the current version from Cargo.toml."""
170
- with open ("Cargo.toml" , "r" ) as f :
201
+ with open ("Cargo.toml" , "r" , encoding = "utf-8" ) as f :
171
202
content = f .read ()
172
203
match = re .search (r'version\s*=\s*"(\d+\.\d+\.\d+)"' , content )
173
204
if match :
174
205
return match .group (1 )
175
206
print_error ("Could not find version in Cargo.toml" )
176
207
sys .exit (1 )
177
208
209
+
178
210
def update_version (new_version : str ) -> None :
179
211
"""Update the version in Cargo.toml."""
180
- with open ("Cargo.toml" , "r" ) as f :
212
+ with open ("Cargo.toml" , "r" , encoding = "utf-8" ) as f :
181
213
content = f .read ()
182
- updated_content = re .sub (r'^(version\s*=\s*)"(\d+\.\d+\.\d+)"' , f'\\ 1"{ new_version } "' , content , flags = re .MULTILINE )
183
- with open ("Cargo.toml" , "w" ) as f :
214
+ updated_content = re .sub (
215
+ r'^(version\s*=\s*)"(\d+\.\d+\.\d+)"' ,
216
+ f'\\ 1"{ new_version } "' ,
217
+ content ,
218
+ flags = re .MULTILINE ,
219
+ )
220
+ with open ("Cargo.toml" , "w" , encoding = "utf-8" ) as f :
184
221
f .write (updated_content )
185
222
print_success (f"Updated version in Cargo.toml to { new_version } " )
186
223
224
+
187
225
def run_checks () -> None :
188
226
"""Run cargo check and cargo test."""
189
227
print_step ("Running cargo check" )
@@ -192,19 +230,90 @@ def run_checks() -> None:
192
230
subprocess .run (["cargo" , "test" ], check = True )
193
231
print_success ("All checks passed" )
194
232
233
+
234
+ def get_previous_tag () -> str :
235
+ """Get the most recent tag before the current HEAD."""
236
+ try :
237
+ # First, get all tags sorted by creation date (most recent first)
238
+ all_tags = (
239
+ subprocess .check_output (["git" , "tag" , "--sort=-creatordate" ], text = True )
240
+ .strip ()
241
+ .split ("\n " )
242
+ )
243
+
244
+ # Check if we have any tags at all
245
+ if not all_tags or all_tags [0 ] == "" :
246
+ print_warning ("No tags found. Using initial commit as reference." )
247
+ return subprocess .check_output (
248
+ ["git" , "rev-list" , "--max-parents=0" , "HEAD" ], text = True
249
+ ).strip ()
250
+
251
+ # If we have at least two tags, return the second one
252
+ if len (all_tags ) >= 2 :
253
+ return all_tags [1 ]
254
+
255
+ # If we only have one tag, use the initial commit
256
+ print_warning ("Could not find previous tag. Using initial commit as reference." )
257
+ return subprocess .check_output (
258
+ ["git" , "rev-list" , "--max-parents=0" , "HEAD" ], text = True
259
+ ).strip ()
260
+ except subprocess .CalledProcessError :
261
+ print_warning ("Could not find previous tag. Using initial commit as reference." )
262
+ # If no previous tag exists, return the initial commit hash
263
+ return subprocess .check_output (
264
+ ["git" , "rev-list" , "--max-parents=0" , "HEAD" ], text = True
265
+ ).strip ()
266
+
267
+
268
+ def generate_changelog (new_version : str ) -> None :
269
+ """Generate changelog using git-iris."""
270
+ print_step ("Generating changelog with git-iris" )
271
+ previous_tag = get_previous_tag ()
272
+
273
+ try :
274
+ print_step (
275
+ f"Updating changelog from { previous_tag } to HEAD with version { new_version } "
276
+ )
277
+ subprocess .run (
278
+ [
279
+ "cargo" ,
280
+ "run" ,
281
+ "--" ,
282
+ "changelog" ,
283
+ "--from" ,
284
+ previous_tag ,
285
+ "--to" ,
286
+ "HEAD" ,
287
+ "--update" ,
288
+ "--version-name" ,
289
+ new_version ,
290
+ ],
291
+ check = True ,
292
+ )
293
+ print_success ("Changelog updated successfully" )
294
+ except subprocess .CalledProcessError as e :
295
+ print_error (f"Failed to generate changelog: { str (e )} " )
296
+ sys .exit (1 )
297
+
298
+
195
299
def show_changes () -> bool :
196
300
"""Show changes and ask for confirmation."""
197
301
print_warning ("The following files will be modified:" )
198
- subprocess .run (["git" , "status" , "--porcelain" ])
199
- confirmation = input (f"{ COLOR_VERSION_PROMPT } Do you want to proceed with these changes? (y/N): { COLOR_RESET } " ).lower ()
302
+ subprocess .run (["git" , "status" , "--porcelain" ], check = False )
303
+ confirmation = input (
304
+ f"{ COLOR_VERSION_PROMPT } Do you want to proceed with these changes? (y/N): { COLOR_RESET } "
305
+ ).lower ()
200
306
return confirmation == "y"
201
307
308
+
202
309
def commit_and_push (version : str ) -> None :
203
310
"""Commit and push changes to the repository."""
204
311
print_step ("Committing and pushing changes" )
205
312
try :
206
- subprocess .run (["git" , "add" , "Cargo.*" ], check = True )
207
- subprocess .run (["git" , "commit" , "-m" , f":rocket: Release version { version } " ], check = True )
313
+ subprocess .run (["git" , "add" , "Cargo.*" , "CHANGELOG.md" ], check = True )
314
+ subprocess .run (
315
+ ["git" , "commit" , "-m" , f":rocket: Release version { version } " ], check = True
316
+ )
208
317
subprocess .run (["git" , "push" ], check = True )
209
318
subprocess .run (["git" , "tag" , f"v{ version } " ], check = True )
210
319
subprocess .run (["git" , "push" , "--tags" ], check = True )
@@ -213,10 +322,12 @@ def commit_and_push(version: str) -> None:
213
322
print_error (f"Git operations failed: { str (e )} " )
214
323
sys .exit (1 )
215
324
325
+
216
326
def is_valid_version (version : str ) -> bool :
217
327
"""Validate version format."""
218
328
return re .match (r"^\d+\.\d+\.\d+$" , version ) is not None
219
329
330
+
220
331
def main () -> None :
221
332
"""Main function to handle the release process."""
222
333
print_logo ()
@@ -229,22 +340,31 @@ def main() -> None:
229
340
check_uncommitted_changes ()
230
341
231
342
current_version = get_current_version ()
232
- new_version = input (f"{ COLOR_VERSION_PROMPT } Current version is { current_version } . What should the new version be? { COLOR_RESET } " )
343
+ new_version = input (
344
+ f"{ COLOR_VERSION_PROMPT } Current version is { current_version } . What should the new version be? { COLOR_RESET } "
345
+ )
233
346
234
347
if not is_valid_version (new_version ):
235
- print_error ("Invalid version format. Please use semantic versioning (e.g., 1.2.3)." )
348
+ print_error (
349
+ "Invalid version format. Please use semantic versioning (e.g., 1.2.3)."
350
+ )
236
351
sys .exit (1 )
237
352
238
353
update_version (new_version )
239
354
run_checks ()
240
355
356
+ generate_changelog (new_version )
357
+
241
358
if not show_changes ():
242
359
print_error ("Release cancelled." )
243
360
sys .exit (1 )
244
361
245
362
commit_and_push (new_version )
246
363
247
- print_success (f"\n 🎉✨ { PROJECT_NAME } v{ new_version } has been successfully released! ✨🎉" )
364
+ print_success (
365
+ f"\n 🎉✨ { PROJECT_NAME } v{ new_version } has been successfully released! ✨🎉"
366
+ )
367
+
248
368
249
369
if __name__ == "__main__" :
250
- main ()
370
+ main ()
0 commit comments