smbcloud_cli/deploy/
mod.rs

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