Skip to content

Commit 5039ce6

Browse files
committed
Merge branch 'feature/decouple-build' into development
2 parents e5d6b59 + c918dc9 commit 5039ce6

35 files changed

Lines changed: 667 additions & 332 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Plan: decouple build/deploy from the CLI into `smbcloud-deploy`
2+
3+
Status: in progress. Stages 0 to 3 (vite_spa slice) shipped. Remaining work below
4+
is the **next milestone**.
5+
6+
## Goal
7+
8+
Move the deploy logic out of `crates/cli` into a reusable `crates/smbcloud-deploy`
9+
engine so the CLI, the CI action, and (later) the server-side git receiver all
10+
drive the same code. They differ only in how they report progress and how they
11+
authenticate.
12+
13+
Two inversions make it reusable:
14+
- `Reporter` replaces direct `spinners` / `dialoguer` / `println!`, so the engine
15+
never owns the terminal. The CLI supplies `SpinnerReporter`.
16+
- Auth is passed in (a token / credentials); the engine never reads `~/.smb` or
17+
prompts for login.
18+
19+
Decisions already made:
20+
- Networking crates (`smbcloud-networking*`) stay direct deps of the engine.
21+
Only reporting and auth are inverted.
22+
- `git.rs` (SSH-git remote setup) stays in the CLI for now. It is the SSH-git
23+
path slated for replacement by git-smart-HTTP, so decoupling it is wasted work.
24+
- Interactive setup (`setup*.rs`, `process_migrate.rs`) stays in the CLI. It is
25+
UX, not engine.
26+
27+
## Done
28+
29+
- **Stage 0** engine crate: `error::DeployError`, `report::{Reporter, NoopReporter}`,
30+
`runner::detect_runner`.
31+
- **Stage 1** CLI depends on the engine; `ui/reporter.rs::SpinnerReporter`;
32+
`detect_runner` rewired; old `deploy/detect_runner.rs` removed.
33+
- **Stage 2** transport: `transport::{Transport, RsyncTransport}` + moved
34+
`known_hosts`. CLI helper `deploy::rsync_transport(config, runner, user_id)`
35+
resolves the local `~/.ssh` identity and remote path.
36+
- **Stage 3 (vite_spa slice)** build split: `build::{BuildStrategy, BuildArtifact,
37+
ViteSpaBuild}`. `process_deploy_vite_spa.rs` now calls the engine for the build
38+
and reuses one `SpinnerReporter` for build + transport.
39+
40+
## Next milestone
41+
42+
### 1. Shared remote-command step
43+
44+
Several strategies do the same "SSH in and restart the service" step, inlined
45+
per file. Extract it into the engine first (e.g. `transport::RemoteCommand` or a
46+
`remote` module) so the strategies can share it. Model it behind the same pinned
47+
host-key SSH used by `RsyncTransport`.
48+
49+
### 2. Remaining `BuildStrategy` extractions (one per pass, compile-green each)
50+
51+
Do smallest first. Each: lift build mechanics into a `BuildStrategy`, swap
52+
`spinners` + `stop_and_persist` for `Reporter`, take auth as a param, route the
53+
remote-restart step through step 1.
54+
55+
| File | Lines | Shape |
56+
| --- | --- | --- |
57+
| `process_deploy_rails.rs` | ~395 | rsync shared lib, SSH compile native gem, git force-push |
58+
| `process_deploy_rust.rs` | ~641 | cross-build Linux binary, rsync, SSH restart |
59+
| `process_deploy_nextjs_ssr.rs` | ~700 | install, build, upload `.next/standalone`, SSH restart |
60+
| `process_deploy_swift.rs` | ~832 | Docker Linux build, rsync binary + Resources, SSH restart |
61+
62+
Follow `ViteSpaBuild` as the template. Keep the deployment-record API calls
63+
(`create_deployment` / `update`) in the CLI router, not the engine.
64+
65+
### 3. Stage 4: router + retire the CommandResult wart
66+
67+
- Flip `process_deploy.rs`'s router to call an `engine::deploy(...)` orchestrator.
68+
- Retire `CommandResult`'s `spinner: Spinner` field (the "return a live spinner,
69+
caller stops it" pattern) now that the engine reports the whole flow through
70+
`Reporter`. `CommandResult` has ~50 construction sites, so this is its own pass:
71+
either make `spinner` optional or drop it and have commands return plain data.
72+
73+
## Reference
74+
75+
- Spec: `smbcloud` repo `.agents/specs/smbcloud-deploy.md` (product direction),
76+
`smbcloud-actions.md` (CI wrapper). Direction is git-smart-HTTP, local-first
77+
build, drop SSH transport later.
78+
- Engine crate: `crates/smbcloud-deploy/`.

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ serde_json = "1.0.82"
2828
serde_repr = "0.1"
2929
smbcloud-auth = { version = "0.4.7", path = "crates/smbcloud-auth" }
3030
smbcloud-auth-sdk = { version = "0.4.7", path = "crates/smbcloud-auth-sdk" }
31+
smbcloud-deploy = { version = "0.4.7", path = "crates/smbcloud-deploy" }
3132
smbcloud-email-sdk = { version = "0.4.7", path = "crates/smbcloud-email-sdk" }
3233
smbcloud-gresiq-sdk = { version = "0.4.7", path = "crates/smbcloud-gresiq-sdk" }
3334
smbcloud-mail = { version = "0.4.7", path = "crates/smbcloud-mail" }

README.MD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@ For the broader product, see [smbCloud](https://smbcloud.xyz/) and the [deployme
118118

119119
## Copyright
120120

121-
© 2026 [smbCloud](https://smbcloud.xyz) (Splitfire AB).
121+
© 2026 [Splitfire AB](https://5mb.app) ([smbCloud](https://smbcloud.xyz)).

crates/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ serde = { workspace = true, features = ["derive"] }
4444
serde_json = { workspace = true }
4545
serde_repr = { workspace = true }
4646
smbcloud-auth = { workspace = true }
47+
smbcloud-deploy = { workspace = true }
4748
smbcloud-mail = { workspace = true }
4849
smbcloud-model = { workspace = true }
4950
smbcloud-network = { workspace = true }

crates/cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,4 @@ More CLI and deployment guides live in the [smbCloud docs](https://smbcloud.xyz/
9999

100100
## Copyright
101101

102-
© 2026 [smbCloud](https://smbcloud.xyz) (Splitfire AB).
102+
© 2026 [Splitfire AB](https://5mb.app) ([smbCloud](https://smbcloud.xyz)).

crates/cli/src/deploy/detect_runner.rs

Lines changed: 0 additions & 94 deletions
This file was deleted.

crates/cli/src/deploy/mod.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
pub mod config;
2-
mod detect_runner;
32
mod git;
4-
mod known_hosts;
53
pub mod process_deploy;
64
pub mod process_deploy_nextjs_ssr;
75
pub mod process_deploy_rails;
@@ -10,7 +8,43 @@ pub mod process_deploy_swift;
108
pub mod process_deploy_vite_spa;
119
pub mod process_migrate;
1210
mod remote_messages;
13-
pub(crate) mod rsync_deploy;
1411
pub(crate) mod setup_create_new_project;
1512
pub(crate) mod setup_project;
1613
pub(crate) mod setup_select_project;
14+
15+
use {
16+
anyhow::{anyhow, Result},
17+
smbcloud_deploy::RsyncTransport,
18+
smbcloud_model::runner::Runner,
19+
smbcloud_utils::config::Config,
20+
};
21+
22+
// The pinned host keys live in the engine crate. Re-export them here so the
23+
// strategy modules that build their own pinned-SSH commands can keep using
24+
// `deploy::known_hosts`.
25+
pub(crate) use smbcloud_deploy::known_hosts;
26+
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,
31+
/// defaulting to `apps/web/<name>`), and the user's local SSH identity file.
32+
pub(crate) fn rsync_transport(
33+
config: &Config,
34+
runner: &Runner,
35+
user_id: i32,
36+
) -> Result<RsyncTransport> {
37+
let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
38+
let identity_file = home.join(".ssh").join(format!("id_{user_id}@smbcloud"));
39+
40+
let remote_path = match &config.project.path {
41+
Some(path) => path.clone(),
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+
))
50+
}

crates/cli/src/deploy/process_deploy.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ use {
55
client,
66
deploy::{
77
config::{check_project, credentials, get_config, overlay_server_config},
8-
detect_runner::detect_runner,
98
git::remote_deployment_setup,
109
process_deploy_nextjs_ssr::process_deploy_nextjs_ssr,
1110
process_deploy_rails::process_deploy_rails,
1211
process_deploy_rust::process_deploy_rust,
1312
process_deploy_swift::process_deploy_swift,
1413
process_deploy_vite_spa::process_deploy_vite_spa,
1514
remote_messages::{build_next_app, start_server},
16-
rsync_deploy::rsync_deploy,
1715
},
1816
token::{get_smb_token::get_smb_token, is_logged_in::is_logged_in},
1917
ui::{fail_message, succeed_message, succeed_symbol},
@@ -22,6 +20,7 @@ use {
2220
dialoguer::{console::Term, theme::ColorfulTheme, Select},
2321
git2::{PushOptions, RemoteCallbacks, Repository},
2422
smbcloud_auth::me::me,
23+
smbcloud_deploy::Transport,
2524
smbcloud_model::{
2625
project::{DeploymentMethod, DeploymentPayload, DeploymentStatus},
2726
runner::Runner,
@@ -171,7 +170,25 @@ pub async fn process_deploy(
171170
// detection needed, the source tree may have no package.json/Gemfile/etc.
172171
let runner = config.project.runner;
173172
let user = me(env, client(), &access_token).await?;
174-
rsync_deploy(&config, &runner, user.id, ".")
173+
let transport = crate::deploy::rsync_transport(&config, &runner, user.id)?;
174+
175+
// The engine ships silently; this command owns the spinner and the
176+
// final line it hands back to `main` to persist.
177+
let spinner = Spinner::new(
178+
spinners::Spinners::Hamburger,
179+
succeed_message(&format!("Syncing to {}…", runner.rsync_host())),
180+
);
181+
match transport.ship(std::path::Path::new("."), &smbcloud_deploy::NoopReporter) {
182+
Ok(()) => Ok(CommandResult {
183+
spinner,
184+
symbol: succeed_symbol(),
185+
msg: succeed_message("Deployment complete via rsync."),
186+
}),
187+
Err(e) => {
188+
drop(spinner);
189+
Err(anyhow!(fail_message(&format!("{e}"))))
190+
}
191+
}
175192
}
176193
DeploymentMethod::Git => git_deploy(env, &access_token, config).await,
177194
}
@@ -184,7 +201,8 @@ async fn git_deploy(
184201
) -> Result<CommandResult> {
185202
// Runner detection requires framework files (package.json, Gemfile, etc.) —
186203
// only needed for the git push path where the server builds the project.
187-
let runner = detect_runner(&config).await?;
204+
let reporter = crate::ui::reporter::SpinnerReporter::new();
205+
let runner = smbcloud_deploy::detect_runner(&config, &reporter)?;
188206
// Check remote repository setup.
189207
let repo = match Repository::open(".") {
190208
Ok(repo) => repo,

0 commit comments

Comments
 (0)