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