syncable_cli/agent/tools/platform/
trigger_deployment.rs1use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
11use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest};
12
13#[derive(Debug, Deserialize)]
15pub struct TriggerDeploymentArgs {
16 pub project_id: String,
18 pub config_id: String,
20 pub commit_sha: Option<String>,
22}
23
24#[derive(Debug, thiserror::Error)]
26#[error("Trigger deployment error: {0}")]
27pub struct TriggerDeploymentError(String);
28
29#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34pub struct TriggerDeploymentTool;
35
36impl TriggerDeploymentTool {
37 pub fn new() -> Self {
39 Self
40 }
41}
42
43impl Tool for TriggerDeploymentTool {
44 const NAME: &'static str = "trigger_deployment";
45
46 type Error = TriggerDeploymentError;
47 type Args = TriggerDeploymentArgs;
48 type Output = String;
49
50 async fn definition(&self, _prompt: String) -> ToolDefinition {
51 ToolDefinition {
52 name: Self::NAME.to_string(),
53 description: r#"Trigger a deployment using a deployment configuration.
54
55Starts a new deployment for the specified config. Returns a task ID that can be
56used to monitor deployment progress with `get_deployment_status`.
57
58**Parameters:**
59- project_id: The project UUID
60- config_id: The deployment config ID (get from list_deployment_configs)
61- commit_sha: Optional specific commit to deploy (defaults to latest on branch)
62
63**Prerequisites:**
64- User must be authenticated via `sync-ctl auth login`
65- A deployment config must exist for the project
66
67**Use Cases:**
68- Deploy the latest code from a branch
69- Deploy a specific commit version
70- Trigger a manual deployment for a service
71
72**Returns:**
73- task_id: Use this to check deployment progress with get_deployment_status
74- status: Initial deployment status
75- message: Human-readable status message"#
76 .to_string(),
77 parameters: json!({
78 "type": "object",
79 "properties": {
80 "project_id": {
81 "type": "string",
82 "description": "The UUID of the project"
83 },
84 "config_id": {
85 "type": "string",
86 "description": "The deployment config ID (from list_deployment_configs)"
87 },
88 "commit_sha": {
89 "type": "string",
90 "description": "Optional: specific commit SHA to deploy (defaults to latest)"
91 }
92 },
93 "required": ["project_id", "config_id"]
94 }),
95 }
96 }
97
98 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
99 if args.project_id.trim().is_empty() {
101 return Ok(format_error_for_llm(
102 "trigger_deployment",
103 ErrorCategory::ValidationFailed,
104 "project_id cannot be empty",
105 Some(vec![
106 "Use list_projects to find valid project IDs",
107 "Use select_project to set the current project context",
108 ]),
109 ));
110 }
111
112 if args.config_id.trim().is_empty() {
114 return Ok(format_error_for_llm(
115 "trigger_deployment",
116 ErrorCategory::ValidationFailed,
117 "config_id cannot be empty",
118 Some(vec![
119 "Use list_deployment_configs to find available deployment configs",
120 ]),
121 ));
122 }
123
124 let client = match PlatformApiClient::new() {
126 Ok(c) => c,
127 Err(e) => {
128 return Ok(format_api_error("trigger_deployment", e));
129 }
130 };
131
132 let request = TriggerDeploymentRequest {
134 project_id: args.project_id.clone(),
135 config_id: args.config_id.clone(),
136 commit_sha: args.commit_sha.clone(),
137 };
138
139 match client.trigger_deployment(&request).await {
141 Ok(response) => {
142 let result = json!({
143 "success": true,
144 "task_id": response.backstage_task_id,
145 "config_id": response.config_id,
146 "status": response.status,
147 "message": response.message,
148 "next_steps": [
149 format!("Use get_deployment_status with task_id '{}' to monitor progress", response.backstage_task_id),
150 "Deployment typically takes 2-5 minutes to complete"
151 ]
152 });
153
154 serde_json::to_string_pretty(&result)
155 .map_err(|e| TriggerDeploymentError(format!("Failed to serialize: {}", e)))
156 }
157 Err(e) => Ok(format_api_error("trigger_deployment", e)),
158 }
159 }
160}
161
162fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
164 match error {
165 PlatformApiError::Unauthorized => format_error_for_llm(
166 tool_name,
167 ErrorCategory::PermissionDenied,
168 "Not authenticated - please run `sync-ctl auth login` first",
169 Some(vec![
170 "The user needs to authenticate with the Syncable platform",
171 "Run: sync-ctl auth login",
172 ]),
173 ),
174 PlatformApiError::NotFound(msg) => format_error_for_llm(
175 tool_name,
176 ErrorCategory::ResourceUnavailable,
177 &format!("Resource not found: {}", msg),
178 Some(vec![
179 "The project ID or config ID may be incorrect",
180 "Use list_deployment_configs to find valid config IDs",
181 ]),
182 ),
183 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
184 tool_name,
185 ErrorCategory::PermissionDenied,
186 &format!("Permission denied: {}", msg),
187 Some(vec![
188 "The user does not have permission to trigger deployments",
189 "Contact the project admin for access",
190 ]),
191 ),
192 PlatformApiError::RateLimited => format_error_for_llm(
193 tool_name,
194 ErrorCategory::ResourceUnavailable,
195 "Rate limit exceeded - please try again later",
196 Some(vec!["Wait a moment before retrying"]),
197 ),
198 PlatformApiError::HttpError(e) => format_error_for_llm(
199 tool_name,
200 ErrorCategory::NetworkError,
201 &format!("Network error: {}", e),
202 Some(vec![
203 "Check network connectivity",
204 "The Syncable API may be temporarily unavailable",
205 ]),
206 ),
207 PlatformApiError::ParseError(msg) => format_error_for_llm(
208 tool_name,
209 ErrorCategory::InternalError,
210 &format!("Failed to parse API response: {}", msg),
211 Some(vec!["This may be a temporary API issue"]),
212 ),
213 PlatformApiError::ApiError { status, message } => format_error_for_llm(
214 tool_name,
215 ErrorCategory::ExternalCommandFailed,
216 &format!("API error ({}): {}", status, message),
217 Some(vec!["Check the error message for details"]),
218 ),
219 PlatformApiError::ServerError { status, message } => format_error_for_llm(
220 tool_name,
221 ErrorCategory::ExternalCommandFailed,
222 &format!("Server error ({}): {}", status, message),
223 Some(vec![
224 "The Syncable API is experiencing issues",
225 "Try again later",
226 ]),
227 ),
228 PlatformApiError::ConnectionFailed => format_error_for_llm(
229 tool_name,
230 ErrorCategory::NetworkError,
231 "Could not connect to Syncable API",
232 Some(vec![
233 "Check your internet connection",
234 "The Syncable API may be temporarily unavailable",
235 ]),
236 ),
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_tool_name() {
246 assert_eq!(TriggerDeploymentTool::NAME, "trigger_deployment");
247 }
248
249 #[test]
250 fn test_tool_creation() {
251 let tool = TriggerDeploymentTool::new();
252 assert!(format!("{:?}", tool).contains("TriggerDeploymentTool"));
253 }
254}