Skip to content

Commit 7750470

Browse files
committed
chore: complete and clean up release script to facilitate new releases
1 parent b4ed589 commit 7750470

File tree

1 file changed

+136
-117
lines changed

1 file changed

+136
-117
lines changed

src/bin/make_release.rs

Lines changed: 136 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,195 @@
1-
/// Release automation script for GDScript Formatter
2-
///
3-
/// This script automates the release process by:
4-
/// 1. Prompting for version bump type (major, minor, or patch)
5-
/// 2. Verifying the git tag doesn't already exist
6-
/// 3. Updating the version in Cargo.toml
7-
/// 4. Running `cargo update` to update Cargo.lock
8-
/// 5. Running `cargo build --release` to verify the build works
9-
/// 6. Committing the changes with a standardized message
10-
/// 7. Creating a git tag with the new version
11-
/// 8. Generating a changelog and copying it to clipboard
12-
///
13-
/// Usage:
14-
/// cargo run --bin make_release
15-
///
16-
/// After the script completes, you'll need to manually push:
17-
/// git push origin main
18-
/// git push origin <version>
191
use std::fs;
202
use std::io::{self, Write};
213
use std::process::{Command, exit};
224

5+
fn print_error_and_exit(message: &str) -> ! {
6+
eprintln!("ERROR: {}", message);
7+
exit(1);
8+
}
9+
10+
/// Prompts the user for input and returns the trimmed string
11+
fn ask_user_input(prompt: &str) -> String {
12+
print!("{}", prompt);
13+
io::stdout().flush().expect("Failed to flush stdout");
14+
let mut input = String::new();
15+
io::stdin()
16+
.read_line(&mut input)
17+
.expect("Failed to read user input");
18+
return input.trim().to_owned();
19+
}
20+
21+
/// Runs a shell command with the given arguments.
22+
/// Exits the process with an error message if the command fails to start or returns a non-zero exit code.
23+
fn run(cmd: &str, args: &[&str]) {
24+
let status = Command::new(cmd)
25+
.args(args)
26+
.status()
27+
.unwrap_or_else(|_| print_error_and_exit(&format!("Failed to run {}", cmd)));
28+
29+
if !status.success() {
30+
print_error_and_exit(&format!("{} {} failed", cmd, args.join(" ")));
31+
}
32+
}
33+
34+
/// Parses a "semantic" version number from a string and returns the version above
35+
fn bump_tag_version(current: &str, bump_type: &str) -> Option<String> {
36+
let mut parts = current.split('.');
37+
let major = parts.next()?.parse::<u32>().ok()?;
38+
let minor = parts.next()?.parse::<u32>().ok()?;
39+
let patch = parts.next()?.parse::<u32>().ok()?;
40+
41+
match bump_type {
42+
"major" => Some(format!("{}.0.0", major + 1)),
43+
"minor" => Some(format!("{}.{}.0", major, minor + 1)),
44+
"patch" => Some(format!("{}.{}.{}", major, minor, patch + 1)),
45+
_ => None,
46+
}
47+
}
48+
49+
fn extract_changelog_section(changelog: &str, version: &str) -> Option<String> {
50+
let header = format!("## Release {}", version);
51+
let start = changelog.find(&header)?;
52+
let section = &changelog[start..];
53+
let end = section
54+
.match_indices("\n## Release ")
55+
.next()
56+
.map(|(index, _)| index)
57+
.unwrap_or(section.len());
58+
Some(section[..end].trim_end().to_owned())
59+
}
60+
2361
fn main() {
24-
println!("=== GDScript Formatter Release Script ===\n");
62+
println!("GDScript Formatter release script\n");
2563

2664
let cargo_toml = fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml");
2765

2866
let current_version = cargo_toml
2967
.lines()
3068
.find(|line| line.starts_with("version = "))
3169
.and_then(|line| line.split('"').nth(1))
32-
.expect("Failed to find version in Cargo.toml");
70+
.expect("Failed to find version in Cargo.toml")
71+
.to_owned();
3372

3473
println!("Current version: {}", current_version);
3574

36-
let parts: Vec<&str> = current_version.split('.').collect();
37-
if parts.len() != 3 {
38-
eprintln!("Invalid version format");
39-
exit(1);
40-
}
41-
let (major, minor, patch) = (
42-
parts[0].parse::<u32>().unwrap(),
43-
parts[1].parse::<u32>().unwrap(),
44-
parts[2].parse::<u32>().unwrap(),
45-
);
46-
47-
print!("\nVersion bump type? [major/minor/patch]: ");
48-
io::stdout().flush().unwrap();
49-
let mut bump_type = String::new();
50-
io::stdin().read_line(&mut bump_type).unwrap();
51-
let bump_type = bump_type.trim().to_lowercase();
52-
53-
let new_version = match bump_type.as_str() {
54-
"major" => format!("{}.0.0", major + 1),
55-
"minor" => format!("{}.{}.0", major, minor + 1),
56-
"patch" => format!("{}.{}.{}", major, minor, patch + 1),
57-
_ => {
58-
eprintln!("Invalid bump type. Use major, minor, or patch.");
59-
exit(1);
60-
}
61-
};
75+
let bump_type = ask_user_input("\nVersion bump type? [major/minor/patch]: ").to_lowercase();
76+
let new_version = bump_tag_version(&current_version, &bump_type).unwrap_or_else(|| {
77+
print_error_and_exit("Invalid version or bump type. Use major, minor, or patch.");
78+
});
6279

6380
println!("\nNew version will be: {}", new_version);
6481

6582
let tag_exists = Command::new("git")
66-
.args(["rev-parse", &new_version])
67-
.output()
68-
.map(|output| output.status.success())
83+
.args([
84+
"rev-parse",
85+
"--verify",
86+
"--quiet",
87+
&format!("refs/tags/{}", new_version),
88+
])
89+
.status()
90+
.map(|status| status.success())
6991
.unwrap_or(false);
7092

7193
if tag_exists {
72-
eprintln!("Error: Git tag '{}' already exists!", new_version);
73-
exit(1);
94+
print_error_and_exit(&format!("Git tag '{}' already exists", new_version));
7495
}
96+
println!("Git tag does not exist");
7597

76-
println!("✓ Git tag does not exist");
98+
println!("\nCommits since {}:", current_version);
99+
let commits_output = Command::new("git")
100+
.args(["log", "--oneline", &format!("{}..HEAD", current_version)])
101+
.output()
102+
.unwrap_or_else(|_| print_error_and_exit("Failed to run git log"));
103+
if !commits_output.status.success() {
104+
print_error_and_exit("git log failed");
105+
}
106+
let commits = String::from_utf8_lossy(&commits_output.stdout).into_owned();
107+
if commits.trim().is_empty() {
108+
println!("(no commits since last release)");
109+
} else {
110+
print!("{}", commits);
111+
}
112+
println!();
113+
114+
let changelog_path = "CHANGELOG.md";
115+
let expected_header = format!("## Release {}", new_version);
116+
let mut changelog = fs::read_to_string(changelog_path).expect("Failed to read CHANGELOG.md");
117+
118+
while !changelog.contains(&expected_header) {
119+
println!(
120+
"WARNING: CHANGELOG.md has no entry for version {}.",
121+
new_version
122+
);
123+
println!(
124+
"Please add a '{}' section to CHANGELOG.md before continuing.",
125+
expected_header
126+
);
127+
println!("Use the commits above as notes for the changelog.\n");
128+
129+
let user_confirmation_input =
130+
ask_user_input(&"Have you updated CHANGELOG.md and are ready to continue? [y/N]: ");
131+
if !(user_confirmation_input.eq_ignore_ascii_case("y")
132+
|| user_confirmation_input.eq_ignore_ascii_case("yes"))
133+
{
134+
print_error_and_exit("Aborting release. Update CHANGELOG.md and re-run the script.");
135+
}
136+
137+
changelog = fs::read_to_string(changelog_path).expect("Failed to re-read CHANGELOG.md");
138+
}
139+
println!("CHANGELOG.md entry verified");
77140

78141
let updated_cargo_toml = cargo_toml.replace(
79142
&format!("version = \"{}\"", current_version),
80143
&format!("version = \"{}\"", new_version),
81144
);
82145
fs::write("Cargo.toml", updated_cargo_toml).expect("Failed to write Cargo.toml");
83-
println!("Updated Cargo.toml");
146+
println!("Updated Cargo.toml");
84147

85148
println!("\nRunning cargo update...");
86-
let update_status = Command::new("cargo")
87-
.arg("update")
88-
.status()
89-
.expect("Failed to run cargo update");
90-
91-
if !update_status.success() {
92-
eprintln!("cargo update failed");
93-
exit(1);
94-
}
95-
println!("✓ cargo update successful");
96-
97-
println!("\nRunning cargo build...");
98-
let build_status = Command::new("cargo")
99-
.args(["build", "--release"])
100-
.status()
101-
.expect("Failed to run cargo build");
149+
run("cargo", &["update"]);
150+
println!("cargo update finished");
102151

103-
if !build_status.success() {
104-
eprintln!("cargo build failed");
105-
exit(1);
106-
}
107-
println!("✓ cargo build successful");
152+
println!("\nRunning cargo build --release...");
153+
run("cargo", &["build", "--release"]);
154+
println!("cargo build --release finished");
108155

109156
println!("\nAdding files to git...");
110-
let add_status = Command::new("git")
111-
.args(["add", "Cargo.toml", "Cargo.lock"])
112-
.status()
113-
.expect("Failed to run git add");
114-
115-
if !add_status.success() {
116-
eprintln!("git add failed");
117-
exit(1);
118-
}
157+
run("git", &["add", "Cargo.toml", "Cargo.lock", changelog_path]);
119158

120-
let commit_msg = format!("Update to version {}", new_version);
121-
let commit_status = Command::new("git")
122-
.args(["commit", "-m", &commit_msg])
123-
.status()
124-
.expect("Failed to run git commit");
159+
let commit_msg = format!("Release {}", new_version);
160+
run("git", &["commit", "-m", &commit_msg]);
161+
println!("Committed changes");
125162

126-
if !commit_status.success() {
127-
eprintln!("git commit failed");
128-
exit(1);
129-
}
130-
println!("✓ Committed changes");
163+
run("git", &["tag", &new_version]);
164+
println!("Created tag '{}'", new_version);
131165

132-
let tag_status = Command::new("git")
133-
.args(["tag", &new_version])
134-
.status()
135-
.expect("Failed to run git tag");
136-
137-
if !tag_status.success() {
138-
eprintln!("git tag failed");
139-
exit(1);
140-
}
141-
println!("✓ Created tag '{}'", new_version);
142-
143-
println!("\nGenerating changelog...");
144-
let shortlog_output = Command::new("git")
145-
.args(["shortlog", &format!("{}..HEAD", current_version)])
146-
.output()
147-
.expect("Failed to run git shortlog");
166+
let section = extract_changelog_section(&changelog, &new_version)
167+
.unwrap_or_else(|| format!("Release {}", new_version));
148168

149-
let shortlog = String::from_utf8_lossy(&shortlog_output.stdout);
150-
println!("\n--- Changelog ---");
151-
println!("{}", shortlog);
152-
println!("--- End Changelog ---\n");
169+
println!("\nChangelog section for {}:", new_version);
170+
println!("{}", section);
171+
println!();
153172

154173
let clipcopy_status = Command::new("clipcopy")
155174
.stdin(std::process::Stdio::piped())
156175
.spawn()
157176
.and_then(|mut child| {
158177
if let Some(mut stdin) = child.stdin.take() {
159-
stdin.write_all(shortlog.as_bytes())?;
178+
stdin.write_all(section.as_bytes())?;
160179
}
161180
child.wait()
162181
});
163182

164183
match clipcopy_status {
165184
Ok(status) if status.success() => {
166-
println!("Changelog copied to clipboard");
185+
println!("Changelog section copied to clipboard");
167186
}
168187
_ => {
169-
println!("⚠ Failed to copy to clipboard (clipcopy might not be available)");
188+
println!("WARNING: Could not copy to clipboard (clipcopy may be missing)");
170189
}
171190
}
172191

173-
println!("\n=== Release {} ready! ===", new_version);
192+
println!("\nRelease {} is ready", new_version);
174193
println!("\nNext steps:");
175194
println!(" git push origin main");
176195
println!(" git push origin {}", new_version);

0 commit comments

Comments
 (0)