3636# Gradient colors for the banner
3737GRADIENT_COLORS = [
3838 (138 , 43 , 226 ), # BlueViolet
39- (75 , 0 , 130 ), # Indigo
40- (0 , 191 , 255 ), # DeepSkyBlue
39+ (75 , 0 , 130 ), # Indigo
40+ (0 , 191 , 255 ), # DeepSkyBlue
4141 (30 , 144 , 255 ), # DodgerBlue
4242 (138 , 43 , 226 ), # BlueViolet
43- (75 , 0 , 130 ), # Indigo
44- (0 , 191 , 255 ), # DeepSkyBlue
43+ (75 , 0 , 130 ), # Indigo
44+ (0 , 191 , 255 ), # DeepSkyBlue
4545]
4646
47+
4748def print_colored (message : str , color : str ) -> None :
4849 """Print a message with a specific color."""
4950 print (f"{ color } { message } { COLOR_RESET } " )
5051
52+
5153def print_step (step : str ) -> None :
5254 """Print a step in the process with a specific color."""
5355 print_colored (f"\n ✨ { step } " , COLOR_STEP )
5456
57+
5558def print_error (message : str ) -> None :
5659 """Print an error message with a specific color."""
5760 print_colored (f"❌ Error: { message } " , COLOR_ERROR )
5861
62+
5963def print_success (message : str ) -> None :
6064 """Print a success message with a specific color."""
6165 print_colored (f"✅ { message } " , COLOR_SUCCESS )
6266
67+
6368def print_warning (message : str ) -> None :
6469 """Print a warning message with a specific color."""
6570 print_colored (f"⚠️ { message } " , COLOR_WARNING )
6671
72+
6773def generate_gradient (colors : List [Tuple [int , int , int ]], steps : int ) -> List [str ]:
6874 """Generate a list of color codes for a smooth multi-color gradient."""
6975 gradient = []
@@ -82,28 +88,33 @@ def generate_gradient(colors: List[Tuple[int, int, int]], steps: int) -> List[st
8288
8389 return gradient
8490
91+
8592def strip_ansi (text : str ) -> str :
8693 """Remove ANSI color codes from a string."""
8794 ansi_escape = re .compile (r"\x1B[@-_][0-?]*[ -/]*[@-~]" )
8895 return ansi_escape .sub ("" , text )
8996
97+
9098def apply_gradient (text : str , gradient : List [str ], line_number : int ) -> str :
9199 """Apply gradient colors diagonally to text."""
92100 return "" .join (
93101 f"{ gradient [(i + line_number ) % len (gradient )]} { char } "
94102 for i , char in enumerate (text )
95103 )
96104
105+
97106def center_text (text : str , width : int ) -> str :
98107 """Center text, accounting for ANSI color codes and Unicode widths."""
99108 visible_length = wcswidth (strip_ansi (text ))
100109 padding = (width - visible_length ) // 2
101110 return f"{ ' ' * padding } { text } { ' ' * (width - padding - visible_length )} "
102111
112+
103113def center_block (block : List [str ], width : int ) -> List [str ]:
104114 """Center a block of text within a given width."""
105115 return [center_text (line , width ) for line in block ]
106116
117+
107118def create_banner () -> str :
108119 """Create a beautiful cosmic-themed banner with diagonal gradient."""
109120 banner_width = 80
@@ -133,57 +144,84 @@ def create_banner() -> str:
133144
134145 release_manager_text = COLOR_STEP + "Release Manager"
135146
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+ )
141159
142160 return "\n " .join (banner )
143161
162+
144163def print_logo () -> None :
145164 """Print the banner/logo for the release manager."""
146165 print (create_banner ())
147166
167+
148168def check_tool_installed (tool_name : str ) -> None :
149169 """Check if a tool is installed."""
150170 if shutil .which (tool_name ) is None :
151171 print_error (f"{ tool_name } is not installed. Please install it and try again." )
152172 sys .exit (1 )
153173
174+
154175def check_branch () -> None :
155176 """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+ )
157182 if current_branch != "main" :
158183 print_error ("You must be on the main branch to release." )
159184 sys .exit (1 )
160185
186+
161187def check_uncommitted_changes () -> None :
162188 """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+ )
164192 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+ )
166196 sys .exit (1 )
167197
198+
168199def get_current_version () -> str :
169200 """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 :
171202 content = f .read ()
172203 match = re .search (r'version\s*=\s*"(\d+\.\d+\.\d+)"' , content )
173204 if match :
174205 return match .group (1 )
175206 print_error ("Could not find version in Cargo.toml" )
176207 sys .exit (1 )
177208
209+
178210def update_version (new_version : str ) -> None :
179211 """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 :
181213 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 :
184221 f .write (updated_content )
185222 print_success (f"Updated version in Cargo.toml to { new_version } " )
186223
224+
187225def run_checks () -> None :
188226 """Run cargo check and cargo test."""
189227 print_step ("Running cargo check" )
@@ -192,19 +230,90 @@ def run_checks() -> None:
192230 subprocess .run (["cargo" , "test" ], check = True )
193231 print_success ("All checks passed" )
194232
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+
195299def show_changes () -> bool :
196300 """Show changes and ask for confirmation."""
197301 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 ()
200306 return confirmation == "y"
201307
308+
202309def commit_and_push (version : str ) -> None :
203310 """Commit and push changes to the repository."""
204311 print_step ("Committing and pushing changes" )
205312 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+ )
208317 subprocess .run (["git" , "push" ], check = True )
209318 subprocess .run (["git" , "tag" , f"v{ version } " ], check = True )
210319 subprocess .run (["git" , "push" , "--tags" ], check = True )
@@ -213,10 +322,12 @@ def commit_and_push(version: str) -> None:
213322 print_error (f"Git operations failed: { str (e )} " )
214323 sys .exit (1 )
215324
325+
216326def is_valid_version (version : str ) -> bool :
217327 """Validate version format."""
218328 return re .match (r"^\d+\.\d+\.\d+$" , version ) is not None
219329
330+
220331def main () -> None :
221332 """Main function to handle the release process."""
222333 print_logo ()
@@ -229,22 +340,31 @@ def main() -> None:
229340 check_uncommitted_changes ()
230341
231342 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+ )
233346
234347 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+ )
236351 sys .exit (1 )
237352
238353 update_version (new_version )
239354 run_checks ()
240355
356+ generate_changelog (new_version )
357+
241358 if not show_changes ():
242359 print_error ("Release cancelled." )
243360 sys .exit (1 )
244361
245362 commit_and_push (new_version )
246363
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+
248368
249369if __name__ == "__main__" :
250- main ()
370+ main ()
0 commit comments