From 3ad1c91c3d437bbd81cb61ae60f1fe953b40bb96 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 16 Jun 2026 14:54:59 -0400 Subject: [PATCH 1/4] Wait for crates.io publish propagation --- tools/release/src/targets/crates.rs | 85 +++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tools/release/src/targets/crates.rs b/tools/release/src/targets/crates.rs index 758993f9b78..460a06906c1 100644 --- a/tools/release/src/targets/crates.rs +++ b/tools/release/src/targets/crates.rs @@ -3,8 +3,12 @@ use anyhow::Result; use std::collections::HashMap; use std::path::PathBuf; use std::process::Command; +use std::thread; +use std::time::{Duration, Instant}; const CRATE_OWNERS: &[&str] = &["cloutiertyler", "jdetter", "bfops", "rekhoff", "spacetimedb-devops"]; +const CRATES_IO_POLL_INTERVAL: Duration = Duration::from_secs(15); +const CRATES_IO_POLL_TIMEOUT: Duration = Duration::from_secs(15 * 60); pub struct CratesRelease { pub dry_run: bool, @@ -61,6 +65,85 @@ impl CratesRelease { )) } + fn crate_version(&self, crate_name: &str, manifest_map: &HashMap) -> Result { + let manifest_path = manifest_map + .get(crate_name) + .ok_or_else(|| format!("Crate '{}' not found in cargo metadata", crate_name))?; + + let mut cmd = Command::new("cargo"); + cmd.args(["pkgid", "--manifest-path", &manifest_path.to_string_lossy()]); + util::print_command(&cmd); + + let output = cmd + .output() + .map_err(|e| format!("Failed to execute cargo pkgid for {}: {}", crate_name, e))?; + + if !output.status.success() { + return Err(format!( + "Failed to get package id for {}\n--- stdout ---\n{}\n--- stderr ---\n{}", + crate_name, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + let pkgid = String::from_utf8_lossy(&output.stdout); + pkgid + .trim() + .rsplit_once('@') + .map(|(_, version)| version.to_string()) + .ok_or_else(|| { + format!( + "Failed to parse crate version from cargo pkgid output: {}", + pkgid.trim() + ) + }) + } + + fn wait_for_crate_available(&self, crate_name: &str, version: &str) -> Result<(), String> { + let spec = format!("{}@{}", crate_name, version); + let deadline = Instant::now() + CRATES_IO_POLL_TIMEOUT; + let mut attempt = 1; + + println!( + "Waiting for {} to be visible in the crates.io index before publishing dependent crates...", + spec + ); + + loop { + let mut cmd = Command::new("cargo"); + cmd.args(["info", &spec]); + util::print_command(&cmd); + + let output = cmd + .output() + .map_err(|e| format!("Failed to execute cargo info {}: {}", spec, e))?; + + if output.status.success() { + println!("{} is visible in the crates.io index.", spec); + return Ok(()); + } + + if Instant::now() >= deadline { + return Err(format!( + "Timed out waiting for {} to become visible in the crates.io index\n--- stdout ---\n{}\n--- stderr ---\n{}", + spec, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + println!( + "{} is not visible yet; retrying in {}s (attempt {}).", + spec, + CRATES_IO_POLL_INTERVAL.as_secs(), + attempt + ); + attempt += 1; + thread::sleep(CRATES_IO_POLL_INTERVAL); + } + } + fn add_crate_owners(&self, crate_name: &str) -> Result<(), String> { println!("Adding owners for crate: {}", crate_name); @@ -128,6 +211,8 @@ impl ReleaseTarget for CratesRelease { println!("\nStarting publish process..."); for crate_name in &crates { self.publish_crate(crate_name, &manifest_map)?; + let version = self.crate_version(crate_name, &manifest_map)?; + self.wait_for_crate_available(crate_name, &version)?; self.add_crate_owners(crate_name)?; } From 0c3f301499a271c0e63232705bb786a43b27035a Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 16 Jun 2026 15:01:22 -0400 Subject: [PATCH 2/4] Pass release version to crates target --- .github/workflows/release.yml | 8 ++++-- tools/release/README.md | 6 ++-- tools/release/src/main.rs | 10 +++++-- tools/release/src/targets/crates.rs | 43 +++-------------------------- 4 files changed, 21 insertions(+), 46 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 066e02deeb5..a4341324b86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,11 +123,15 @@ jobs: - name: Run release (dry-run) if: ${{ inputs.dry_run }} # NOTE: This will print a warning that `cargo-release release crates` dry runs are not supported - run: cargo-release release crates --dry-run + env: + RELEASE_TAG: ${{ github.event.inputs.release_tag }} + run: cargo-release release crates "${RELEASE_TAG#v}" --dry-run - name: Run release if: ${{ !inputs.dry_run }} - run: cargo-release release crates + env: + RELEASE_TAG: ${{ github.event.inputs.release_tag }} + run: cargo-release release crates "${RELEASE_TAG#v}" release-csharp: needs: build-cargo-release diff --git a/tools/release/README.md b/tools/release/README.md index 103f7ed4c2e..a6fa52d95f2 100644 --- a/tools/release/README.md +++ b/tools/release/README.md @@ -49,15 +49,17 @@ Release the following packages to crates.io: - sdk ```bash -cargo release crates +cargo release crates 1.2.0 ``` You can also perform a dry run to see what would be published without actually publishing: ```bash -cargo release crates --dry-run +cargo release crates 1.2.0 --dry-run ``` +After each crate is published, the release waits for that crate version to become visible in the crates.io index before publishing dependent crates. + ### NPM Package Release the TypeScript SDK to npm. This will: diff --git a/tools/release/src/main.rs b/tools/release/src/main.rs index 024b330d36c..00dd0065f01 100644 --- a/tools/release/src/main.rs +++ b/tools/release/src/main.rs @@ -33,6 +33,7 @@ struct ReleaseArgs { enum Commands { /// Release crates.io packages Crates { + release_version: String, #[arg(long)] dry_run: bool, }, @@ -82,8 +83,11 @@ fn main() { let CargoCli::Release(release_args) = cli.command; let result = match &release_args.command { - Commands::Crates { dry_run } => { - let target = CratesRelease::new(*dry_run); + Commands::Crates { + release_version: version, + dry_run, + } => { + let target = CratesRelease::new(version.clone(), *dry_run); target.release() } Commands::Csharp { @@ -131,7 +135,7 @@ fn release_all(version: String, skip: Option>, dry_run: bool) -> Res let skip_targets = skip.unwrap_or_default(); let targets: Vec> = vec![ - Box::new(CratesRelease::new(dry_run)), + Box::new(CratesRelease::new(version.clone(), dry_run)), Box::new(NpmRelease::new(version.clone(), dry_run)), Box::new(CSharpRelease::new(version.clone(), dry_run)), Box::new(CppRelease::new(version.clone(), dry_run)), diff --git a/tools/release/src/targets/crates.rs b/tools/release/src/targets/crates.rs index 460a06906c1..951b69b80cd 100644 --- a/tools/release/src/targets/crates.rs +++ b/tools/release/src/targets/crates.rs @@ -11,12 +11,13 @@ const CRATES_IO_POLL_INTERVAL: Duration = Duration::from_secs(15); const CRATES_IO_POLL_TIMEOUT: Duration = Duration::from_secs(15 * 60); pub struct CratesRelease { + pub version: String, pub dry_run: bool, } impl CratesRelease { - pub fn new(dry_run: bool) -> Self { - Self { dry_run } + pub fn new(version: String, dry_run: bool) -> Self { + Self { version, dry_run } } /// Publishes a single crate to crates.io @@ -65,41 +66,6 @@ impl CratesRelease { )) } - fn crate_version(&self, crate_name: &str, manifest_map: &HashMap) -> Result { - let manifest_path = manifest_map - .get(crate_name) - .ok_or_else(|| format!("Crate '{}' not found in cargo metadata", crate_name))?; - - let mut cmd = Command::new("cargo"); - cmd.args(["pkgid", "--manifest-path", &manifest_path.to_string_lossy()]); - util::print_command(&cmd); - - let output = cmd - .output() - .map_err(|e| format!("Failed to execute cargo pkgid for {}: {}", crate_name, e))?; - - if !output.status.success() { - return Err(format!( - "Failed to get package id for {}\n--- stdout ---\n{}\n--- stderr ---\n{}", - crate_name, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - - let pkgid = String::from_utf8_lossy(&output.stdout); - pkgid - .trim() - .rsplit_once('@') - .map(|(_, version)| version.to_string()) - .ok_or_else(|| { - format!( - "Failed to parse crate version from cargo pkgid output: {}", - pkgid.trim() - ) - }) - } - fn wait_for_crate_available(&self, crate_name: &str, version: &str) -> Result<(), String> { let spec = format!("{}@{}", crate_name, version); let deadline = Instant::now() + CRATES_IO_POLL_TIMEOUT; @@ -211,8 +177,7 @@ impl ReleaseTarget for CratesRelease { println!("\nStarting publish process..."); for crate_name in &crates { self.publish_crate(crate_name, &manifest_map)?; - let version = self.crate_version(crate_name, &manifest_map)?; - self.wait_for_crate_available(crate_name, &version)?; + self.wait_for_crate_available(crate_name, &self.version)?; self.add_crate_owners(crate_name)?; } From 7317f8b393bd3b45cff4d8c36848f192592fbb1c Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 16 Jun 2026 15:05:57 -0400 Subject: [PATCH 3/4] Match crates release workflow invocation --- .github/workflows/release.yml | 8 ++------ tools/release/src/targets/crates.rs | 3 ++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4341324b86..561696e2e0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,15 +123,11 @@ jobs: - name: Run release (dry-run) if: ${{ inputs.dry_run }} # NOTE: This will print a warning that `cargo-release release crates` dry runs are not supported - env: - RELEASE_TAG: ${{ github.event.inputs.release_tag }} - run: cargo-release release crates "${RELEASE_TAG#v}" --dry-run + run: cargo-release release crates ${{ github.event.inputs.release_tag }} --dry-run - name: Run release if: ${{ !inputs.dry_run }} - env: - RELEASE_TAG: ${{ github.event.inputs.release_tag }} - run: cargo-release release crates "${RELEASE_TAG#v}" + run: cargo-release release crates ${{ github.event.inputs.release_tag }} release-csharp: needs: build-cargo-release diff --git a/tools/release/src/targets/crates.rs b/tools/release/src/targets/crates.rs index 951b69b80cd..82fc3af0f83 100644 --- a/tools/release/src/targets/crates.rs +++ b/tools/release/src/targets/crates.rs @@ -175,9 +175,10 @@ impl ReleaseTarget for CratesRelease { } println!("\nStarting publish process..."); + let crates_io_version = self.version.strip_prefix('v').unwrap_or(&self.version); for crate_name in &crates { self.publish_crate(crate_name, &manifest_map)?; - self.wait_for_crate_available(crate_name, &self.version)?; + self.wait_for_crate_available(crate_name, crates_io_version)?; self.add_crate_owners(crate_name)?; } From e4234042b739a2d966ee589c6b8ffbd592b0c258 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 16 Jun 2026 15:11:12 -0400 Subject: [PATCH 4/4] Use release tag in crates release docs --- tools/release/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/release/README.md b/tools/release/README.md index a6fa52d95f2..03075d4abfa 100644 --- a/tools/release/README.md +++ b/tools/release/README.md @@ -49,13 +49,13 @@ Release the following packages to crates.io: - sdk ```bash -cargo release crates 1.2.0 +cargo release crates v1.2.0 ``` You can also perform a dry run to see what would be published without actually publishing: ```bash -cargo release crates 1.2.0 --dry-run +cargo release crates v1.2.0 --dry-run ``` After each crate is published, the release waits for that crate version to become visible in the crates.io index before publishing dependent crates.