Skip to content

Commit ef9c092

Browse files
committed
✨ Integrate automatic changelog generation in release script
Implement automatic changelog generation during release process This change enhances the release workflow by leveraging the recently added version name feature: - Add get_previous_tag() to determine the previous release tag - Implement generate_changelog() to create changelog entries - Update git commit process to include CHANGELOG.md - Improve code formatting with consistent function spacing - Add error handling for subprocess calls with check=False - Fix file handling with proper encoding parameters This creates a seamless release experience where version bumping and changelog generation happen in a single coordinated workflow.
1 parent 67d9ead commit ef9c092

File tree

1 file changed

+111
-24
lines changed

1 file changed

+111
-24
lines changed

Diff for: scripts/release.py

+111-24
Original file line numberDiff line numberDiff line change
@@ -36,34 +36,40 @@
3636
# Gradient colors for the banner
3737
GRADIENT_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+
4748
def print_colored(message: str, color: str) -> None:
4849
"""Print a message with a specific color."""
4950
print(f"{color}{message}{COLOR_RESET}")
5051

52+
5153
def 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+
5558
def print_error(message: str) -> None:
5659
"""Print an error message with a specific color."""
5760
print_colored(f"❌ Error: {message}", COLOR_ERROR)
5861

62+
5963
def print_success(message: str) -> None:
6064
"""Print a success message with a specific color."""
6165
print_colored(f"✅ {message}", COLOR_SUCCESS)
6266

67+
6368
def print_warning(message: str) -> None:
6469
"""Print a warning message with a specific color."""
6570
print_colored(f"⚠️ {message}", COLOR_WARNING)
6671

72+
6773
def 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+
8592
def 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+
9098
def 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+
97106
def 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+
103113
def 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+
107118
def 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+
144163
def print_logo() -> None:
145164
"""Print the banner/logo for the release manager."""
146165
print(create_banner())
147166

167+
148168
def 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+
154175
def 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+
161187
def 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+
168199
def 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+
178210
def 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+
187225
def run_checks() -> None:
188226
"""Run cargo check and cargo test."""
189227
print_step("Running cargo check")
@@ -192,19 +230,57 @@ def run_checks() -> None:
192230
subprocess.run(["cargo", "test"], check=True)
193231
print_success("All checks passed")
194232

233+
234+
def generate_changelog(new_version: str) -> None:
235+
"""Generate changelog using git-iris."""
236+
print_step("Generating changelog with git-iris")
237+
current_version = get_current_version()
238+
from_tag = f"v{current_version}"
239+
240+
try:
241+
print_step(
242+
f"Updating changelog from {from_tag} to HEAD with version {new_version}"
243+
)
244+
subprocess.run(
245+
[
246+
"cargo",
247+
"run",
248+
"--",
249+
"changelog",
250+
"--from",
251+
from_tag,
252+
"--to",
253+
"HEAD",
254+
"--update",
255+
"--version-name",
256+
new_version,
257+
],
258+
check=True,
259+
)
260+
print_success("Changelog updated successfully")
261+
except subprocess.CalledProcessError as e:
262+
print_error(f"Failed to generate changelog: {str(e)}")
263+
sys.exit(1)
264+
265+
195266
def show_changes() -> bool:
196267
"""Show changes and ask for confirmation."""
197268
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()
269+
subprocess.run(["git", "status", "--porcelain"], check=False)
270+
confirmation = input(
271+
f"{COLOR_VERSION_PROMPT}Do you want to proceed with these changes? (y/N): {COLOR_RESET}"
272+
).lower()
200273
return confirmation == "y"
201274

275+
202276
def commit_and_push(version: str) -> None:
203277
"""Commit and push changes to the repository."""
204278
print_step("Committing and pushing changes")
205279
try:
206-
subprocess.run(["git", "add", "Cargo.*"], check=True)
207-
subprocess.run(["git", "commit", "-m", f":rocket: Release version {version}"], check=True)
280+
subprocess.run(["git", "add", "Cargo.*", "CHANGELOG.md"], check=True)
281+
subprocess.run(
282+
["git", "commit", "-m", f":rocket: Release version {version}"], check=True
283+
)
208284
subprocess.run(["git", "push"], check=True)
209285
subprocess.run(["git", "tag", f"v{version}"], check=True)
210286
subprocess.run(["git", "push", "--tags"], check=True)
@@ -213,10 +289,12 @@ def commit_and_push(version: str) -> None:
213289
print_error(f"Git operations failed: {str(e)}")
214290
sys.exit(1)
215291

292+
216293
def is_valid_version(version: str) -> bool:
217294
"""Validate version format."""
218295
return re.match(r"^\d+\.\d+\.\d+$", version) is not None
219296

297+
220298
def main() -> None:
221299
"""Main function to handle the release process."""
222300
print_logo()
@@ -229,22 +307,31 @@ def main() -> None:
229307
check_uncommitted_changes()
230308

231309
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}")
310+
new_version = input(
311+
f"{COLOR_VERSION_PROMPT}Current version is {current_version}. What should the new version be? {COLOR_RESET}"
312+
)
233313

234314
if not is_valid_version(new_version):
235-
print_error("Invalid version format. Please use semantic versioning (e.g., 1.2.3).")
315+
print_error(
316+
"Invalid version format. Please use semantic versioning (e.g., 1.2.3)."
317+
)
236318
sys.exit(1)
237319

238320
update_version(new_version)
239321
run_checks()
240322

323+
generate_changelog(new_version)
324+
241325
if not show_changes():
242326
print_error("Release cancelled.")
243327
sys.exit(1)
244328

245329
commit_and_push(new_version)
246330

247-
print_success(f"\n🎉✨ {PROJECT_NAME} v{new_version} has been successfully released! ✨🎉")
331+
print_success(
332+
f"\n🎉✨ {PROJECT_NAME} v{new_version} has been successfully released! ✨🎉"
333+
)
334+
248335

249336
if __name__ == "__main__":
250-
main()
337+
main()

0 commit comments

Comments
 (0)