@smbcloud / cli / commits / e22401e

Handle AstroJS rsync

keypair34 committed Aug 14, 2026 at 02:10 UTC e22401e9c83296894a3e23048400436cbeb8876c
5 files changed +58 -11
crates/cli/src/cloud-deploy/mod.rs
+38 -6
@@ -27,7 +27,7 @@ pub(crate) use smbcloud_deploy::known_hosts;
27 /// Build the rsync transport for the current project.
28 ///
29 /// This resolves the front-end-specific bits the engine deliberately doesn't
30 -/// know about: the server host (from the runner), the remote path (from config,
30 +/// know about: the server host (from the runner or an approved override), the remote path (from config,
31 /// defaulting to `apps/web/<name>`), and the user's local SSH identity file.
32 pub(crate) fn rsync_transport(
33 config: &Config,
@@ -42,9 +42,41 @@ pub(crate) fn rsync_transport(
42 None => format!("apps/web/{}", config.project.name),
43 };
44
45 - Ok(RsyncTransport::new(
46 - runner.rsync_host(),
47 - remote_path,
48 - identity_file,
49 - ))
45 + let rsync_host = resolve_rsync_host(config.project.rsync_host.as_deref(), runner)?;
46 +
47 + Ok(RsyncTransport::new(rsync_host, remote_path, identity_file))
48 +}
49 +
50 +pub(crate) fn resolve_rsync_host(configured_host: Option<&str>, runner: &Runner) -> Result<String> {
51 + let host = configured_host
52 + .map(str::to_owned)
53 + .unwrap_or_else(|| runner.rsync_host());
54 +
55 + if !known_hosts::is_pinned_host(&host) {
56 + return Err(anyhow!(
57 + "Unsupported rsync host '{host}'. Choose a smbCloud host with a pinned SSH key."
58 + ));
59 + }
60 +
61 + Ok(host)
62 +}
63 +
64 +#[cfg(test)]
65 +mod tests {
66 + use super::*;
67 +
68 + #[test]
69 + fn configured_rsync_host_overrides_runner_default() {
70 + let host = resolve_rsync_host(Some("api-1.smbcloud.xyz"), &Runner::NodeJs)
71 + .expect("pinned host should be accepted");
72 +
73 + assert_eq!(host, "api-1.smbcloud.xyz");
74 + }
75 +
76 + #[test]
77 + fn unpinned_rsync_host_is_rejected() {
78 + let result = resolve_rsync_host(Some("example.com"), &Runner::NodeJs);
79 +
80 + assert!(result.is_err());
81 + }
82 }
crates/cli/src/cloud-deploy/process_deploy_vite_spa.rs
+3 -5
@@ -62,13 +62,11 @@ pub async fn process_deploy_vite_spa(env: Environment, config: Config) -> Result
62 .await
63 .ok();
64
65 - // ── Step 3: rsync <project_path>/<output_dir>/ to api.smbcloud.xyz ───────
65 + // ── Step 3: rsync <project_path>/<output_dir>/ to the configured host ───
66 //
67 // config.project.path holds the remote destination on the server
68 - // (e.g. "apps/web/myapp"). rsync_deploy appends a
69 - // trailing slash and targets git@api.smbcloud.xyz:<path>/ using the
70 - // pinned known-hosts and the user's smbCloud SSH key — exactly the same
71 - // transport used for static site deployments.
68 + // (e.g. "apps/web/myapp"). The transport appends a trailing slash and
69 + // uses the selected host's pinned SSH key and the user's smbCloud identity.
70
71 let runner = config.project.runner;
72 let transport = crate::deploy::rsync_transport(&config, &runner, user.id)?;
crates/cli/src/cloud-deploy/process_migrate.rs
+1
@@ -188,6 +188,7 @@ fn strip_project(project: &Project) -> Project {
188 path: None,
189 repository: None,
190 deploy_repo_id: None,
191 + rsync_host: None,
192 source_path: None,
193 created_at: project.created_at,
194 updated_at: project.updated_at,
crates/smbcloud-deploy/src/known_hosts.rs
+12
@@ -49,6 +49,11 @@ pub fn for_host(rsync_host: &str) -> &'static str {
49 }
50 }
51
52 +/// Returns whether `host` has a host key pinned by this crate.
53 +pub fn is_pinned_host(host: &str) -> bool {
54 + matches!(host, "api.smbcloud.xyz" | "api-1.smbcloud.xyz")
55 +}
56 +
57 #[cfg(test)]
58 mod tests {
59 use super::*;
@@ -77,4 +82,11 @@ mod tests {
82 assert!(API_SMBCLOUD_XYZ.contains("AAAA"));
83 assert!(API_1_SMBCLOUD_XYZ.contains("AAAA"));
84 }
85 +
86 + #[test]
87 + fn rejects_unpinned_hosts() {
88 + assert!(is_pinned_host("api.smbcloud.xyz"));
89 + assert!(is_pinned_host("api-1.smbcloud.xyz"));
90 + assert!(!is_pinned_host("example.com"));
91 + }
92 }
crates/smbcloud-model/src/project.rs
+4
@@ -75,6 +75,10 @@ pub struct Project {
75 /// Repo ID backing this deploy target. Optional until the API exposes it
76 /// consistently to the CLI.
77 pub deploy_repo_id: Option<i64>,
78 + /// Approved rsync host override for deployments that do not use the
79 + /// runner's default static tier.
80 + #[serde(default)]
81 + pub rsync_host: Option<String>,
82 /// Repo-relative app path for monorepo targets, e.g. "apps/web/console".
83 pub source_path: Option<String>,
84 #[serde(default = "default_datetime")]