From 365b558d7e669b2baa55434887579347c35883a0 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 2 Jul 2026 17:44:54 +0530 Subject: [PATCH 1/4] fix: use supported Wrangler validation build --- crates/edgezero-adapter-cloudflare/src/cli.rs | 15 +++++++++++++-- docs/guide/configuration.md | 2 +- examples/app-demo/edgezero.toml | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/edgezero-adapter-cloudflare/src/cli.rs b/crates/edgezero-adapter-cloudflare/src/cli.rs index f858021c..8e07d938 100644 --- a/crates/edgezero-adapter-cloudflare/src/cli.rs +++ b/crates/edgezero-adapter-cloudflare/src/cli.rs @@ -38,7 +38,7 @@ static CLOUDFLARE_BLUEPRINT: AdapterBlueprint = AdapterBlueprint { build_features: &["cloudflare"], }, commands: CommandTemplates { - build: "wrangler build --cwd {crate_dir}", + build: "wrangler deploy --dry-run --cwd {crate_dir}", deploy: "wrangler deploy --cwd {crate_dir}", serve: "wrangler dev --cwd {crate_dir}", }, @@ -919,7 +919,7 @@ fn read_wrangler_kv_key( } /// # Errors -/// Returns an error if the Cloudflare wrangler build command fails. +/// Returns an error if the Cloudflare Cargo build command fails. #[inline] pub fn build(extra_args: &[String]) -> Result { let manifest = @@ -1148,6 +1148,17 @@ mod tests { const TEST_CONFIG_ID: &str = "app_config"; const TEST_SECRET_ID: &str = "default"; + #[test] + fn cloudflare_scaffold_does_not_emit_removed_wrangler_build_command() { + let command = CLOUDFLARE_BLUEPRINT.commands.build; + assert!(!command.contains("wrangler build"), "{command}"); + assert_eq!( + command, + "wrangler deploy --dry-run --cwd {crate_dir}", + "the validation build must execute Wrangler's real custom-build pipeline" + ); + } + /// RAII guard: prepends a directory to `$PATH` and restores the original /// value on drop. Mirrors the `PathPrepend` used in `push_cloud.rs`. #[cfg(unix)] diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index ce283df6..14b0d83c 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -462,7 +462,7 @@ target = "wasm32-unknown-unknown" profile = "release" [adapters.cloudflare.commands] -build = "wrangler build --cwd crates/my-app-adapter-cloudflare" +build = "wrangler deploy --dry-run --cwd crates/my-app-adapter-cloudflare" deploy = "wrangler deploy --cwd crates/my-app-adapter-cloudflare" serve = "wrangler dev --cwd crates/my-app-adapter-cloudflare" diff --git a/examples/app-demo/edgezero.toml b/examples/app-demo/edgezero.toml index bd8b93cd..23ee473c 100644 --- a/examples/app-demo/edgezero.toml +++ b/examples/app-demo/edgezero.toml @@ -172,7 +172,7 @@ profile = "release" features = ["cloudflare"] [adapters.cloudflare.commands] -build = "wrangler build --cwd crates/app-demo-adapter-cloudflare" +build = "wrangler deploy --dry-run --cwd crates/app-demo-adapter-cloudflare" deploy = "wrangler deploy --cwd crates/app-demo-adapter-cloudflare" serve = "wrangler dev --cwd crates/app-demo-adapter-cloudflare" From 9e839ebe20f36a4c453ce05f317b3a1db5ff7c9c Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 2 Jul 2026 18:08:36 +0530 Subject: [PATCH 2/4] fix: reject ambiguous provider manifests --- crates/edgezero-adapter-cloudflare/src/cli.rs | 40 ++++++---- crates/edgezero-adapter-fastly/src/cli.rs | 37 ++++++--- crates/edgezero-adapter-spin/src/cli.rs | 37 ++++++--- crates/edgezero-adapter/src/cli_support.rs | 79 +++++++++++++++++++ 4 files changed, 155 insertions(+), 38 deletions(-) diff --git a/crates/edgezero-adapter-cloudflare/src/cli.rs b/crates/edgezero-adapter-cloudflare/src/cli.rs index 8e07d938..2a446b2b 100644 --- a/crates/edgezero-adapter-cloudflare/src/cli.rs +++ b/crates/edgezero-adapter-cloudflare/src/cli.rs @@ -7,7 +7,8 @@ use std::process::Command; use ctor::ctor; use edgezero_adapter::cli_support::{ - find_manifest_upwards, find_workspace_root, path_distance, read_package_name, run_native_cli, + find_manifest_upwards, find_workspace_root, read_package_name, run_native_cli, + select_nearest_manifest, }; use edgezero_adapter::registry::{ register_adapter, Adapter, AdapterAction, AdapterPushContext, ProvisionStores, ReadConfigEntry, @@ -1024,7 +1025,7 @@ fn find_wrangler_manifest(start: &Path) -> Result { } let root = find_workspace_root(start); - let mut candidates: Vec = WalkDir::new(&root) + let candidates: Vec = WalkDir::new(&root) .follow_links(true) .max_depth(8) .into_iter() @@ -1038,16 +1039,7 @@ fn find_wrangler_manifest(start: &Path) -> Result { }) .collect(); - if candidates.is_empty() { - return Err("could not locate wrangler.toml".to_owned()); - } - - candidates.sort_by_key(|path| { - let parent = path.parent().unwrap_or(Path::new("")); - path_distance(start, parent) - }); - - Ok(candidates.remove(0)) + select_nearest_manifest(start, candidates, "wrangler.toml") } fn locate_artifact( @@ -1153,12 +1145,32 @@ mod tests { let command = CLOUDFLARE_BLUEPRINT.commands.build; assert!(!command.contains("wrangler build"), "{command}"); assert_eq!( - command, - "wrangler deploy --dry-run --cwd {crate_dir}", + command, "wrangler deploy --dry-run --cwd {crate_dir}", "the validation build must execute Wrangler's real custom-build pipeline" ); } + #[test] + fn rejects_equidistant_wrangler_manifests() { + let dir = tempdir().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); + + let first = root.join("apps/first"); + fs::create_dir_all(&first).unwrap(); + fs::write(first.join("Cargo.toml"), "[package]\nname=\"first\"").unwrap(); + fs::write(first.join("wrangler.toml"), "name=\"first\"").unwrap(); + + let second = root.join("apps/second"); + fs::create_dir_all(&second).unwrap(); + fs::write(second.join("Cargo.toml"), "[package]\nname=\"second\"").unwrap(); + fs::write(second.join("wrangler.toml"), "name=\"second\"").unwrap(); + + let error = find_wrangler_manifest(root).expect_err("equidistant manifests are ambiguous"); + assert!(error.contains(first.join("wrangler.toml").to_string_lossy().as_ref())); + assert!(error.contains(second.join("wrangler.toml").to_string_lossy().as_ref())); + } + /// RAII guard: prepends a directory to `$PATH` and restores the original /// value on drop. Mirrors the `PathPrepend` used in `push_cloud.rs`. #[cfg(unix)] diff --git a/crates/edgezero-adapter-fastly/src/cli.rs b/crates/edgezero-adapter-fastly/src/cli.rs index a3de1cba..20844e74 100644 --- a/crates/edgezero-adapter-fastly/src/cli.rs +++ b/crates/edgezero-adapter-fastly/src/cli.rs @@ -8,7 +8,8 @@ use std::process::Stdio; use crate::chunked_config::{prepare_fastly_config_entries, resolve_fastly_config_value}; use ctor::ctor; use edgezero_adapter::cli_support::{ - find_manifest_upwards, find_workspace_root, path_distance, read_package_name, run_native_cli, + find_manifest_upwards, find_workspace_root, read_package_name, run_native_cli, + select_nearest_manifest, }; use edgezero_adapter::registry::{ register_adapter, Adapter, AdapterAction, AdapterPushContext, ProvisionStores, ReadConfigEntry, @@ -1284,7 +1285,7 @@ fn find_fastly_manifest(start: &Path) -> Result { } let root = find_workspace_root(start); - let mut candidates: Vec = WalkDir::new(&root) + let candidates: Vec = WalkDir::new(&root) .follow_links(true) .max_depth(8) .into_iter() @@ -1298,16 +1299,7 @@ fn find_fastly_manifest(start: &Path) -> Result { }) .collect(); - if candidates.is_empty() { - return Err("could not locate fastly.toml".to_owned()); - } - - candidates.sort_by_key(|path| { - let parent = path.parent().unwrap_or(Path::new("")); - path_distance(start, parent) - }); - - Ok(candidates.remove(0)) + select_nearest_manifest(start, candidates, "fastly.toml") } fn locate_artifact( @@ -1441,6 +1433,27 @@ mod tests { } } + #[test] + fn rejects_equidistant_fastly_manifests() { + let dir = tempdir().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); + + let first = root.join("apps/first"); + fs::create_dir_all(&first).unwrap(); + fs::write(first.join("Cargo.toml"), "[package]\nname=\"first\"").unwrap(); + fs::write(first.join("fastly.toml"), "name=\"first\"").unwrap(); + + let second = root.join("apps/second"); + fs::create_dir_all(&second).unwrap(); + fs::write(second.join("Cargo.toml"), "[package]\nname=\"second\"").unwrap(); + fs::write(second.join("fastly.toml"), "name=\"second\"").unwrap(); + + let error = find_fastly_manifest(root).expect_err("equidistant manifests are ambiguous"); + assert!(error.contains(first.join("fastly.toml").to_string_lossy().as_ref())); + assert!(error.contains(second.join("fastly.toml").to_string_lossy().as_ref())); + } + #[test] fn finds_closest_manifest_when_multiple_exist() { let dir = tempdir().unwrap(); diff --git a/crates/edgezero-adapter-spin/src/cli.rs b/crates/edgezero-adapter-spin/src/cli.rs index 11c1d6a2..e1d8ef23 100644 --- a/crates/edgezero-adapter-spin/src/cli.rs +++ b/crates/edgezero-adapter-spin/src/cli.rs @@ -14,7 +14,8 @@ use std::process::Command; use ctor::ctor; use edgezero_adapter::cli_support::{ - find_manifest_upwards, find_workspace_root, path_distance, read_package_name, run_native_cli, + find_manifest_upwards, find_workspace_root, read_package_name, run_native_cli, + select_nearest_manifest, }; use edgezero_adapter::registry::{ register_adapter, Adapter, AdapterAction, AdapterPushContext, ProvisionStores, ReadConfigEntry, @@ -1115,7 +1116,7 @@ fn find_spin_manifest(start: &Path) -> Result { } let root = find_workspace_root(start); - let mut candidates: Vec = WalkDir::new(&root) + let candidates: Vec = WalkDir::new(&root) .follow_links(true) .max_depth(8) .into_iter() @@ -1129,16 +1130,7 @@ fn find_spin_manifest(start: &Path) -> Result { }) .collect(); - if candidates.is_empty() { - return Err("could not locate spin.toml".to_owned()); - } - - candidates.sort_by_key(|path| { - let parent = path.parent().unwrap_or(Path::new("")); - path_distance(start, parent) - }); - - Ok(candidates.remove(0)) + select_nearest_manifest(start, candidates, "spin.toml") } fn locate_artifact( @@ -1436,6 +1428,27 @@ mod tests { assert_eq!(SpinCliAdapter.single_store_kinds(), &["secrets"]); } + #[test] + fn rejects_equidistant_spin_manifests() { + let dir = tempdir().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); + + let first = root.join("apps/first"); + fs::create_dir_all(&first).unwrap(); + fs::write(first.join("Cargo.toml"), "[package]\nname=\"first\"").unwrap(); + fs::write(first.join("spin.toml"), "spin_manifest_version = 2").unwrap(); + + let second = root.join("apps/second"); + fs::create_dir_all(&second).unwrap(); + fs::write(second.join("Cargo.toml"), "[package]\nname=\"second\"").unwrap(); + fs::write(second.join("spin.toml"), "spin_manifest_version = 2").unwrap(); + + let error = find_spin_manifest(root).expect_err("equidistant manifests are ambiguous"); + assert!(error.contains(first.join("spin.toml").to_string_lossy().as_ref())); + assert!(error.contains(second.join("spin.toml").to_string_lossy().as_ref())); + } + #[test] fn finds_closest_manifest_when_multiple_exist() { let dir = tempdir().unwrap(); diff --git a/crates/edgezero-adapter/src/cli_support.rs b/crates/edgezero-adapter/src/cli_support.rs index 94ace713..4e33b3f8 100644 --- a/crates/edgezero-adapter/src/cli_support.rs +++ b/crates/edgezero-adapter/src/cli_support.rs @@ -66,6 +66,59 @@ pub fn path_distance(left: &Path, right: &Path) -> usize { .saturating_add(right_components.len().saturating_sub(common)) } +/// Select the unique nearest provider manifest instead of making an +/// order-dependent choice. +/// +/// # Errors +/// Returns an error when no candidate exists or multiple candidates have the +/// same minimum distance from `start`. +#[inline] +pub fn select_nearest_manifest( + start: &Path, + mut candidates: Vec, + manifest_name: &str, +) -> Result { + if candidates.is_empty() { + return Err(format!("could not locate {manifest_name}")); + } + + candidates.sort(); + let mut ranked: Vec<(usize, PathBuf)> = candidates + .into_iter() + .map(|path| { + let parent = path.parent().unwrap_or(Path::new("")); + (path_distance(start, parent), path) + }) + .collect(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); + + let nearest_distance = ranked + .first() + .map(|(distance, _)| *distance) + .ok_or_else(|| format!("could not select {manifest_name}"))?; + let mut nearest: Vec<_> = ranked + .into_iter() + .filter(|(distance, _)| *distance == nearest_distance) + .map(|(_, path)| path) + .collect(); + + if nearest.len() == 1 { + return nearest + .pop() + .ok_or_else(|| format!("could not select {manifest_name}")); + } + + let rendered = nearest + .iter() + .map(|path| format!(" - {}", path.display())) + .collect::>() + .join("\n"); + Err(format!( + "ambiguous {manifest_name}: multiple equally near manifests found:\n{rendered}\n\ + set the working directory to the intended adapter crate or define an explicit adapter command" + )) +} + /// Spawn `program args…` inheriting parent stdio, returning a /// human-readable error message. /// @@ -193,6 +246,32 @@ mod tests { assert_eq!(path_distance(left, right), 3); } + #[test] + fn nearest_manifest_returns_unique_closest_candidate() { + let start = Path::new("/repo/apps/a"); + let candidates = vec![ + PathBuf::from("/repo/apps/a/fastly.toml"), + PathBuf::from("/repo/apps/b/fastly.toml"), + ]; + let selected = + select_nearest_manifest(start, candidates, "fastly.toml").expect("unique manifest"); + assert_eq!(selected, PathBuf::from("/repo/apps/a/fastly.toml")); + } + + #[test] + fn nearest_manifest_rejects_equal_distance_candidates() { + let start = Path::new("/repo"); + let candidates = vec![ + PathBuf::from("/repo/apps/a/fastly.toml"), + PathBuf::from("/repo/apps/b/fastly.toml"), + ]; + let error = + select_nearest_manifest(start, candidates, "fastly.toml").expect_err("ambiguous"); + assert!(error.contains("ambiguous fastly.toml")); + assert!(error.contains("/repo/apps/a/fastly.toml")); + assert!(error.contains("/repo/apps/b/fastly.toml")); + } + #[test] fn read_package_prefers_package_table() { let dir = tempdir().unwrap(); From 98fe8cc620c892c8523fca2b1c936b8fdabc69a6 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 2 Jul 2026 18:21:34 +0530 Subject: [PATCH 3/4] test: preserve Cloudflare module item ordering --- crates/edgezero-adapter-cloudflare/src/cli.rs | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/crates/edgezero-adapter-cloudflare/src/cli.rs b/crates/edgezero-adapter-cloudflare/src/cli.rs index 2a446b2b..7ae79376 100644 --- a/crates/edgezero-adapter-cloudflare/src/cli.rs +++ b/crates/edgezero-adapter-cloudflare/src/cli.rs @@ -1140,37 +1140,6 @@ mod tests { const TEST_CONFIG_ID: &str = "app_config"; const TEST_SECRET_ID: &str = "default"; - #[test] - fn cloudflare_scaffold_does_not_emit_removed_wrangler_build_command() { - let command = CLOUDFLARE_BLUEPRINT.commands.build; - assert!(!command.contains("wrangler build"), "{command}"); - assert_eq!( - command, "wrangler deploy --dry-run --cwd {crate_dir}", - "the validation build must execute Wrangler's real custom-build pipeline" - ); - } - - #[test] - fn rejects_equidistant_wrangler_manifests() { - let dir = tempdir().unwrap(); - let root = dir.path(); - fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); - - let first = root.join("apps/first"); - fs::create_dir_all(&first).unwrap(); - fs::write(first.join("Cargo.toml"), "[package]\nname=\"first\"").unwrap(); - fs::write(first.join("wrangler.toml"), "name=\"first\"").unwrap(); - - let second = root.join("apps/second"); - fs::create_dir_all(&second).unwrap(); - fs::write(second.join("Cargo.toml"), "[package]\nname=\"second\"").unwrap(); - fs::write(second.join("wrangler.toml"), "name=\"second\"").unwrap(); - - let error = find_wrangler_manifest(root).expect_err("equidistant manifests are ambiguous"); - assert!(error.contains(first.join("wrangler.toml").to_string_lossy().as_ref())); - assert!(error.contains(second.join("wrangler.toml").to_string_lossy().as_ref())); - } - /// RAII guard: prepends a directory to `$PATH` and restores the original /// value on drop. Mirrors the `PathPrepend` used in `push_cloud.rs`. #[cfg(unix)] @@ -1206,6 +1175,37 @@ mod tests { } } + #[test] + fn cloudflare_scaffold_does_not_emit_removed_wrangler_build_command() { + let command = CLOUDFLARE_BLUEPRINT.commands.build; + assert!(!command.contains("wrangler build"), "{command}"); + assert_eq!( + command, "wrangler deploy --dry-run --cwd {crate_dir}", + "the validation build must execute Wrangler's real custom-build pipeline" + ); + } + + #[test] + fn rejects_equidistant_wrangler_manifests() { + let dir = tempdir().unwrap(); + let root = dir.path(); + fs::write(root.join("Cargo.toml"), "[workspace]").unwrap(); + + let first = root.join("apps/first"); + fs::create_dir_all(&first).unwrap(); + fs::write(first.join("Cargo.toml"), "[package]\nname=\"first\"").unwrap(); + fs::write(first.join("wrangler.toml"), "name=\"first\"").unwrap(); + + let second = root.join("apps/second"); + fs::create_dir_all(&second).unwrap(); + fs::write(second.join("Cargo.toml"), "[package]\nname=\"second\"").unwrap(); + fs::write(second.join("wrangler.toml"), "name=\"second\"").unwrap(); + + let error = find_wrangler_manifest(root).expect_err("equidistant manifests are ambiguous"); + assert!(error.contains(first.join("wrangler.toml").to_string_lossy().as_ref())); + assert!(error.contains(second.join("wrangler.toml").to_string_lossy().as_ref())); + } + // ---------- extract_namespace_id ---------- #[test] From b35822af838e8398ccc5040529eaa03f562c0baf Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 2 Jul 2026 18:21:35 +0530 Subject: [PATCH 4/4] docs: define deployment argument and manifest contracts --- docs/guide/cli-reference.md | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index ce42d628..89bb3469 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -103,12 +103,18 @@ edgezero build --adapter axum The command executes the `build` command from `[adapters..commands]` in `edgezero.toml`, or falls back to the built-in adapter helper. -Any arguments after `--` are forwarded to the adapter command: +Any arguments after `--` are forwarded unchanged to the manifest-defined or +built-in adapter build command: ```bash -edgezero build --adapter fastly -- --flag value +edgezero build --adapter -- ``` +Use passthrough arguments only for non-secret options. EdgeZero omits them from +its own command log, but the provider CLI, shell, process list, or CI runner may +still expose them. Pass credentials through the provider's supported +environment variables or credential store instead. + ### edgezero serve Run the provider-specific local server: @@ -169,6 +175,16 @@ edgezero deploy --adapter cloudflare edgezero deploy --adapter spin ``` +Any arguments after `--` are forwarded unchanged to the manifest-defined or +built-in adapter deploy command: + +```bash +edgezero deploy --adapter -- +``` + +As with build arguments, passthrough deploy arguments must not contain secrets. +Use environment variables or the provider's credential store for credentials. + **Provider behavior:** - **Fastly**: Runs `fastly compute deploy` @@ -179,6 +195,29 @@ edgezero deploy --adapter spin The `axum` adapter doesn't support `deploy` - use standard container/binary deployment instead. ::: +#### Command dispatch and provider-manifest resolution + +For `build` and `deploy`, a command explicitly configured in +`[adapters..commands]` takes precedence over the built-in adapter helper. +EdgeZero runs that command from the directory containing `edgezero.toml` and +appends any passthrough arguments after shell-escaping them. + +When no explicit command is configured, EdgeZero delegates to the registered +built-in adapter. The Fastly, Cloudflare, and Spin helpers resolve their +provider manifest (`fastly.toml`, `wrangler.toml`, or `spin.toml`) as follows: + +1. Search the current working directory and then its ancestors. The first + matching provider manifest wins. +2. If the upward search finds nothing, scan the Cargo workspace and select the + unique manifest whose parent directory is nearest to the current working + directory. +3. If multiple manifests are equally near, fail with an error listing the + candidates. Run the command from the intended adapter crate or define the + explicit `[adapters..commands]` entry to remove the ambiguity. + +This avoids silently deploying a different application from a multi-adapter or +multi-application workspace. + ### edgezero config validate Validate `edgezero.toml` together with the typed `.toml` app