Commit c6d01f6
Changed files (6)
tools
tools/release_tool/src/main.rs
@@ -0,0 +1,112 @@
+mod summary;
+
+use crate::summary::Summary;
+use anyhow::{bail, Context, Result};
+use regex::Regex;
+use std::io::Write;
+use std::{env, fs, io, path::PathBuf};
+
+// TODO:
+// - changelog management
+// - weblate integration
+// - git commits / PR-ing
+// - release tagging / github publishing
+
+fn main() -> Result<()>{
+ let mut summary = Summary::default();
+
+ summary.root = git_dir()
+ .context("Couldn't find repository root")?;
+ println!("Using repository at {:?}", summary.root);
+
+ let version = get_version(&summary.root)
+ .context("Couldn't read current app version")?;
+ println!("Current app version is {} ({})", &version.0, &version.1);
+
+ let bump_version = prompt_bool(format!("Bump app version ({}->{})?", version.1, version.1 + 1).as_str(), Some(true))?;
+ if bump_version {
+ print!("Version name (e.g. {}): ", version.0);
+ io::stdout().flush()?;
+ let mut buffer = String::new();
+ io::stdin().read_line(&mut buffer)?;
+ buffer = buffer.trim().to_string();
+ let regex = Regex::new(r"(\d+).(\d+).(\d+)")?;
+ if regex.is_match(&buffer) {
+ summary.new_version_line = Some(format!("version: {buffer}+{}", version.1 + 1));
+ } else {
+ bail!("New version is malformed");
+ }
+ }
+
+ summary.update_flutter = prompt_bool("Update flutter?", Some(true))?;
+ summary.update_dependencies = prompt_bool("Update dependencies?", Some(true))?;
+ summary.run_tests = prompt_bool("Run tests?", Some(false))?;
+ summary.build = prompt_bool("Build app?", Some(true))?;
+
+ summary.print();
+ summary.apply();
+
+ Ok(())
+}
+
+pub fn prompt_bool(prompt: &str, default: Option<bool>) -> Result<bool> {
+ let y = if default.is_some_and(|d| d) { "Y" } else { "y" };
+ let n = if default.is_some_and(|d| !d) { "N" } else { "n" };
+ print!("{} [{}/{}] ", prompt, y, n);
+ io::stdout().flush()?;
+
+ let mut buffer = String::new();
+ io::stdin().read_line(&mut buffer)?;
+ buffer = buffer.trim().to_string();
+
+ if buffer.eq_ignore_ascii_case("y") {
+ Ok(true)
+ } else if buffer.eq_ignore_ascii_case("n") {
+ Ok(false)
+ } else if let Some(default) = default {
+ Ok(default)
+ } else {
+ bail!("Invalid input '{buffer}', please provide either 'y' or 'n'");
+ }
+}
+
+
+/// Get the closest ancestor dir that contains a .git folder in order to find the repository root.
+pub fn git_dir() -> Result<PathBuf> {
+ let mut dir = env::current_dir()
+ .context("no CWD")?;
+
+ loop {
+ // find a child dir with matching name
+ let child = dir.read_dir()?
+ .find(|e| e.is_ok() && e.as_ref().unwrap().file_name()
+ .eq_ignore_ascii_case(".git"));
+ if let Some(Ok(_)) = child {
+ return Ok(dir);
+ }
+ if let Some(parent) = dir.parent() {
+ dir = parent.to_path_buf();
+ } else {
+ bail!("Reached fs root")
+ }
+ }
+}
+
+/// Read current app verison name and number from pubspec in `$root/app/pubspec.yaml`.
+///
+/// Example: ("1.8.4", 49)
+pub fn get_version(root: &PathBuf) -> Result<(String, usize)> {
+ let pubspec = root.join("app").join("pubspec.yaml");
+ let pubspec = fs::read_to_string(pubspec).context("Couldn't find pubspec.yaml")?;
+
+ // Matches the `version: ...+..` line of the file, capturing the name in 1 and the number in 2.
+ let regex = Regex::new(r"version:\s*([0-9.]*)\+([0-9]*)")?;
+ let pubspec = regex.captures(&pubspec)
+ .context("Can't find app version declaration in pubspec.yaml")?;
+
+ let version_name = pubspec.get(1).expect("implied by regex");
+ let version_num = pubspec.get(2).expect("implied by regex");
+ let parsed_version_num = version_num.as_str().parse::<usize>()
+ .context(format!("Extracted version string is: '{}'", version_num.as_str()))?;
+ Ok((version_name.as_str().to_string(), parsed_version_num))
+}
\ No newline at end of file
tools/release_tool/src/summary.rs
@@ -0,0 +1,290 @@
+use crate::prompt_bool;
+use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
+use regex::Regex;
+use std::ffi::OsStr;
+use std::fs;
+use std::io::{BufReader, Read};
+use std::os::unix::prelude::OsStrExt;
+use std::path::PathBuf;
+use std::process::{exit, Command, Stdio};
+
+/// Summary of actions that will be taken
+#[derive(Debug, Default)]
+pub struct Summary {
+ pub root: PathBuf,
+ /// E.g. `version: 1.8.8+50`
+ pub new_version_line: Option<String>,
+ pub update_flutter: bool,
+ pub update_dependencies: bool,
+ pub run_tests: bool,
+ pub build: bool,
+}
+
+impl Summary {
+ pub fn print(&self) {
+ println!("Summary:\n\
+ > root = {}\n\
+ > new_version_line = {:?}\n\
+ > update_flutter = {}\n\
+ > update_dependencies = {}\n\
+ > run_tests = {}\n\
+ > build = {}",
+ &self.root.to_string_lossy(),
+ &self.new_version_line,
+ &self.update_flutter,
+ &self.update_dependencies,
+ &self.run_tests,
+ &self.build);
+ }
+
+ /// Apply this config to the file system.
+ pub fn apply(&self) {
+ // Order:
+ // 1. update flutter
+ // 2. Write new app and flutter verrsion to pubspec
+ // 3. update dependencies
+ // 4. run tests and build
+ let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
+ .unwrap()
+ .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
+ let m = MultiProgress::new();
+
+ if self.update_flutter {
+ let pb = m.add(ProgressBar::new_spinner());
+ pb.set_style(spinner_style.clone());
+ pb.set_prefix("$ flutter upgrade");
+ pb.set_message("Starting...");
+
+ let child = Command::new("flutter")
+ .arg("upgrade")
+ .stdout(Stdio::piped())
+ .spawn();
+ if let Ok(mut child) = child {
+ let mut stdout = child.stdout.take().unwrap();
+ pb.set_message("Started");
+ loop {
+ let mut buf = [0;255];
+ match stdout.read(&mut buf) {
+ Err(err) => {
+ println!("{}] Error reading from stream: {}", line!(), err);
+ break;
+ }
+ Ok(got) => {
+ if got == 0 {
+ break;
+ } else {
+ let msg = OsStr::from_bytes(&buf)
+ .to_string_lossy();
+ pb.println(msg);
+ }
+ }
+ }
+ }
+ pb.set_message("Stopped reading from stdout");
+ child.wait().unwrap();
+ pb.finish_with_message("Completed");
+ } else if let Err(err) = child {
+ pb.println(format!("{}", err));
+ pb.finish_with_message("flutter upgrade failed");
+ }
+ }
+
+ let current_flutter_version = {
+ let pb = m.add(ProgressBar::new_spinner());
+ pb.set_style(spinner_style.clone());
+ pb.set_prefix("$ flutter --version");
+ pb.set_message("Starting...");
+ let ouput = Command::new("flutter").arg("--version").stdout(Stdio::piped()).output().unwrap();
+ pb.set_message("Extracting version...");
+ if !ouput.status.success() {
+ pb.println(format!("{}", String::from_utf8_lossy(&ouput.stderr)));
+ exit(1);
+ }
+ pb.finish_with_message("Completed");
+ let output = String::from_utf8_lossy(&ouput.stdout);
+ let version = output.split(" ").nth(1).unwrap();
+ version.to_string()
+ };
+
+ if self.update_dependencies {
+ let pb = m.add(ProgressBar::new_spinner());
+ pb.set_style(spinner_style.clone());
+ pb.set_prefix("Updating dependencies");
+
+ pb.set_message("health_data_store pub deps");
+ Self::spawn_propagating_logs(
+ Command::new("dart")
+ .arg("pub").arg("upgrade")
+ .arg("--tighten").arg("--major-versions")
+ .current_dir(&self.root.join("health_data_store")),
+ &pb,
+ );
+
+ pb.set_message("health_data_store generate");
+ Self::spawn_propagating_logs(
+ Command::new("dart")
+ .arg("run").arg("build_runner").arg("build")
+ .arg("--delete-conflicting-outputs")
+ .current_dir(&self.root.join("health_data_store")),
+ &pb,
+ );
+
+ pb.set_message("app pub deps");
+ Self::spawn_propagating_logs(
+ Command::new("flutter")
+ .arg("pub").arg("upgrade")
+ .arg("--tighten").arg("--major-versions")
+ .current_dir(&self.root.join("app")),
+ &pb,
+ );
+
+ pb.set_message("app generate");
+ Self::spawn_propagating_logs(
+ Command::new("flutter").arg("pub")
+ .arg("run").arg("build_runner").arg("build")
+ .arg("--delete-conflicting-outputs")
+ .current_dir(&self.root.join("app")),
+ &pb,
+ );
+
+ pb.finish_with_message("Completed");
+ }
+
+ if self.update_flutter || self.new_version_line.is_some() {
+ _ = m.println("Updating pubspec.yaml");
+ let pubspec = fs::read_to_string(self.root.join("app").join("pubspec.yaml")).unwrap();
+ let version_re = Regex::new(r"flutter: '\d*.\d*.\d*'").unwrap();
+ let pubspec = version_re.replace(pubspec.as_str(), format!("flutter: '{current_flutter_version}'"));
+
+ let pubspec = if let Some(new_version_line) = &self.new_version_line {
+ let version_re = Regex::new(r"version:\s*\d*.\d*.\d*\+\d*").unwrap();
+ version_re.replace(&pubspec, new_version_line)
+ } else { pubspec };
+
+ fs::write(self.root.join("app").join("pubspec.yaml"), pubspec.to_string()).unwrap();
+ }
+
+ if self.run_tests {
+ let pb = m.add(ProgressBar::new_spinner());
+ pb.set_style(spinner_style.clone());
+ pb.set_prefix("Running tests");
+
+ pb.set_message("Testing health_data_store");
+ let libs_ok = Self::spawn_propagating_logs(
+ Command::new("dart").arg("test")
+ .current_dir(&self.root.join("health_data_store")),
+ &pb,
+ );
+
+ pb.set_message("Testing app");
+ let app_ok = Self::spawn_propagating_logs(
+ Command::new("flutter").arg("test")
+ .current_dir(&self.root.join("app")),
+ &pb,
+ );
+
+ if !libs_ok || !app_ok {
+ if !prompt_bool("App or Library tests failed. Do you want to proceed?", None).unwrap_or(false) {
+ exit(0);
+ }
+ }
+
+ pb.finish();
+ }
+
+ if self.build {
+ let pb = m.add(ProgressBar::new_spinner());
+ pb.set_style(spinner_style.clone());
+ pb.set_prefix("Build App");
+
+ pb.set_message("Cleaning...");
+ Self::spawn_propagating_logs(
+ Command::new("flutter").arg("clean")
+ .current_dir(&self.root.join("app")),
+ &pb,
+ );
+
+ pb.set_message("Build APK...");
+ let debug_info_path = self.root
+ .join("app")
+ .join("build")
+ .join("debug_info");
+ Self::spawn_propagating_logs(
+ Command::new("flutter").arg("build").arg("apk")
+ .arg("--release").arg("--flavor").arg("github")
+ .arg("--obfuscate").arg(format!("--split-debug-info={}", debug_info_path.display()))
+ .current_dir(&self.root.join("app")),
+ &pb,
+ );
+
+ pb.set_message("Build bundle...");
+ Self::spawn_propagating_logs(
+ Command::new("flutter").arg("build").arg("appbundle")
+ .arg("--release").arg("--flavor").arg("github")
+ .arg("--obfuscate").arg(format!("--split-debug-info={}", debug_info_path.display()))
+ .current_dir(&self.root.join("app")),
+ &pb,
+ );
+
+ pb.set_message("Compressing debug symbols");
+ Command::new("zip")
+ .arg("-r").arg("debug-info.zip").arg(".") // zip everything in debug_info
+ .current_dir(&debug_info_path)
+ .status().unwrap();
+
+ // Clean target dir and copy files
+ pb.set_message("Copying outputs");
+ let target_dir = self.root.join("target");
+ if target_dir.exists() {
+ fs::remove_dir_all(&target_dir).unwrap();
+ }
+ fs::create_dir_all(&target_dir).unwrap();
+ let out_base = self.root.join("app").join("build").join("app").join("outputs");
+ let apk_path = out_base.join("flutter-apk").join("app-github-release.apk");
+ let aab_path = out_base.join("bundle").join("githubRelease").join("app-github-release.aab");
+ let zip_path = debug_info_path.join("debug-info.zip");
+
+ fs::copy(&apk_path, target_dir.join("app-github-release.apk")).unwrap();
+ fs::copy(&aab_path, target_dir.join("app-github-release.aab")).unwrap();
+ fs::copy(&zip_path, target_dir.join("debug-info.zip")).unwrap();
+
+ pb.finish();
+ }
+ }
+
+ fn spawn_propagating_logs(cmd: &mut Command, pb: &ProgressBar) -> bool {
+ let child = cmd.stdout(Stdio::piped()).spawn();
+ if let Ok(mut child) = child {
+ let stdout = child.stdout.take().unwrap();
+ let mut reader = BufReader::new(stdout);
+ let mut line_buf: Vec<u8> = Vec::new();
+ let mut read_buf = [0; 1024];
+
+ while let Ok(n) = reader.read(&mut read_buf) {
+ if n == 0 {
+ break;
+ }
+
+ for &byte in &read_buf[..n] {
+ match byte {
+ b'\n' | b'\r' => {
+ pb.println(&String::from_utf8_lossy(&line_buf));
+ pb.force_draw();
+ line_buf.clear();
+ }
+ _ => {
+ line_buf.push(byte);
+ }
+ }
+ }
+ }
+
+ if !line_buf.is_empty() {
+ pb.println(&String::from_utf8_lossy(&line_buf));
+ }
+
+ return child.wait().is_ok_and(|e| e.success())
+ }
+ false
+ }
+}
\ No newline at end of file
tools/release_tool/.gitignore
@@ -0,0 +1,1 @@
+target
tools/release_tool/Cargo.lock
@@ -0,0 +1,327 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "console"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys",
+]
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "indicatif"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unicode-width",
+ "unit-prefix",
+ "web-time",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "release_tool"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "indicatif",
+ "regex",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
+
+[[package]]
+name = "unit-prefix"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
tools/release_tool/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "release_tool"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.98"
+indicatif = "0.18.0"
+regex = "1.11.1"
+
cleanup-changelogs.sh → tools/cleanup-changelogs.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Check if an argument is provided
if [ -z "$1" ]; then