use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
struct LatestBinaryVersion {
version: String,
}
fn build_latest_url(control_plane_url: &str, robot_id: &str, target: &str) -> String {
format!(
"{}/api/catalog/binaries/by-robot/{}/latest?target={}",
control_plane_url.trim_end_matches('/'),
robot_id,
target
)
}
pub async fn fetch_latest_build_version(
control_plane_url: &str,
robot_id: &str,
target: &str,
api_key: &str,
user_id: &str,
) -> Result<Option<String>> {
let url = build_latest_url(control_plane_url, robot_id, target);
let client = reqwest::Client::new();
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("X-User-ID", user_id)
.send()
.await
.context("Failed to reach registry")?;
if response.status().as_u16() == 404 {
return Ok(None);
}
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
anyhow::bail!("Latest build version check failed with status {}: {}", status, body);
}
let info: LatestBinaryVersion = response
.json()
.await
.context("Failed to parse latest build version response")?;
Ok(Some(info.version))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_latest_url_includes_target_query_param() {
let url = build_latest_url("https://api.example.com", "robot-123", "remote");
assert_eq!(
url,
"https://api.example.com/api/catalog/binaries/by-robot/robot-123/latest?target=remote"
);
}
#[test]
fn build_latest_url_trims_trailing_slash_on_control_plane_url() {
let url = build_latest_url("https://api.example.com/", "robot-123", "robot");
assert_eq!(
url,
"https://api.example.com/api/catalog/binaries/by-robot/robot-123/latest?target=robot"
);
}
}