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