Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions tools/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 7 additions & 3 deletions tools/release/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct ReleaseArgs {
enum Commands {
/// Release crates.io packages
Crates {
release_version: String,
#[arg(long)]
dry_run: bool,
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -131,7 +135,7 @@ fn release_all(version: String, skip: Option<Vec<String>>, dry_run: bool) -> Res
let skip_targets = skip.unwrap_or_default();

let targets: Vec<Box<dyn ReleaseTarget>> = 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)),
Expand Down
55 changes: 53 additions & 2 deletions tools/release/src/targets/crates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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)?;
}

Expand Down
Loading