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