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,56 @@ def run_checks() -> None:
192
230
subprocess .run (["cargo" , "test" ], check = True )
193
231
print_success ("All checks passed" )
194
232
233
+
234
+ def generate_changelog (original_version : str , new_version : str ) -> None :
235
+ """Generate changelog using git-iris."""
236
+ print_step ("Generating changelog with git-iris" )
237
+ from_tag = f"v{ original_version } "
238
+
239
+ try :
240
+ print_step (
241
+ f"Updating changelog from { from_tag } to HEAD with version { new_version } "
242
+ )
243
+ subprocess .run (
244
+ [
245
+ "cargo" ,
246
+ "run" ,
247
+ "--" ,
248
+ "changelog" ,
249
+ "--from" ,
250
+ from_tag ,
251
+ "--to" ,
252
+ "HEAD" ,
253
+ "--update" ,
254
+ "--version-name" ,
255
+ new_version ,
256
+ ],
257
+ check = True ,
258
+ )
259
+ print_success ("Changelog updated successfully" )
260
+ except subprocess .CalledProcessError as e :
261
+ print_error (f"Failed to generate changelog: { str (e )} " )
262
+ sys .exit (1 )
263
+
264
+
195
265
def show_changes () -> bool :
196
266
"""Show changes and ask for confirmation."""
197
267
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 ()
268
+ subprocess .run (["git" , "status" , "--porcelain" ], check = False )
269
+ confirmation = input (
270
+ f"{ COLOR_VERSION_PROMPT } Do you want to proceed with these changes? (y/N): { COLOR_RESET } "
271
+ ).lower ()
200
272
return confirmation == "y"
201
273
274
+
202
275
def commit_and_push (version : str ) -> None :
203
276
"""Commit and push changes to the repository."""
204
277
print_step ("Committing and pushing changes" )
205
278
try :
206
- subprocess .run (["git" , "add" , "Cargo.*" ], check = True )
207
- subprocess .run (["git" , "commit" , "-m" , f":rocket: Release version { version } " ], check = True )
279
+ subprocess .run (["git" , "add" , "Cargo.*" , "CHANGELOG.md" ], check = True )
280
+ subprocess .run (
281
+ ["git" , "commit" , "-m" , f":rocket: Release version { version } " ], check = True
282
+ )
208
283
subprocess .run (["git" , "push" ], check = True )
209
284
subprocess .run (["git" , "tag" , f"v{ version } " ], check = True )
210
285
subprocess .run (["git" , "push" , "--tags" ], check = True )
@@ -213,10 +288,12 @@ def commit_and_push(version: str) -> None:
213
288
print_error (f"Git operations failed: { str (e )} " )
214
289
sys .exit (1 )
215
290
291
+
216
292
def is_valid_version (version : str ) -> bool :
217
293
"""Validate version format."""
218
294
return re .match (r"^\d+\.\d+\.\d+$" , version ) is not None
219
295
296
+
220
297
def main () -> None :
221
298
"""Main function to handle the release process."""
222
299
print_logo ()
@@ -229,22 +306,31 @@ def main() -> None:
229
306
check_uncommitted_changes ()
230
307
231
308
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 } " )
309
+ new_version = input (
310
+ f"{ COLOR_VERSION_PROMPT } Current version is { current_version } . What should the new version be? { COLOR_RESET } "
311
+ )
233
312
234
313
if not is_valid_version (new_version ):
235
- print_error ("Invalid version format. Please use semantic versioning (e.g., 1.2.3)." )
314
+ print_error (
315
+ "Invalid version format. Please use semantic versioning (e.g., 1.2.3)."
316
+ )
236
317
sys .exit (1 )
237
318
238
319
update_version (new_version )
239
320
run_checks ()
240
321
322
+ generate_changelog (current_version , new_version )
323
+
241
324
if not show_changes ():
242
325
print_error ("Release cancelled." )
243
326
sys .exit (1 )
244
327
245
328
commit_and_push (new_version )
246
329
247
- print_success (f"\n 🎉✨ { PROJECT_NAME } v{ new_version } has been successfully released! ✨🎉" )
330
+ print_success (
331
+ f"\n 🎉✨ { PROJECT_NAME } v{ new_version } has been successfully released! ✨🎉"
332
+ )
333
+
248
334
249
335
if __name__ == "__main__" :
250
- main ()
336
+ main ()
0 commit comments