@smbcloud / cli / commits / 842c0ff

use crate model runner

Seto committed Sep 20, 2025 at 17:57 UTC 842c0ff37c5fd5b62d0b80e1a1a34fc2b6926784
7 files changed +294 -102
crates/cli/src/deploy/detect_runner.rs new
+70
@@ -0,0 +1,70 @@
1 +use {
2 + crate::ui::{fail_message, fail_symbol, succeed_message, succeed_symbol},
3 + anyhow::Result,
4 + smbcloud_model::runner::Runner,
5 + spinners::Spinner,
6 + std::{env::current_dir, path::Path},
7 +};
8 +
9 +pub(crate) async fn detect_runner() -> Result<Runner> {
10 + let mut spinner: Spinner = Spinner::new(
11 + spinners::Spinners::SimpleDotsScrolling,
12 + succeed_message("Checking runner"),
13 + );
14 +
15 + let path = match current_dir() {
16 + Ok(path) => path,
17 + Err(_) => {
18 + spinner.stop_and_persist(
19 + &fail_symbol(),
20 + fail_message(
21 + "Could not detect project runner: no package.json, Gemfile, or Package.swift found",
22 + ),
23 + );
24 + anyhow::bail!(
25 + "Could not detect project runner: no package.json, Gemfile, or Package.swift found"
26 + );
27 + }
28 + };
29 + let runner = match Runner::from(&path) {
30 + Ok(runner) => runner,
31 + Err(_) => {
32 + spinner.stop_and_persist(
33 + &fail_symbol(),
34 + fail_message(
35 + "Could not detect project runner: no package.json, Gemfile, or Package.swift found",
36 + ),
37 + );
38 + anyhow::bail!(
39 + "Could not detect project runner: no package.json, Gemfile, or Package.swift found"
40 + );
41 + }
42 + };
43 +
44 + match runner {
45 + Runner::NodeJs => {
46 + spinner.stop_and_persist(
47 + &succeed_symbol(),
48 + succeed_message("NodeJs 🟩 runner detected"),
49 + );
50 + }
51 + Runner::Ruby => {
52 + if Path::new("Gemfile").exists() {
53 + spinner.stop_and_persist(
54 + &succeed_symbol(),
55 + succeed_message("Ruby 🟥 runner with Rails app detected"),
56 + );
57 + }
58 + }
59 + Runner::Swift => {
60 + if Path::new("Package.swift").exists() {
61 + spinner.stop_and_persist(
62 + &succeed_symbol(),
63 + succeed_message("Swift 🟧 runner with Vapor app detected"),
64 + );
65 + }
66 + }
67 + };
68 +
69 + Ok(runner)
70 +}
crates/cli/src/deploy/git.rs
+1 -1
@@ -1,9 +1,9 @@
1 use {
2 - crate::project::runner::Runner,
2 crate::ui::{fail_message, fail_symbol, succeed_message, succeed_symbol},
3 anyhow::{anyhow, Result},
4 console::style,
5 git2::{Remote, Repository},
6 + smbcloud_model::runner::Runner,
7 spinners::Spinner,
8 };
9
crates/cli/src/deploy/mod.rs
+2 -1
@@ -1,14 +1,15 @@
1 pub mod config;
2 +mod detect_runner;
3 mod git;
4 mod remote_messages;
5 mod setup;
6
7 +use crate::deploy::detect_runner::detect_runner;
8 use crate::token::get_smb_token;
9 use crate::{
10 account::{lib::is_logged_in, login::process_login},
11 cli::CommandResult,
12 deploy::config::check_project,
11 - project::runner::detect_runner,
13 ui::{fail_message, succeed_message, succeed_symbol},
14 };
15 use anyhow::{anyhow, Result};
crates/cli/src/deploy/process_deploy.rs new
+210
@@ -0,0 +1,210 @@
1 +use crate::deploy::{
2 + account::{lib::is_logged_in, login::process_login},
3 + cli::CommandResult,
4 + config::check_config,
5 + deploy::config::check_project,
6 + detect_runner::detect_runner,
7 + git::remote_deployment_setup,
8 + remote_messages::{build_next_app, start_server},
9 + token::get_smb_token,
10 + ui::{fail_message, succeed_message, succeed_symbol},
11 +};
12 +use anyhow::{anyhow, Result};
13 +use git2::{PushOptions, RemoteCallbacks, Repository};
14 +use smbcloud_model::project::{DeploymentPayload, DeploymentStatus};
15 +use smbcloud_network::environment::Environment;
16 +use smbcloud_networking_account::me::me;
17 +use smbcloud_networking_project::{
18 + crud_project_deployment_create::create_deployment, crud_project_deployment_update::update,
19 +};
20 +use spinners::Spinner;
21 +use std::sync::atomic::AtomicBool;
22 +use std::sync::atomic::Ordering;
23 +use std::sync::Arc;
24 +
25 +pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
26 + // Check credentials.
27 + if !is_logged_in(env) {
28 + let _ = process_login(env).await;
29 + }
30 +
31 + // Get current token
32 + let access_token = get_smb_token(env).await?;
33 +
34 + // Check config.
35 + let config = check_config(env).await?;
36 +
37 + // Check runner.
38 + let runner = detect_runner().await?;
39 +
40 + // Validate config with project.
41 + check_project(env, &access_token, config.project.id).await?;
42 +
43 + // Check remote repository setup.
44 + let repo = match Repository::open(".") {
45 + Ok(repo) => repo,
46 + Err(_) => {
47 + return Err(anyhow!(fail_message(
48 + "No git repository found. Init with `git init` command."
49 + )))
50 + }
51 + };
52 +
53 + // Get the current branch
54 + let head = match repo.head() {
55 + Ok(head) => head,
56 + Err(_) => {
57 + return Err(anyhow!(fail_message(
58 + "No HEAD reference found. Create a commit with `git commit` command."
59 + )))
60 + }
61 + };
62 +
63 + // Check if we're on the main branch
64 + let branch_name = match head.shorthand() {
65 + Some(name) => name,
66 + None => {
67 + return Err(anyhow!(fail_message(
68 + "Unable to determine current branch name."
69 + )))
70 + }
71 + };
72 +
73 + if branch_name != "main" && branch_name != "master" {
74 + return Err(anyhow!(fail_message(
75 + &format!("Not on main branch. Current branch: '{}'. Switch to main branch with `git checkout main` command.", branch_name)
76 + )));
77 + }
78 +
79 + let main_branch = head;
80 +
81 + let repository = match &config.project.repository {
82 + Some(repo) => repo,
83 + None => return Err(anyhow!(fail_message("Repository not found."))),
84 + };
85 +
86 + let mut origin = remote_deployment_setup(&runner, &repo, repository).await?;
87 +
88 + let commit_hash = match main_branch.resolve() {
89 + Ok(result) => match result.target() {
90 + Some(hash_id) => hash_id,
91 + None => return Err(anyhow!("Should have at least one commit.")),
92 + },
93 + Err(_) => return Err(anyhow!("Cannot resolve main branch.")),
94 + };
95 + let payload = DeploymentPayload {
96 + commit_hash: commit_hash.to_string(),
97 + status: DeploymentStatus::Started,
98 + };
99 +
100 + let created_deployment =
101 + create_deployment(env, &access_token, config.project.id, payload).await?;
102 + let user = me(env, &access_token).await?;
103 +
104 + let mut push_opts = PushOptions::new();
105 + let mut callbacks = RemoteCallbacks::new();
106 +
107 + // For updating status to failed
108 + let deployment_failed_flag = Arc::new(AtomicBool::new(false));
109 + let update_env = env; // Env is Copy
110 + let update_access_token = access_token.clone();
111 + let update_project_id = config.project.id;
112 + let update_deployment_id = created_deployment.id;
113 +
114 + // Set the credentials
115 + callbacks.credentials(config.credentials(user));
116 + callbacks.sideband_progress(|data| {
117 + // Convert bytes to string, print line by line
118 + if let Ok(text) = std::str::from_utf8(data) {
119 + for line in text.lines() {
120 + if line.contains(&build_next_app()) {
121 + println!("Building the app {}", succeed_symbol());
122 + }
123 + if line.contains(&start_server(repository)) {
124 + println!("App restart {}", succeed_symbol());
125 + }
126 + }
127 + }
128 + true // continue receiving.
129 + });
130 + callbacks.push_update_reference({
131 + let flag_clone = deployment_failed_flag.clone();
132 + let access_token_for_update_cb = update_access_token.clone();
133 + let project_id_for_update_cb = update_project_id;
134 + let deployment_id_for_update_cb = update_deployment_id;
135 +
136 + move |_refname, status_message| {
137 + if let Some(e) = status_message {
138 + // Try to set the flag. If it was already true, do nothing.
139 + if !flag_clone.swap(true, Ordering::SeqCst) {
140 + println!(
141 + "Deployment ref update failed: {}. Marking deployment as Failed.",
142 + e
143 + );
144 +
145 + let update_payload = DeploymentPayload {
146 + commit_hash: commit_hash.to_string(),
147 + status: DeploymentStatus::Failed,
148 + };
149 +
150 + // We are in a sync callback, so we need to block on the async task.
151 + let handle = tokio::runtime::Handle::current();
152 + let result = handle.block_on(async {
153 + update(
154 + update_env, // Env is Copy
155 + access_token_for_update_cb.clone(),
156 + project_id_for_update_cb,
157 + deployment_id_for_update_cb,
158 + update_payload,
159 + )
160 + .await
161 + });
162 +
163 + match result {
164 + Ok(_) => println!("Deployment status successfully updated to Failed."),
165 + Err(update_err) => {
166 + eprintln!("Error updating deployment status to Failed: {}", update_err)
167 + }
168 + }
169 + }
170 + }
171 + Ok(()) // Report success for the git callback itself, error is handled above.
172 + }
173 + });
174 + push_opts.remote_callbacks(callbacks);
175 +
176 + let spinner = Spinner::new(
177 + spinners::Spinners::Hamburger,
178 + succeed_message("Deploying > "),
179 + );
180 +
181 + match origin.push(&["refs/heads/main:refs/heads/main"], Some(&mut push_opts)) {
182 + Ok(_) => {
183 + // Update deployment status to Done
184 + let update_payload = DeploymentPayload {
185 + commit_hash: commit_hash.to_string(),
186 + status: DeploymentStatus::Done,
187 + };
188 + let result = update(
189 + env,
190 + access_token.clone(),
191 + config.project.id,
192 + created_deployment.id,
193 + update_payload,
194 + )
195 + .await;
196 + match result {
197 + Ok(_) => println!("App is running {}", succeed_symbol()),
198 + Err(update_err) => {
199 + eprintln!("Error updating deployment status to Done: {}", update_err)
200 + }
201 + }
202 + Ok(CommandResult {
203 + spinner,
204 + symbol: succeed_symbol(),
205 + msg: succeed_message("Deployment complete."),
206 + })
207 + }
208 + Err(e) => Err(anyhow!(fail_message(&e.to_string()))),
209 + }
210 +}
crates/cli/src/project/mod.rs
-1
@@ -5,4 +5,3 @@ pub mod crud_read;
5 pub mod crud_update;
6 mod deployment;
7 pub mod process;
8 -pub(crate) mod runner;
crates/cli/src/project/runner.rs deleted
-99
@@ -1,99 +0,0 @@
1 -use crate::ui::{fail_message, fail_symbol, succeed_message, succeed_symbol};
2 -use anyhow::Result;
3 -use spinners::Spinner;
4 -use std::fs;
5 -use std::path::Path;
6 -
7 -pub(crate) enum Runner {
8 - NodeJs,
9 - Ruby,
10 - Swift,
11 -}
12 -
13 -impl Runner {
14 - pub fn git_host(&self) -> String {
15 - format!("git@{}.smbcloud.xyz", self.api())
16 - }
17 -
18 - fn api(&self) -> &str {
19 - match self {
20 - Runner::NodeJs => "api",
21 - Runner::Ruby | Runner::Swift => "api-1",
22 - }
23 - }
24 -}
25 -
26 -// Helper function to detect any next.config.* file
27 -fn next_config_exists() -> bool {
28 - if let Ok(entries) = fs::read_dir(".") {
29 - for entry in entries.flatten() {
30 - let filename = entry.file_name();
31 - let filename_str = filename.to_string_lossy();
32 - if filename_str.starts_with("next.config.") {
33 - return true;
34 - }
35 - }
36 - }
37 - false
38 -}
39 -
40 -// Helper function to detect any astro.config.* file
41 -fn astro_config_exists() -> bool {
42 - if let Ok(entries) = fs::read_dir(".") {
43 - for entry in entries.flatten() {
44 - let filename = entry.file_name();
45 - let filename_str = filename.to_string_lossy();
46 - if filename_str.starts_with("astro.config.") {
47 - return true;
48 - }
49 - }
50 - }
51 - false
52 -}
53 -
54 -pub(crate) async fn detect_runner() -> Result<Runner> {
55 - let mut spinner: Spinner = Spinner::new(
56 - spinners::Spinners::SimpleDotsScrolling,
57 - succeed_message("Checking runner"),
58 - );
59 -
60 - if Path::new("package.json").exists() && (next_config_exists() || astro_config_exists()) {
61 - let app_string = if next_config_exists() {
62 - "Next.js app"
63 - } else if astro_config_exists() {
64 - "Astro app"
65 - } else {
66 - panic!("Unsupported app.")
67 - };
68 -
69 - spinner.stop_and_persist(
70 - &succeed_symbol(),
71 - succeed_message(format!("NodeJs 🟩 runner with {} detected", app_string).as_str()),
72 - );
73 - return Ok(Runner::NodeJs);
74 - }
75 - if Path::new("Gemfile").exists() {
76 - spinner.stop_and_persist(
77 - &succeed_symbol(),
78 - succeed_message("Ruby 🟥 runner with Rails app detected"),
79 - );
80 - return Ok(Runner::Ruby);
81 - }
82 - if Path::new("Package.swift").exists() {
83 - spinner.stop_and_persist(
84 - &succeed_symbol(),
85 - succeed_message("Swift 🟧 runner with Vapor app detected"),
86 - );
87 - return Ok(Runner::Swift);
88 - }
89 -
90 - spinner.stop_and_persist(
91 - &fail_symbol(),
92 - fail_message(
93 - "Could not detect project runner: no package.json, Gemfile, or Package.swift found",
94 - ),
95 - );
96 - anyhow::bail!(
97 - "Could not detect project runner: no package.json, Gemfile, or Package.swift found"
98 - );
99 -}
crates/smbcloud-model/src/runner.rs
+11
@@ -69,6 +69,17 @@ impl Runner {
69 message: UnsupportedRunner.message(None).to_string(),
70 });
71 }
72 +
73 + pub fn git_host(&self) -> String {
74 + format!("git@{}.smbcloud.xyz", self.api())
75 + }
76 +
77 + fn api(&self) -> &str {
78 + match self {
79 + Runner::NodeJs => "api",
80 + Runner::Ruby | Runner::Swift => "api-1",
81 + }
82 + }
83 }
84
85 // Helper function to detect any next.config.* file