diff --git a/Cargo.lock b/Cargo.lock index 1379939..ff3849a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1720,6 +1729,35 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "reqsign" version = "0.16.5" @@ -3138,6 +3176,7 @@ dependencies = [ "open", "opendal", "rcgen", + "regex", "reqwest", "rustls", "semver", diff --git a/Cargo.toml b/Cargo.toml index 503c79a..23637cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,9 @@ toml = "0.8" # Error Handling anyhow = "1.0" +# Regex +regex = "1.10" + # Progress & UI indicatif = "0.17" diff --git a/src/builds.rs b/src/builds.rs index 65a8727..fb685cc 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -47,25 +47,28 @@ struct TempCredsResponse { } async fn get_temp_credentials( - org_slug: &str, - game_slug: &str, - branch_slug: &str, + org: &str, + game: &str, + environment: &str, engine: &str, engine_version: &str, + build_version: &str, entrypoint: Option<&str>, + build_message: Option<&str>, api_key: &str, ) -> Result { let client = config::create_http_client()?; let api_host = config::get("api_host")?; let url = format!( - "{}/api/organizations/{}/games/{}/branches/{}/builds/create-temp-r2-creds", - api_host, org_slug, game_slug, branch_slug + "{}/api/organizations/{}/games/{}/environments/{}/builds/create-temp-r2-creds", + api_host, org, game, environment ); let mut request_body = serde_json::json!({ "engine": engine, - "engineVersion": engine_version + "engineVersion": engine_version, + "version": build_version }); // Add entrypoint if provided @@ -73,6 +76,11 @@ async fn get_temp_credentials( request_body["entrypoint"] = serde_json::json!(ep); } + // Add build message if provided + if let Some(msg) = build_message { + request_body["buildMessage"] = serde_json::json!(msg); + } + let response = client .post(&url) .header("Authorization", format!("Bearer {}", api_key)) @@ -101,9 +109,9 @@ async fn get_temp_credentials( } async fn notify_upload_complete( - org_slug: &str, - game_slug: &str, - branch_slug: &str, + org: &str, + game: &str, + environment: &str, build_id: &str, api_key: &str, ) -> Result<()> { @@ -111,8 +119,8 @@ async fn notify_upload_complete( let api_host = config::get("api_host")?; let url = format!( - "{}/api/organizations/{}/games/{}/branches/{}/builds/{}/upload-completed", - api_host, org_slug, game_slug, branch_slug, build_id + "{}/api/organizations/{}/games/{}/environments/{}/builds/{}/upload-completed", + api_host, org, game, environment, build_id ); let response = client @@ -140,7 +148,7 @@ async fn notify_upload_complete( Ok(()) } -pub async fn handle_build_push(config_path: PathBuf, verbose: bool) -> Result<()> { +pub async fn handle_build_push(config_path: PathBuf, message: Option, verbose: bool) -> Result<()> { // Load wavedash.toml config let wavedash_config = WavedashConfig::load(&config_path)?; @@ -167,12 +175,14 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool) -> Result<() // Get temporary R2 credentials let engine_kind = wavedash_config.engine_type()?; let creds = get_temp_credentials( - &wavedash_config.org_slug, - &wavedash_config.game_slug, - &wavedash_config.branch_slug, + &wavedash_config.org, + &wavedash_config.game, + wavedash_config.environment.as_str(), engine_kind.as_config_key(), - wavedash_config.version()?, + wavedash_config.engine_version()?, + wavedash_config.get_build_version(), wavedash_config.entrypoint(), + message.as_deref(), &api_key, ) .await?; @@ -199,9 +209,9 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool) -> Result<() // Notify the server that upload is complete notify_upload_complete( - &wavedash_config.org_slug, - &wavedash_config.game_slug, - &wavedash_config.branch_slug, + &wavedash_config.org, + &wavedash_config.game, + wavedash_config.environment.as_str(), &creds.game_build_id, &api_key, ) diff --git a/src/config.rs b/src/config.rs index 0f78c2c..de234e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use anyhow::Result; use directories::BaseDirs; +use regex::Regex; use serde::Deserialize; use std::path::PathBuf; @@ -102,11 +103,48 @@ pub struct CustomSection { pub entrypoint: String, } +/// The cloud environment for the game build +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Environment { + Production, + Demo, + Sandbox, +} + +impl Environment { + pub fn as_str(&self) -> &'static str { + match self { + Environment::Production => "production", + Environment::Demo => "demo", + Environment::Sandbox => "sandbox", + } + } +} + +impl std::fmt::Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + #[derive(Debug, Deserialize)] pub struct WavedashConfig { - pub org_slug: String, - pub game_slug: String, - pub branch_slug: String, + /// Organization slug + pub org: String, + + /// Game slug + pub game: String, + + /// Environment: production, demo, or sandbox + pub environment: Environment, + + /// Build version in semantic versioning format (major.minor.patch) + /// Example: "1.0.0", "2.1.3" + /// Required - must match a release version to be selectable + #[serde(rename = "version")] + pub build_version: String, + pub upload_dir: PathBuf, #[serde(rename = "godot")] @@ -157,6 +195,15 @@ impl WavedashConfig { let config: WavedashConfig = toml::from_str(&config_content) .map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?; + // Validate build_version format (required, must be semver) + let semver_regex = Regex::new(r"^\d+\.\d+\.\d+$").unwrap(); + if !semver_regex.is_match(&config.build_version) { + anyhow::bail!( + "Invalid version '{}'. Must be in semantic version format: major.minor.patch (e.g., 1.0.0, 2.1.3)", + config.build_version + ); + } + Ok(config) } @@ -175,7 +222,8 @@ impl WavedashConfig { } } - pub fn version(&self) -> Result<&str> { + /// Get the engine version (e.g., Godot version, Unity version) + pub fn engine_version(&self) -> Result<&str> { if let Some(ref godot) = self.godot { Ok(&godot.version) } else if let Some(ref unity) = self.unity { @@ -187,6 +235,11 @@ impl WavedashConfig { } } + /// Get the build version (semantic version: major.minor.patch) + pub fn get_build_version(&self) -> &str { + &self.build_version + } + pub fn entrypoint(&self) -> Option<&str> { self.custom.as_ref().map(|c| c.entrypoint.as_str()) } diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 6224015..238ac9c 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -113,7 +113,7 @@ pub async fn handle_dev(config_path: Option, verbose: bool, no_open: bo let sandbox_url = build_sandbox_url( &wavedash_config, engine_label, - wavedash_config.version()?, + wavedash_config.engine_version()?, &local_origin, entrypoint.as_deref(), entrypoint_params.as_ref(), diff --git a/src/dev/sandbox.rs b/src/dev/sandbox.rs index 4690fd5..bd803b5 100644 --- a/src/dev/sandbox.rs +++ b/src/dev/sandbox.rs @@ -18,15 +18,15 @@ pub fn build_sandbox_url( let base = host.trim_end_matches('/'); // First, build the game URL (what will be the rdurl parameter) - let game_url_full = format!("{}/play/{}", base, wavedash_config.game_slug); + let game_url_full = format!("{}/play/{}", base, wavedash_config.game); let mut game_url = Url::parse(&game_url_full) .with_context(|| format!("Unable to parse website host {}", game_url_full))?; { let mut pairs = game_url.query_pairs_mut(); - pairs.append_pair(UrlParams::GAME_SUBDOMAIN, &wavedash_config.game_slug); - pairs.append_pair(UrlParams::GAME_BRANCH_SLUG, &wavedash_config.branch_slug); - pairs.append_pair(UrlParams::GAME_CLOUD_ID, &wavedash_config.org_slug); + pairs.append_pair(UrlParams::GAME_SUBDOMAIN, &wavedash_config.game); + pairs.append_pair(UrlParams::GAME_ENVIRONMENT, wavedash_config.environment.as_str()); + pairs.append_pair(UrlParams::GAME_CLOUD_ID, &wavedash_config.org); pairs.append_pair(UrlParams::LOCAL_ORIGIN, local_origin); pairs.append_pair(UrlParams::SANDBOX, "true"); pairs.append_pair(UrlParams::ENGINE, engine_label); @@ -47,7 +47,7 @@ pub fn build_sandbox_url( let host_url = Url::parse(&format!("https://{}", base.trim_start_matches("https://").trim_start_matches("http://")))?; let main_host = host_url.host_str().ok_or_else(|| anyhow::anyhow!("Could not extract host"))?; - let subdomain = format!("{}.sandbox.{}", wavedash_config.game_slug, main_host); + let subdomain = format!("{}.sandbox.{}", wavedash_config.game, main_host); let permission_grant_url = format!("https://{}/sandbox/permission-grant", subdomain); let mut url = Url::parse(&permission_grant_url)?; diff --git a/src/dev/url_params.rs b/src/dev/url_params.rs index e48474a..34d2703 100644 --- a/src/dev/url_params.rs +++ b/src/dev/url_params.rs @@ -2,7 +2,7 @@ pub struct UrlParams; impl UrlParams { pub const GAME_SUBDOMAIN: &'static str = "gsdomain"; - pub const GAME_BRANCH_SLUG: &'static str = "gbrslug"; + pub const GAME_ENVIRONMENT: &'static str = "genv"; pub const GAME_CLOUD_ID: &'static str = "gcid"; pub const SANDBOX: &'static str = "sandbox"; pub const LOCAL_ORIGIN: &'static str = "localorigin"; diff --git a/src/main.rs b/src/main.rs index 40ff81f..0bf30fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,6 +75,8 @@ enum BuildCommands { default_value = "./wavedash.toml" )] config: PathBuf, + #[arg(short = 'm', long = "message", help = "Build message (like a commit message)")] + message: Option, }, } @@ -144,8 +146,8 @@ async fn main() -> Result<()> { } } Commands::Build { action } => match action { - BuildCommands::Push { config } => { - handle_build_push(config, cli.verbose).await?; + BuildCommands::Push { config, message } => { + handle_build_push(config, message, cli.verbose).await?; } }, Commands::Dev { config, no_open } => {