Cargo.lock 🔗
@@ -19803,6 +19803,9 @@ dependencies = [
"cargo_metadata",
"cargo_toml",
"clap",
+ "indoc",
+ "toml 0.8.20",
+ "toml_edit",
"workspace-hack",
]
Mikayla Maki created
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
Cargo.lock | 3
Cargo.toml | 1
tooling/xtask/Cargo.toml | 3
tooling/xtask/src/main.rs | 3
tooling/xtask/src/tasks.rs | 1
tooling/xtask/src/tasks/publish_gpui.rs | 386 +++++++++++++++++++++++++++
6 files changed, 397 insertions(+)
@@ -19803,6 +19803,9 @@ dependencies = [
"cargo_metadata",
"cargo_toml",
"clap",
+ "indoc",
+ "toml 0.8.20",
+ "toml_edit",
"workspace-hack",
]
@@ -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"
@@ -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
@@ -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,3 +1,4 @@
pub mod clippy;
pub mod licenses;
pub mod package_conformity;
+pub mod publish_gpui;
@@ -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(¤t_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"
+ );
+ }
+}