Skip to content

Commit

Permalink
Merge pull request #814 from hendrikmaus/#538_wait-for-crate-versions…
Browse files Browse the repository at this point in the history
…-to-appear

Release operator: Wait for crate version to appear upstream
  • Loading branch information
hannobraun authored Jul 18, 2022
2 parents f791034 + ea759e0 commit 8681d23
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 71 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tools/release-operator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ regex = "1.6.0"
secstr = "0.5.0"
semver = "1.0.12"
serde_json = "1.0.82"
thiserror = "1.0.31"

[dependencies.reqwest]
version = "0.11.11"
Expand Down
189 changes: 118 additions & 71 deletions tools/release-operator/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use std::time::{Duration, Instant};

#[derive(thiserror::Error, Debug)]
enum Error {
#[error("crate was not found on crates.io")]
CrateNotFound,
}

pub struct Registry {
token: SecUtf8,
Expand Down Expand Up @@ -63,90 +70,99 @@ impl Crate {
}
}

fn determine_state(&self) -> anyhow::Result<CrateState> {
let theirs = {
#[derive(Deserialize)]
struct CrateVersions {
versions: Vec<CrateVersion>,
}
fn get_local_version(&self) -> anyhow::Result<semver::Version> {
let name = format!("{self}");
let cargo_toml_location = std::fs::canonicalize(&self.path)
.context("absolute path to Cargo.toml")?;
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.manifest_path(format!(
"{}/Cargo.toml",
cargo_toml_location.to_string_lossy()
))
.no_deps();

let metadata = cmd.exec()?;
let package = metadata
.packages
.iter()
.find(|p| p.name == name)
.ok_or_else(|| anyhow!("could not find package"))?;

let version = package.version.to_owned();
log::debug!("{self} found as {version} on our side");

Ok(version)
}

#[derive(Deserialize)]
struct CrateVersion {
#[serde(rename = "num")]
version: semver::Version,
}
fn get_upstream_version(&self) -> anyhow::Result<semver::Version> {
#[derive(Deserialize)]
struct CrateVersions {
versions: Vec<CrateVersion>,
}

let client = reqwest::blocking::ClientBuilder::new()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.build()
.context("build http client")?;

let resp = client
.get(format!("https://crates.io/api/v1/crates/{self}"))
.send()
.context("fetching crate versions from the registry")?;

if resp.status() == reqwest::StatusCode::NOT_FOUND {
log::info!("{self} has not been published yet");
return Ok(CrateState::Unknown);
}
#[derive(Deserialize)]
struct CrateVersion {
#[serde(rename = "num")]
version: semver::Version,
}

if resp.status() != reqwest::StatusCode::OK {
return Err(anyhow!(
"{self} request to crates.io failed with {} '{}'",
resp.status(),
resp.text().unwrap_or_else(|_| {
"[response body could not be read]".to_string()
})
));
}
let client = reqwest::blocking::ClientBuilder::new()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.build()
.context("build http client")?;

let versions =
serde_json::from_str::<CrateVersions>(resp.text()?.as_str())
.context("deserializing crates.io response")?;
let resp = client
.get(format!("https://crates.io/api/v1/crates/{self}"))
.send()
.context("fetching crate versions from the registry")?;

versions.versions.get(0).unwrap().version.to_owned()
};
if resp.status() == reqwest::StatusCode::NOT_FOUND {
return Err(anyhow::Error::new(Error::CrateNotFound));
}

let ours = {
let name = format!("{self}");
let cargo_toml_location = std::fs::canonicalize(&self.path)
.context("absolute path to Cargo.toml")?;
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.manifest_path(format!(
"{}/Cargo.toml",
cargo_toml_location.to_string_lossy()
))
.no_deps();
if resp.status() != reqwest::StatusCode::OK {
return Err(anyhow!(
"{self} request to crates.io failed with {} '{}'",
resp.status(),
resp.text().unwrap_or_else(|_| {
"[response body could not be read]".to_string()
})
));
}

let metadata = cmd.exec()?;
let package = metadata
.packages
.iter()
.find(|p| p.name == name)
.ok_or_else(|| anyhow!("could not find package"))?;
let versions =
serde_json::from_str::<CrateVersions>(resp.text()?.as_str())
.context("deserializing crates.io response")?;

let version = package.version.to_owned();
log::debug!("{self} found as {version} on our side");
Ok(versions.versions.get(0).unwrap().version.to_owned())
}

version
fn determine_state(&self) -> anyhow::Result<CrateState> {
let theirs = match self.get_upstream_version() {
Ok(version) => version,
Err(error) => match error.downcast_ref::<Error>() {
Some(Error::CrateNotFound) => return Ok(CrateState::Unknown),
None => return Err(error),
},
};

if ours == theirs {
log::info!("{self} has already been published as {ours}");
return Ok(CrateState::Published);
}
let ours = self.get_local_version()?;

if ours < theirs {
log::warn!("{self} has already been published as {ours}, which is a newer version");
return Ok(CrateState::Behind);
match theirs.cmp(&ours) {
std::cmp::Ordering::Less => Ok(CrateState::Ahead),
std::cmp::Ordering::Equal => {
log::info!("{self} has already been published as {ours}");
Ok(CrateState::Published)
}
std::cmp::Ordering::Greater => {
log::warn!("{self} has already been published as {ours}, which is a newer version");
Ok(CrateState::Behind)
}
}

Ok(CrateState::Ahead)
}

fn submit(&self, token: &SecUtf8, dry_run: bool) -> anyhow::Result<()> {
Expand Down Expand Up @@ -175,6 +191,37 @@ impl Crate {
}
}

let ours = self.get_local_version()?;
let delay = Duration::from_secs(10);
let start_time = Instant::now();
let timeout = Duration::from_secs(600);

log::info!(
"{self} should appear as {ours} on the registry, waiting... [{delay:?}|{timeout:?}]"
);

loop {
if Instant::now() - start_time > timeout {
return Err(anyhow!("{self} did not appear as {ours} on the registry within {timeout:?}"));
}

let theirs = self.get_upstream_version()?;

match theirs.cmp(&ours) {
std::cmp::Ordering::Less => (),
std::cmp::Ordering::Equal => {
log::info!("{self} appeared as {ours} on the registry");
break;
}
std::cmp::Ordering::Greater => {
return Err(anyhow!("{self} appeared as {theirs} on the registry which is newer than the current release ({ours})"))
},
}

log::info!("{self} waiting for {ours}...");
std::thread::sleep(delay);
}

Ok(())
}
}
Expand Down

0 comments on commit 8681d23

Please sign in to comment.