Add script to bump GPUI version (#39573)

This script successfully published the [0.2.0-test.4 GPUI
prerelease](https://crates.io/crates/gpui/0.2.0-test.4).

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki
2025-10-06 01:42:17 +00:00
committed by GitHub
parent e317d98915
commit 0f0974f105
6 changed files with 397 additions and 0 deletions
Generated
+3
View File
@@ -19803,6 +19803,9 @@ dependencies = [
"cargo_metadata",
"cargo_toml",
"clap",
"indoc",
"toml 0.8.20",
"toml_edit",
"workspace-hack",
]
+1
View File
@@ -669,6 +669,7 @@ tiny_http = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.25.10", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
+3
View File
@@ -13,4 +13,7 @@ anyhow.workspace = true
cargo_metadata.workspace = true
cargo_toml.workspace = true
clap = { workspace = true, features = ["derive"] }
toml.workspace = true
indoc.workspace = true
toml_edit.workspace = true
workspace-hack.workspace = true
+3
View File
@@ -18,6 +18,8 @@ enum CliCommand {
Licenses(tasks::licenses::LicensesArgs),
/// Checks that packages conform to a set of standards.
PackageConformity(tasks::package_conformity::PackageConformityArgs),
/// Publishes GPUI and its dependencies to crates.io.
PublishGpui(tasks::publish_gpui::PublishGpuiArgs),
}
fn main() -> Result<()> {
@@ -29,5 +31,6 @@ fn main() -> Result<()> {
CliCommand::PackageConformity(args) => {
tasks::package_conformity::run_package_conformity(args)
}
CliCommand::PublishGpui(args) => tasks::publish_gpui::run_publish_gpui(args),
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod clippy;
pub mod licenses;
pub mod package_conformity;
pub mod publish_gpui;
+386
View File
@@ -0,0 +1,386 @@
#![allow(clippy::disallowed_methods, reason = "tooling is exempt")]
use std::io::{self, Write};
use std::process::{Command, Output, Stdio};
use anyhow::{Context as _, Result, bail};
use clap::Parser;
#[derive(Parser)]
pub struct PublishGpuiArgs {
/// Optional pre-release identifier to append to the version (e.g., alpha, test.1). Always bumps the minor version.
#[arg(long)]
pre_release: Option<String>,
/// Perform a dry-run and wait for user confirmation before each publish
#[arg(long)]
dry_run: bool,
}
pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> {
println!(
"Starting GPUI publish process{}...",
if args.dry_run { " (with dry-run)" } else { "" }
);
let start_time = std::time::Instant::now();
check_workspace_root()?;
ensure_cargo_set_version()?;
check_git_clean()?;
let current_version = read_gpui_version()?;
let new_version = bump_version(&current_version, args.pre_release.as_deref())?;
println!(
"Updating GPUI version: {} -> {}",
current_version, new_version
);
publish_dependencies(&new_version, args.dry_run)?;
publish_gpui(&new_version, args.dry_run)?;
println!("GPUI published in {}s", start_time.elapsed().as_secs_f32());
Ok(())
}
fn read_gpui_version() -> Result<String> {
let gpui_cargo_toml_path = "crates/gpui/Cargo.toml";
let contents = std::fs::read_to_string(gpui_cargo_toml_path)
.context("Failed to read crates/gpui/Cargo.toml")?;
let cargo_toml: toml::Value =
toml::from_str(&contents).context("Failed to parse crates/gpui/Cargo.toml")?;
let version = cargo_toml
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.context("Failed to find version in crates/gpui/Cargo.toml")?;
Ok(version.to_string())
}
fn bump_version(current_version: &str, pre_release: Option<&str>) -> Result<String> {
// Strip any existing metadata and pre-release
let without_metadata = current_version.split('+').next().unwrap();
let base_version = without_metadata.split('-').next().unwrap();
// Parse major.minor.patch
let parts: Vec<&str> = base_version.split('.').collect();
if parts.len() != 3 {
bail!("Invalid version format: {}", current_version);
}
let major: u32 = parts[0].parse().context("Failed to parse major version")?;
let minor: u32 = parts[1].parse().context("Failed to parse minor version")?;
// Always bump minor version
let new_version = format!("{}.{}.0", major, minor + 1);
// Add pre-release if specified
if let Some(pre) = pre_release {
Ok(format!("{}-{}", new_version, pre))
} else {
Ok(new_version)
}
}
fn publish_dependencies(new_version: &str, dry_run: bool) -> Result<()> {
let gpui_dependencies = vec![
("zed-collections", "collections"),
("zed-perf", "perf"),
("zed-util-macros", "util_macros"),
("zed-util", "util"),
("gpui-macros", "gpui_macros"),
("zed-http-client", "http_client"),
("zed-derive-refineable", "derive_refineable"),
("zed-refineable", "refineable"),
("zed-semantic-version", "semantic_version"),
("zed-sum-tree", "sum_tree"),
("zed-media", "media"),
];
for (crate_name, package_name) in gpui_dependencies {
println!(
"Publishing dependency: {} (package: {})",
crate_name, package_name
);
update_crate_version(crate_name, new_version)?;
update_workspace_dependency_version(package_name, new_version)?;
publish_crate(crate_name, dry_run)?;
// println!("Waiting 60s for the rate limit...");
// thread::sleep(Duration::from_secs(60));
}
Ok(())
}
fn publish_gpui(new_version: &str, dry_run: bool) -> Result<()> {
update_crate_version("gpui", new_version)?;
publish_crate("gpui", dry_run)?;
Ok(())
}
fn update_crate_version(package_name: &str, new_version: &str) -> Result<()> {
let output = run_command(
Command::new("cargo")
.arg("set-version")
.arg("--package")
.arg(package_name)
.arg(new_version),
)?;
if !output.status.success() {
bail!("Failed to set version for package {}", package_name);
}
Ok(())
}
fn publish_crate(crate_name: &str, dry_run: bool) -> Result<()> {
let publish_crate_impl = |crate_name, dry_run| {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut command = Command::new(&cargo);
command
.arg("publish")
.arg("--allow-dirty")
.args(["-p", crate_name]);
if dry_run {
command.arg("--dry-run");
}
run_command(&mut command)?;
anyhow::Ok(())
};
if dry_run {
publish_crate_impl(crate_name, true)?;
print!("Press Enter to publish for real (or ctrl-c to abort)...");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
}
publish_crate_impl(crate_name, false)?;
Ok(())
}
fn update_workspace_dependency_version(package_name: &str, new_version: &str) -> Result<()> {
let workspace_cargo_toml_path = "Cargo.toml";
let contents = std::fs::read_to_string(workspace_cargo_toml_path)
.context("Failed to read workspace Cargo.toml")?;
let updated = update_dependency_version_in_toml(&contents, package_name, new_version)?;
std::fs::write(workspace_cargo_toml_path, updated)
.context("Failed to write workspace Cargo.toml")?;
Ok(())
}
fn update_dependency_version_in_toml(
toml_contents: &str,
package_name: &str,
new_version: &str,
) -> Result<String> {
let mut doc = toml_contents
.parse::<toml_edit::DocumentMut>()
.context("Failed to parse TOML")?;
// Navigate to workspace.dependencies.<package_name>
let dependency = doc
.get_mut("workspace")
.and_then(|w| w.get_mut("dependencies"))
.and_then(|d| d.get_mut(package_name))
.context(format!(
"Failed to find {} in workspace dependencies",
package_name
))?;
// Update the version field if it exists
if let Some(dep_table) = dependency.as_table_like_mut() {
if dep_table.contains_key("version") {
dep_table.insert("version", toml_edit::value(new_version));
} else {
bail!(
"No version field found for {} in workspace dependencies",
package_name
);
}
} else {
bail!("{} is not a table in workspace dependencies", package_name);
}
Ok(doc.to_string())
}
fn check_workspace_root() -> Result<()> {
let cwd = std::env::current_dir().context("Failed to get current directory")?;
// Check if Cargo.toml exists in the current directory
let cargo_toml_path = cwd.join("Cargo.toml");
if !cargo_toml_path.exists() {
bail!(
"Cargo.toml not found in current directory. Please run this command from the workspace root."
);
}
// Check if it's a workspace by looking for [workspace] section
let contents =
std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?;
if !contents.contains("[workspace]") {
bail!(
"Current directory does not appear to be a workspace root. Please run this command from the workspace root."
);
}
Ok(())
}
fn ensure_cargo_set_version() -> Result<()> {
let output = run_command(
Command::new("which")
.arg("cargo-set-version")
.stdout(Stdio::piped()),
)
.context("Failed to check for cargo-set-version")?;
if !output.status.success() {
println!("cargo-set-version not found. Installing cargo-edit...");
let install_output = run_command(Command::new("cargo").arg("install").arg("cargo-edit"))?;
if !install_output.status.success() {
bail!("Failed to install cargo-edit");
}
}
Ok(())
}
fn check_git_clean() -> Result<()> {
let output = run_command(
Command::new("git")
.args(["status", "--porcelain"])
.stdout(Stdio::piped())
.stderr(Stdio::piped()),
)?;
if !output.status.success() {
bail!("git status command failed");
}
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
bail!(
"Working directory is not clean. Please commit or stash your changes before publishing."
);
}
Ok(())
}
fn run_command(command: &mut Command) -> Result<Output> {
let command_str = {
let program = command.get_program().to_string_lossy();
let args = command
.get_args()
.map(|arg| arg.to_string_lossy())
.collect::<Vec<_>>()
.join(" ");
if args.is_empty() {
program.to_string()
} else {
format!("{} {}", program, args)
}
};
eprintln!("+ {}", command_str);
let output = command
.spawn()
.context("failed to spawn child process")?
.wait_with_output()
.context("failed to wait for child process")?;
Ok(output)
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
#[test]
fn test_update_dependency_version_in_toml() {
let input = indoc! {r#"
[workspace]
resolver = "2"
[workspace.dependencies]
# here's a comment
collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
"#};
let result = update_dependency_version_in_toml(input, "collections", "0.2.0").unwrap();
let output = indoc! {r#"
[workspace]
resolver = "2"
[workspace.dependencies]
# here's a comment
collections = { path = "crates/collections", package = "zed-collections", version = "0.2.0" }
util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
"#};
assert_eq!(result, output);
}
#[test]
fn test_bump_version() {
// Test bumping minor version (default behavior)
assert_eq!(bump_version("0.1.0", None).unwrap(), "0.2.0");
assert_eq!(bump_version("0.1.5", None).unwrap(), "0.2.0");
assert_eq!(bump_version("1.42.7", None).unwrap(), "1.43.0");
// Test stripping pre-release and bumping minor
assert_eq!(bump_version("0.1.0-alpha.1", None).unwrap(), "0.2.0");
assert_eq!(bump_version("0.1.0-beta", None).unwrap(), "0.2.0");
// Test stripping existing metadata and bumping
assert_eq!(bump_version("0.1.0+old.metadata", None).unwrap(), "0.2.0");
// Test bumping minor with pre-release
assert_eq!(bump_version("0.1.0", Some("alpha")).unwrap(), "0.2.0-alpha");
// Test bumping minor with complex pre-release identifier
assert_eq!(
bump_version("0.1.0", Some("test.1")).unwrap(),
"0.2.0-test.1"
);
// Test bumping from existing pre-release adds new pre-release
assert_eq!(
bump_version("0.1.0-alpha", Some("beta")).unwrap(),
"0.2.0-beta"
);
// Test bumping and stripping metadata while adding pre-release
assert_eq!(
bump_version("0.1.0+metadata", Some("alpha")).unwrap(),
"0.2.0-alpha"
);
}
}