Skip to content
Draft
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
51 changes: 37 additions & 14 deletions crates/edgezero-adapter-cloudflare/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,7 +39,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}",
},
Expand Down Expand Up @@ -919,7 +920,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<PathBuf, String> {
let manifest =
Expand Down Expand Up @@ -1024,7 +1025,7 @@ fn find_wrangler_manifest(start: &Path) -> Result<PathBuf, String> {
}

let root = find_workspace_root(start);
let mut candidates: Vec<PathBuf> = WalkDir::new(&root)
let candidates: Vec<PathBuf> = WalkDir::new(&root)
.follow_links(true)
.max_depth(8)
.into_iter()
Expand All @@ -1038,16 +1039,7 @@ fn find_wrangler_manifest(start: &Path) -> Result<PathBuf, String> {
})
.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(
Expand Down Expand Up @@ -1183,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]
Expand Down
37 changes: 25 additions & 12 deletions crates/edgezero-adapter-fastly/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1284,7 +1285,7 @@ fn find_fastly_manifest(start: &Path) -> Result<PathBuf, String> {
}

let root = find_workspace_root(start);
let mut candidates: Vec<PathBuf> = WalkDir::new(&root)
let candidates: Vec<PathBuf> = WalkDir::new(&root)
.follow_links(true)
.max_depth(8)
.into_iter()
Expand All @@ -1298,16 +1299,7 @@ fn find_fastly_manifest(start: &Path) -> Result<PathBuf, String> {
})
.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(
Expand Down Expand Up @@ -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();
Expand Down
37 changes: 25 additions & 12 deletions crates/edgezero-adapter-spin/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1115,7 +1116,7 @@ fn find_spin_manifest(start: &Path) -> Result<PathBuf, String> {
}

let root = find_workspace_root(start);
let mut candidates: Vec<PathBuf> = WalkDir::new(&root)
let candidates: Vec<PathBuf> = WalkDir::new(&root)
.follow_links(true)
.max_depth(8)
.into_iter()
Expand All @@ -1129,16 +1130,7 @@ fn find_spin_manifest(start: &Path) -> Result<PathBuf, String> {
})
.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(
Expand Down Expand Up @@ -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();
Expand Down
79 changes: 79 additions & 0 deletions crates/edgezero-adapter/src/cli_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
manifest_name: &str,
) -> Result<PathBuf, String> {
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::<Vec<_>>()
.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.
///
Expand Down Expand Up @@ -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();
Expand Down
43 changes: 41 additions & 2 deletions docs/guide/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,18 @@ edgezero build --adapter axum

The command executes the `build` command from `[adapters.<name>.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 <name> -- <provider-build-args...>
```

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:
Expand Down Expand Up @@ -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 <name> -- <provider-deploy-args...>
```

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`
Expand All @@ -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.<name>.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.<name>.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 `<name>.toml` app
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading