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