Skip to content

Commit d0f8c01

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 d0f8c01

File tree

1 file changed

+144
-24
lines changed

1 file changed

+144
-24
lines changed

Diff for: scripts/release.py

+144-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,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+
195299
def 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+
202309
def 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+
216326
def is_valid_version(version: str) -> bool:
217327
"""Validate version format."""
218328
return re.match(r"^\d+\.\d+\.\d+$", version) is not None
219329

330+
220331
def 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

249369
if __name__ == "__main__":
250-
main()
370+
main()

0 commit comments

Comments
 (0)