diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 066e02deeb5..561696e2e0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,11 +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 - run: cargo-release release crates --dry-run + run: cargo-release release crates ${{ github.event.inputs.release_tag }} --dry-run - name: Run release if: ${{ !inputs.dry_run }} - run: cargo-release release crates + run: cargo-release release crates ${{ github.event.inputs.release_tag }} release-csharp: needs: build-cargo-release diff --git a/tools/release/README.md b/tools/release/README.md index 103f7ed4c2e..03075d4abfa 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 v1.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 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. + ### 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 758993f9b78..82fc3af0f83 100644 --- a/tools/release/src/targets/crates.rs +++ b/tools/release/src/targets/crates.rs @@ -3,16 +3,21 @@ 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 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 @@ -61,6 +66,50 @@ impl CratesRelease { )) } + 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); @@ -126,8 +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, crates_io_version)?; self.add_crate_owners(crate_name)?; }