Skip to main content

systemprompt_sync/
crate_deploy.rs

1use std::env;
2use std::io::Write;
3use std::path::PathBuf;
4use std::process::Command;
5
6use crate::api_client::SyncApiClient;
7use crate::error::{SyncError, SyncResult};
8use crate::{SyncConfig, SyncOperationResult};
9
10#[derive(Debug)]
11pub struct CrateDeployService {
12    config: SyncConfig,
13    api_client: SyncApiClient,
14}
15
16impl CrateDeployService {
17    pub const fn new(config: SyncConfig, api_client: SyncApiClient) -> Self {
18        Self { config, api_client }
19    }
20
21    pub async fn deploy(
22        &self,
23        skip_build: bool,
24        custom_tag: Option<String>,
25    ) -> SyncResult<SyncOperationResult> {
26        let project_root = Self::get_project_root()?;
27        let app_id = self.get_app_id().await?;
28
29        let tag = if let Some(t) = custom_tag {
30            t
31        } else {
32            let timestamp = chrono::Utc::now().timestamp();
33            let git_sha = Self::get_git_sha()?;
34            format!("deploy-{timestamp}-{git_sha}")
35        };
36
37        let image = format!("registry.fly.io/{app_id}:{tag}");
38
39        if !skip_build {
40            Self::build_release(&project_root)?;
41        }
42
43        Self::build_docker(&project_root, &image)?;
44
45        let token = self
46            .api_client
47            .get_registry_token(&self.config.tenant_id)
48            .await?;
49        Self::docker_login(&token.registry, &token.username, &token.token)?;
50        Self::docker_push(&image)?;
51
52        let response = self
53            .api_client
54            .deploy(&self.config.tenant_id, &image)
55            .await?;
56
57        Ok(
58            SyncOperationResult::success("crate_deploy", 1).with_details(serde_json::json!({
59                "image": image,
60                "status": response.status,
61                "app_url": response.app_url,
62            })),
63        )
64    }
65
66    fn get_project_root() -> SyncResult<PathBuf> {
67        let current = env::current_dir()?;
68        if current.join("infrastructure").exists() {
69            Ok(current)
70        } else {
71            Err(SyncError::NotProjectRoot)
72        }
73    }
74
75    async fn get_app_id(&self) -> SyncResult<String> {
76        self.api_client
77            .get_tenant_app_id(&self.config.tenant_id)
78            .await
79    }
80
81    fn get_git_sha() -> SyncResult<String> {
82        let output = Command::new("git")
83            .args(["rev-parse", "--short", "HEAD"])
84            .output()
85            .map_err(|source| SyncError::CommandSpawnFailed {
86                command: "git rev-parse --short HEAD".into(),
87                source,
88            })?;
89
90        String::from_utf8(output.stdout)
91            .map(|sha| sha.trim().to_string())
92            .map_err(|_| SyncError::GitShaUnavailable)
93    }
94
95    fn build_release(project_root: &PathBuf) -> SyncResult<()> {
96        Self::run_command(
97            "cargo",
98            &[
99                "build",
100                "--release",
101                "--manifest-path=core/Cargo.toml",
102                "--bin",
103                "systemprompt",
104            ],
105            project_root,
106        )
107    }
108
109    fn build_docker(project_root: &PathBuf, image: &str) -> SyncResult<()> {
110        Self::run_command(
111            "docker",
112            &[
113                "build",
114                "-f",
115                "infrastructure/docker/app.Dockerfile",
116                "-t",
117                image,
118                ".",
119            ],
120            project_root,
121        )
122    }
123
124    fn docker_login(registry: &str, username: &str, token: &str) -> SyncResult<()> {
125        let mut command = Command::new("docker");
126        command.args(["login", registry, "-u", username, "--password-stdin"]);
127        command.stdin(std::process::Stdio::piped());
128
129        let mut child = command
130            .spawn()
131            .map_err(|source| SyncError::CommandSpawnFailed {
132                command: format!("docker login {registry}"),
133                source,
134            })?;
135        if let Some(mut stdin) = child.stdin.take() {
136            stdin.write_all(token.as_bytes())?;
137        }
138
139        let status = child.wait()?;
140        if !status.success() {
141            return Err(SyncError::DockerLoginFailed);
142        }
143        Ok(())
144    }
145
146    fn docker_push(image: &str) -> SyncResult<()> {
147        Self::run_command("docker", &["push", image], &env::current_dir()?)
148    }
149
150    fn run_command(cmd: &str, args: &[&str], dir: &PathBuf) -> SyncResult<()> {
151        let command_str = format!("{cmd} {}", args.join(" "));
152        let status = Command::new(cmd)
153            .args(args)
154            .current_dir(dir)
155            .status()
156            .map_err(|source| SyncError::CommandSpawnFailed {
157                command: command_str.clone(),
158                source,
159            })?;
160
161        if !status.success() {
162            return Err(SyncError::CommandFailed {
163                command: command_str,
164            });
165        }
166        Ok(())
167    }
168}