systemprompt_sync/
crate_deploy.rs1use 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}