1use serde::{Deserialize, Serialize};
5
6pub mod paths {
7 pub const ROOT: &str = "/internal/v1/deploy";
8 pub const APPS: &str = "/internal/v1/deploy/apps";
9 pub const DEPLOYMENTS: &str = "/internal/v1/deploy/deployments";
10 pub const OPERATIONS: &str = "/internal/v1/deploy/operations";
11 pub const ROUTES: &str = "/internal/v1/deploy/routes";
12 pub const ENVIRONMENTS: &str = "/internal/v1/deploy/environments";
13 pub const ARTIFACTS: &str = "/internal/v1/deploy/artifacts";
14 pub const PROMOTIONS: &str = "/internal/v1/deploy/promotions";
15 pub const APP: &str = "/internal/v1/deploy/apps/{appId}";
16 pub const APP_DEPLOYMENTS: &str = "/internal/v1/deploy/apps/{appId}/deployments";
17 pub const DEPLOYMENT: &str = "/internal/v1/deploy/deployments/{deploymentId}";
18 pub const DEPLOYMENT_CANCEL: &str = "/internal/v1/deploy/deployments/{deploymentId}/cancel";
19 pub const DEPLOYMENT_ROLLBACK: &str = "/internal/v1/deploy/deployments/{deploymentId}/rollback";
20 pub const DEPLOYMENT_RESUME: &str = "/internal/v1/deploy/deployments/{deploymentId}/resume";
21 pub const DEPLOYMENT_LOGS: &str = "/internal/v1/deploy/deployments/{deploymentId}/logs";
22 pub const OPERATION: &str = "/internal/v1/deploy/operations/{operationId}";
23 pub const RELEASES: &str = "/internal/v1/deploy/environments/{environmentId}/releases";
24 pub const APP_ENVIRONMENTS: &str = "/internal/v1/deploy/apps/{appId}/environments";
25 pub const ENVIRONMENT: &str = "/internal/v1/deploy/environments/{environmentId}";
26 pub const ARTIFACT: &str = "/internal/v1/deploy/artifacts/{digest}";
27 pub const RELEASE: &str =
28 "/internal/v1/deploy/environments/{environmentId}/releases/{releaseId}";
29 pub const RELEASE_ACTIVATE: &str =
30 "/internal/v1/deploy/environments/{environmentId}/releases/{releaseId}/activate";
31 pub const ROUTE: &str = "/internal/v1/deploy/routes/{appId}/{environment}";
32 pub const PROMOTION: &str = "/internal/v1/deploy/promotions/{promotionId}";
33 pub const PROMOTION_APPROVE: &str = "/internal/v1/deploy/promotions/{promotionId}/approve";
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "camelCase")]
38pub struct Application {
39 pub id: String,
40 pub name: String,
41 pub source_repository: String,
42 pub default_build_spec: BuildSpec,
43 pub runtime_port: Option<u16>,
44 pub version: i64,
45 pub created_at: String,
46 pub updated_at: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "camelCase")]
51pub struct CreateApplicationRequest {
52 pub name: String,
53 pub source_repository: String,
54 pub default_build_spec: BuildSpec,
55 pub runtime_port: Option<u16>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase")]
60pub struct UpdateApplicationRequest {
61 pub name: String,
62 pub source_repository: String,
63 pub default_build_spec: BuildSpec,
64 pub runtime_port: Option<u16>,
65 pub expected_version: i64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "camelCase")]
70pub struct DeleteApplicationRequest {
71 pub expected_version: i64,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(rename_all = "camelCase")]
76pub struct DeleteResult {
77 pub deleted: bool,
78}
79
80pub type ApplicationPage = CursorPage<Application>;
81pub type DeploymentPage = CursorPage<Deployment>;
82pub type ReleasePage = CursorPage<Release>;
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "camelCase")]
86pub struct CreateDeploymentRequest {
87 pub source_revision: SourceRevision,
88 pub environment: String,
89 #[serde(default)]
90 pub strategy: DeploymentStrategy,
91 pub build_spec: Option<BuildSpec>,
92 #[serde(default)]
93 pub labels: std::collections::BTreeMap<String, String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
97#[serde(rename_all = "camelCase")]
98pub struct SourceRevision {
99 pub repository: String,
100 pub commit_sha: String,
101 pub archive_digest: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(rename_all = "camelCase")]
106pub struct BuildSpec {
107 pub dockerfile: String,
108 pub context: String,
109 pub platform: Option<String>,
110 pub cpu_millis: u32,
111 pub memory_bytes: u64,
112 pub disk_bytes: u64,
113 pub timeout_seconds: u32,
114}
115
116#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "snake_case")]
118pub enum DeploymentStrategy {
119 #[default]
120 Recreate,
121 Rolling,
122 Canary,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126#[serde(rename_all = "camelCase")]
127pub struct RollingConfig {
128 pub max_surge: u32,
129 pub max_unavailable: u32,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[serde(rename_all = "camelCase")]
134pub struct CanaryConfig {
135 pub steps_percent: Vec<u8>,
136 pub observation_seconds: u32,
137 pub auto_promote: bool,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "camelCase")]
142pub struct SecretRef {
143 pub provider: String,
144 pub key: String,
145 pub version: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149#[serde(rename_all = "camelCase")]
150pub struct Environment {
151 pub id: String,
152 pub app_id: String,
153 pub name: String,
154 pub strategy: DeploymentStrategy,
155 pub rolling: Option<RollingConfig>,
156 pub canary: Option<CanaryConfig>,
157 #[serde(default)]
158 pub secret_refs: Vec<SecretRef>,
159 pub approval_required: bool,
160 pub version: i64,
161 pub created_at: String,
162 pub updated_at: String,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166#[serde(rename_all = "camelCase")]
167pub struct CreateEnvironmentRequest {
168 pub name: String,
169 pub strategy: DeploymentStrategy,
170 pub rolling: Option<RollingConfig>,
171 pub canary: Option<CanaryConfig>,
172 #[serde(default)]
173 pub secret_refs: Vec<SecretRef>,
174 pub approval_required: bool,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "camelCase")]
179pub struct Artifact {
180 pub digest: String,
181 pub size_bytes: u64,
182 pub storage_key: String,
183 pub sbom_digest: String,
184 pub signature: String,
185 pub policy_state: ArtifactPolicyState,
186 pub created_at: String,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
190#[serde(rename_all = "snake_case")]
191pub enum ArtifactPolicyState {
192 Pending,
193 Accepted,
194 Rejected,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198#[serde(rename_all = "camelCase")]
199pub struct GetOperationRequest {
200 pub operation_id: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "camelCase")]
205pub struct CancelDeploymentRequest {
206 pub reason: Option<String>,
207 pub version: Option<i64>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
211#[serde(rename_all = "camelCase")]
212pub struct ResumeDeploymentRequest {
213 pub expected_version: i64,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217#[serde(rename_all = "camelCase")]
218pub struct RollbackDeploymentRequest {
219 pub target_deployment_id: String,
220 pub environment: String,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
224#[serde(rename_all = "camelCase")]
225pub struct ListLogsRequest {
226 pub after: Option<i64>,
227 pub limit: Option<u32>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
231#[serde(rename_all = "camelCase")]
232pub struct LogChunk {
233 pub cursor: i64,
234 pub stream: LogStream,
235 pub timestamp: String,
236 pub payload: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240#[serde(rename_all = "snake_case")]
241pub enum LogStream {
242 Stdout,
243 Stderr,
244 System,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "camelCase")]
249pub struct CursorPage<T> {
250 pub items: Vec<T>,
251 pub next_cursor: Option<String>,
252}
253
254pub type LogPage = CursorPage<LogChunk>;
255
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
257#[serde(rename_all = "camelCase")]
258pub struct RouteBinding {
259 pub app_id: String,
260 pub environment: String,
261 pub release_id: String,
262 pub upstream: String,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub fallback_upstream: Option<String>,
267 #[serde(default = "full_traffic")]
269 pub traffic_percent: u8,
270 pub healthy: bool,
271 pub version: i64,
272 pub updated_at: String,
273}
274
275const fn full_traffic() -> u8 {
276 100
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
280#[serde(rename_all = "camelCase")]
281pub struct Release {
282 pub id: String,
283 pub app_id: String,
284 pub environment: String,
285 pub artifact_digest: String,
286 pub runtime_config_digest: String,
287 #[serde(default)]
293 pub upstream: String,
294 pub created_at: String,
295 pub approved_by: Option<String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
299#[serde(rename_all = "camelCase")]
300pub struct ActivateReleaseRequest {
301 pub expected_route_version: i64,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
305#[serde(rename_all = "camelCase")]
306pub struct PromoteReleaseRequest {
307 pub source_release_id: String,
308 pub target_environment_id: String,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "camelCase")]
313pub struct ApprovalRequest {
314 pub expected_version: i64,
315 pub comment: Option<String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
319#[serde(rename_all = "camelCase")]
320pub struct Promotion {
321 pub id: String,
322 pub source_release_id: String,
323 pub target_environment_id: String,
324 pub state: PromotionState,
325 pub requested_by: String,
326 pub approved_by: Option<String>,
327 pub comment: Option<String>,
328 pub version: i64,
329 pub created_at: String,
330 pub updated_at: String,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
334#[serde(rename_all = "snake_case")]
335pub enum PromotionState {
336 PendingApproval,
337 Approved,
338 Rejected,
339 Activated,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "snake_case")]
344pub enum OperationState {
345 Queued,
346 Running,
347 Succeeded,
348 Failed,
349 Canceling,
350 Canceled,
351}
352
353impl OperationState {
354 pub fn parse(value: &str) -> Option<Self> {
355 Some(match value {
356 "queued" => Self::Queued,
357 "running" => Self::Running,
358 "succeeded" => Self::Succeeded,
359 "failed" => Self::Failed,
360 "canceling" => Self::Canceling,
361 "canceled" => Self::Canceled,
362 _ => return None,
363 })
364 }
365
366 pub const fn is_terminal(&self) -> bool {
367 matches!(self, Self::Succeeded | Self::Failed | Self::Canceled)
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
372#[serde(rename_all = "snake_case")]
373pub enum DeploymentPhase {
374 Queued,
375 Fetching,
376 Building,
377 Publishing,
378 Starting,
379 Verifying,
380 Activating,
381 AwaitingApproval,
382 Succeeded,
383 Failed,
384 Canceling,
385 Canceled,
386}
387
388impl DeploymentPhase {
389 pub fn parse(value: &str) -> Option<Self> {
390 Some(match value {
391 "queued" => Self::Queued,
392 "fetching" => Self::Fetching,
393 "building" => Self::Building,
394 "publishing" | "uploading" => Self::Publishing,
395 "starting" => Self::Starting,
396 "verifying" => Self::Verifying,
397 "activating" => Self::Activating,
398 "awaiting_approval" => Self::AwaitingApproval,
399 "succeeded" => Self::Succeeded,
400 "failed" => Self::Failed,
401 "canceling" => Self::Canceling,
402 "canceled" => Self::Canceled,
403 _ => return None,
404 })
405 }
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
409#[serde(rename_all = "snake_case")]
410pub enum DeploymentStatus {
411 Pending,
412 Running,
413 Succeeded,
414 Failed,
415 Canceling,
416 Canceled,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
420#[serde(rename_all = "camelCase")]
421pub struct Operation {
422 pub id: String,
423 pub deployment_id: String,
424 pub state: OperationState,
425 pub phase: DeploymentPhase,
426 pub attempt: i64,
427 pub lease_owner: Option<String>,
428 pub lease_deadline: Option<String>,
429 pub fencing_token: i64,
430 pub provider_external_id: String,
431 pub version: i64,
432 pub created_at: String,
433 pub started_at: Option<String>,
434 pub finished_at: Option<String>,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub next_poll_after_ms: Option<u64>,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub error_code: Option<String>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
442#[serde(rename_all = "camelCase")]
443pub struct Deployment {
444 pub id: String,
445 pub app_id: String,
446 pub operation_id: String,
447 pub status: DeploymentStatus,
448 pub phase: DeploymentPhase,
449 pub provider_ref: Option<String>,
450 pub endpoint: Option<String>,
451 pub created_at: String,
452 pub updated_at: String,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
456#[serde(rename_all = "camelCase")]
457pub struct ErrorEnvelope {
458 pub error: InfraError,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
462#[serde(rename_all = "camelCase")]
463pub struct InfraError {
464 pub code: String,
465 pub message: String,
466 pub category: ErrorCategory,
467 pub retryable: bool,
468 pub request_id: String,
469 #[serde(default)]
470 pub details: serde_json::Value,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
474#[serde(rename_all = "snake_case")]
475pub enum ErrorCategory {
476 Validation,
477 Auth,
478 Forbidden,
479 Conflict,
480 Quota,
481 NotFound,
482 Upstream,
483 Timeout,
484 Internal,
485}
486
487pub const IDEMPOTENCY_CONFLICT: &str = "IDEMPOTENCY_CONFLICT";
488pub const VERSION_CONFLICT: &str = "VERSION_CONFLICT";
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 #[test]
494 fn operation_wire_is_camel_case() {
495 let value = serde_json::to_value(Operation {
496 id: "o".into(),
497 deployment_id: "d".into(),
498 state: OperationState::Queued,
499 phase: DeploymentPhase::Queued,
500 attempt: 0,
501 lease_owner: None,
502 lease_deadline: None,
503 fencing_token: 0,
504 provider_external_id: "deploy:d".into(),
505 version: 1,
506 created_at: "t".into(),
507 started_at: None,
508 finished_at: None,
509 next_poll_after_ms: Some(250),
510 error_code: None,
511 })
512 .unwrap();
513 assert_eq!(value["deploymentId"], "d");
514 assert!(value.get("deployment_id").is_none());
515 }
516
517 #[test]
518 fn progressive_route_and_release_upstream_are_stable_wire_contracts() {
519 let route = RouteBinding {
520 app_id: "app".into(),
521 environment: "prod".into(),
522 release_id: "new".into(),
523 upstream: "https://provider/new".into(),
524 fallback_upstream: Some("https://provider/old".into()),
525 traffic_percent: 25,
526 healthy: true,
527 version: 2,
528 updated_at: "now".into(),
529 };
530 let value = serde_json::to_value(&route).unwrap();
531 assert_eq!(value["fallbackUpstream"], "https://provider/old");
532 assert_eq!(value["trafficPercent"], 25);
533 let decoded: RouteBinding = serde_json::from_value(value).unwrap();
534 assert_eq!(decoded, route);
535
536 let legacy: RouteBinding = serde_json::from_value(serde_json::json!({
537 "appId":"app","environment":"prod","releaseId":"old",
538 "upstream":"https://provider/old","healthy":true,"version":1,"updatedAt":"now"
539 }))
540 .unwrap();
541 assert_eq!(legacy.traffic_percent, 100);
542 assert!(legacy.fallback_upstream.is_none());
543 }
544}