Skip to main content

aivcs_core/domain/
release.rs

1//! Release and promotion tracking.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Deployment environment for a release.
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "UPPERCASE")]
10pub enum ReleaseEnvironment {
11    Dev,
12    Staging,
13    Production,
14}
15
16/// A release of an agent into a specific environment.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct Release {
19    /// Unique identifier for this release.
20    pub release_id: Uuid,
21
22    /// Name of the agent being released.
23    pub agent_name: String,
24
25    /// Digest of the AgentSpec being released.
26    pub spec_digest: String,
27
28    /// Digest of the tools configuration.
29    pub tools_digest: String,
30
31    /// Digest of the graph definition.
32    pub graph_digest: String,
33
34    /// Semantic version of the release.
35    pub version: String,
36
37    /// Target environment.
38    pub environment: ReleaseEnvironment,
39
40    /// When the release was promoted.
41    pub promoted_at: DateTime<Utc>,
42
43    /// User/system that promoted this release.
44    pub promoted_by: String,
45
46    /// Additional metadata (changelog, notes, etc.).
47    pub metadata: serde_json::Value,
48}
49
50impl Release {
51    /// Create a new release.
52    pub fn new(
53        agent_name: String,
54        spec_digest: String,
55        tools_digest: String,
56        graph_digest: String,
57        version: String,
58        environment: ReleaseEnvironment,
59        promoted_by: String,
60    ) -> Self {
61        Self {
62            release_id: Uuid::new_v4(),
63            agent_name,
64            spec_digest,
65            tools_digest,
66            graph_digest,
67            version,
68            environment,
69            promoted_at: Utc::now(),
70            promoted_by,
71            metadata: serde_json::json!({}),
72        }
73    }
74}
75
76/// Pointer to the current and previous release for an agent in an environment.
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub struct ReleasePointer {
79    /// Name of the agent.
80    pub agent_name: String,
81
82    /// Digest of the currently deployed spec.
83    pub current_spec_digest: String,
84
85    /// Digest of the previously deployed spec (for rollback).
86    pub previous_spec_digest: Option<String>,
87
88    /// When the current release was deployed.
89    pub last_updated: DateTime<Utc>,
90}
91
92impl ReleasePointer {
93    /// Create a new release pointer.
94    pub fn new(agent_name: String, current_spec_digest: String) -> Self {
95        Self {
96            agent_name,
97            current_spec_digest,
98            previous_spec_digest: None,
99            last_updated: Utc::now(),
100        }
101    }
102
103    /// Promote a new spec, moving current to previous.
104    pub fn promote(&mut self, new_spec_digest: String) {
105        self.previous_spec_digest = Some(self.current_spec_digest.clone());
106        self.current_spec_digest = new_spec_digest;
107        self.last_updated = Utc::now();
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_release_serde_roundtrip() {
117        let release = Release::new(
118            "my_agent".to_string(),
119            "spec_digest_123".to_string(),
120            "tools_digest_abc".to_string(),
121            "graph_digest_xyz".to_string(),
122            "1.2.3".to_string(),
123            ReleaseEnvironment::Production,
124            "github-ci".to_string(),
125        );
126
127        let json = serde_json::to_string(&release).expect("serialize");
128        let deserialized: Release = serde_json::from_str(&json).expect("deserialize");
129
130        assert_eq!(release, deserialized);
131    }
132
133    #[test]
134    fn test_release_environment_serde() {
135        let envs = [
136            ReleaseEnvironment::Dev,
137            ReleaseEnvironment::Staging,
138            ReleaseEnvironment::Production,
139        ];
140
141        for env in &envs {
142            let json = serde_json::to_string(env).expect("serialize");
143            let deserialized: ReleaseEnvironment =
144                serde_json::from_str(&json).expect("deserialize");
145            assert_eq!(*env, deserialized);
146        }
147    }
148
149    #[test]
150    fn test_release_pointer_previous_is_none() {
151        let pointer = ReleasePointer::new("my_agent".to_string(), "spec_digest_abc".to_string());
152
153        assert_eq!(pointer.agent_name, "my_agent");
154        assert_eq!(pointer.current_spec_digest, "spec_digest_abc");
155        assert!(pointer.previous_spec_digest.is_none());
156    }
157
158    #[test]
159    fn test_release_pointer_promote() {
160        let mut pointer = ReleasePointer::new("my_agent".to_string(), "spec_digest_v1".to_string());
161
162        pointer.promote("spec_digest_v2".to_string());
163
164        assert_eq!(pointer.current_spec_digest, "spec_digest_v2");
165        assert_eq!(
166            pointer.previous_spec_digest,
167            Some("spec_digest_v1".to_string())
168        );
169    }
170
171    #[test]
172    fn test_release_pointer_serde_roundtrip() {
173        let pointer =
174            ReleasePointer::new("my_agent".to_string(), "spec_digest_current".to_string());
175
176        let json = serde_json::to_string(&pointer).expect("serialize");
177        let deserialized: ReleasePointer = serde_json::from_str(&json).expect("deserialize");
178
179        assert_eq!(pointer, deserialized);
180    }
181
182    #[test]
183    fn test_release_environment_all_variants() {
184        let dev_json = serde_json::to_string(&ReleaseEnvironment::Dev).expect("serialize");
185        let staging_json = serde_json::to_string(&ReleaseEnvironment::Staging).expect("serialize");
186        let prod_json = serde_json::to_string(&ReleaseEnvironment::Production).expect("serialize");
187
188        assert!(dev_json.contains("DEV"));
189        assert!(staging_json.contains("STAGING"));
190        assert!(prod_json.contains("PRODUCTION"));
191    }
192}