Skip to main content

systemprompt_sync/deploy/
artifacts.rs

1//! Pre-deploy validation of the project's build artifacts.
2//!
3//! [`DeployArtifacts`] locates the release binary and profile Dockerfile,
4//! then asserts that required extension assets, storage, and template
5//! directories exist inside the Docker build context before a cloud deploy
6//! proceeds.
7
8use std::path::{Path, PathBuf};
9
10use systemprompt_cloud::ProjectContext;
11use systemprompt_extension::{AssetPaths, ExtensionRegistry};
12use systemprompt_models::paths::constants::build;
13
14use crate::error::{SyncError, SyncResult};
15
16struct ProjectAssetPaths {
17    storage_files: PathBuf,
18    web_dist: PathBuf,
19}
20
21impl AssetPaths for ProjectAssetPaths {
22    fn storage_files(&self) -> &Path {
23        &self.storage_files
24    }
25    fn web_dist(&self) -> &Path {
26        &self.web_dist
27    }
28}
29
30#[derive(Debug)]
31pub struct DeployArtifacts {
32    pub binary: PathBuf,
33    pub dockerfile: PathBuf,
34    project_root: PathBuf,
35}
36
37impl DeployArtifacts {
38    pub fn resolve(project_root: &Path, profile_name: &str) -> SyncResult<Self> {
39        let binary = project_root
40            .join(build::CARGO_TARGET)
41            .join("release")
42            .join(build::BINARY_NAME);
43
44        let ctx = ProjectContext::new(project_root.to_path_buf());
45        let dockerfile = ctx.profile_dockerfile(profile_name);
46
47        let artifacts = Self {
48            binary,
49            dockerfile,
50            project_root: project_root.to_path_buf(),
51        };
52        artifacts.validate()?;
53        Ok(artifacts)
54    }
55
56    fn validate(&self) -> SyncResult<()> {
57        if !self.binary.exists() {
58            return Err(SyncError::BuildArtifacts(format!(
59                "Release binary not found: {}\n\nRun: cargo build --release --bin systemprompt",
60                self.binary.display()
61            )));
62        }
63
64        self.validate_extension_assets()?;
65        self.validate_storage_directory()?;
66        self.validate_templates_directory()?;
67
68        if !self.dockerfile.exists() {
69            return Err(SyncError::BuildArtifacts(format!(
70                "Dockerfile not found: {}\n\nCreate a Dockerfile at this location",
71                self.dockerfile.display()
72            )));
73        }
74
75        Ok(())
76    }
77
78    fn validate_extension_assets(&self) -> SyncResult<()> {
79        let paths = ProjectAssetPaths {
80            storage_files: self.project_root.join("storage/files"),
81            web_dist: self.project_root.join("web/dist"),
82        };
83        let registry = ExtensionRegistry::discover()?;
84        let mut missing = Vec::new();
85        let mut outside_context = Vec::new();
86
87        for ext in registry.asset_extensions() {
88            let ext_id = ext.id();
89            for asset in ext.required_assets(&paths) {
90                if !asset.is_required() {
91                    continue;
92                }
93
94                let source = asset.source();
95
96                if !source.exists() {
97                    missing.push(format!("[ext:{}] {}", ext_id, source.display()));
98                    continue;
99                }
100
101                if !source.starts_with(&self.project_root) {
102                    outside_context.push(format!(
103                        "[ext:{}] {} (not under {})",
104                        ext_id,
105                        source.display(),
106                        self.project_root.display()
107                    ));
108                }
109            }
110        }
111
112        if !missing.is_empty() {
113            return Err(SyncError::BuildArtifacts(format!(
114                "Missing required extension assets:\n  {}\n\nCreate these files or mark them as \
115                 optional.",
116                missing.join("\n  ")
117            )));
118        }
119
120        if !outside_context.is_empty() {
121            return Err(SyncError::BuildArtifacts(format!(
122                "Extension assets outside Docker build context:\n  {}\n\nMove assets inside the \
123                 project directory.",
124                outside_context.join("\n  ")
125            )));
126        }
127
128        Ok(())
129    }
130
131    fn validate_storage_directory(&self) -> SyncResult<()> {
132        let storage_dir = self.project_root.join("storage");
133
134        if !storage_dir.exists() {
135            return Err(SyncError::BuildArtifacts(format!(
136                "Storage directory not found: {}\n\nExpected: storage/\n\nCreate this directory \
137                 for files, images, and other assets.",
138                storage_dir.display()
139            )));
140        }
141
142        let files_dir = storage_dir.join("files");
143        if !files_dir.exists() {
144            return Err(SyncError::BuildArtifacts(format!(
145                "Storage files directory not found: {}\n\nExpected: storage/files/\n\nThis \
146                 directory is required for serving static assets.",
147                files_dir.display()
148            )));
149        }
150
151        Ok(())
152    }
153
154    fn validate_templates_directory(&self) -> SyncResult<()> {
155        let templates_dir = self.project_root.join("services/web/templates");
156
157        if !templates_dir.exists() {
158            return Err(SyncError::BuildArtifacts(format!(
159                "Templates directory not found: {}\n\nExpected: \
160                 services/web/templates/\n\nCreate this directory with your HTML templates.",
161                templates_dir.display()
162            )));
163        }
164
165        Ok(())
166    }
167}