smbcloud_cli/deploy/
mod.rs

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