posthog_cli/api/
releases.rs

1use std::collections::HashMap;
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use tracing::{info, warn};
7use uuid::Uuid;
8
9use crate::{
10    invocation_context::context,
11    utils::{files::content_hash, git::GitInfo},
12};
13
14#[derive(Debug, Clone, Deserialize)]
15pub struct Release {
16    pub id: Uuid,
17    pub hash_id: String,
18    pub version: String,
19    pub project: String,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct ReleaseBuilder {
24    project: Option<String>,
25    version: Option<String>,
26    metadata: HashMap<String, Value>,
27}
28
29// Internal, what we send to the API
30#[derive(Debug, Clone, Serialize, Deserialize)]
31struct CreateReleaseRequest {
32    #[serde(skip_serializing_if = "HashMap::is_empty")]
33    pub metadata: HashMap<String, Value>,
34    pub hash_id: String,
35    pub version: String,
36    pub project: String,
37}
38
39impl Release {
40    pub fn lookup(project: &str, version: &str) -> Result<Option<Self>> {
41        let hash_id = content_hash([project, version]);
42        let context = context();
43        let token = &context.token;
44
45        let url = format!(
46            "{}/api/environments/{}/error_tracking/releases/hash/{hash_id}",
47            token.get_host(),
48            token.env_id,
49        );
50
51        let response = context
52            .client
53            .get(&url)
54            .header("Authorization", format!("Bearer {}", token.token))
55            .header("Content-Type", "application/json")
56            .send()?;
57
58        if response.status().as_u16() == 404 {
59            warn!("Release {} of project {} not found", version, project);
60            return Ok(None);
61        }
62
63        if response.status().is_success() {
64            info!("Found release {} of project {}", version, project);
65            Ok(Some(response.json()?))
66        } else {
67            response.error_for_status()?;
68            Ok(None) // unreachable
69        }
70    }
71}
72
73impl ReleaseBuilder {
74    pub fn init_from_git(info: GitInfo) -> Self {
75        let mut metadata = HashMap::new();
76        metadata.insert(
77            "git".to_string(),
78            serde_json::to_value(info.clone()).expect("can serialize gitinfo"),
79        );
80
81        Self {
82            metadata,
83            version: Some(info.commit_id), // TODO - We should pull this commits tags and use them if we can
84            project: info.repo_name,
85        }
86    }
87
88    pub fn with_git(&mut self, info: GitInfo) -> &mut Self {
89        self.with_metadata("git", info)
90            .expect("We can serialise git info")
91    }
92
93    pub fn with_metadata<T>(&mut self, key: &str, val: T) -> Result<&mut Self>
94    where
95        T: Serialize,
96    {
97        self.metadata
98            .insert(key.to_string(), serde_json::to_value(val)?);
99        Ok(self)
100    }
101
102    pub fn with_project(&mut self, project: &str) -> &mut Self {
103        self.project = Some(project.to_string());
104        self
105    }
106
107    pub fn with_version(&mut self, version: &str) -> &mut Self {
108        self.version = Some(version.to_string());
109        self
110    }
111
112    pub fn can_create(&self) -> bool {
113        self.version.is_some() && self.project.is_some()
114    }
115
116    pub fn missing(&self) -> Vec<&str> {
117        let mut missing = Vec::new();
118
119        if self.version.is_none() {
120            missing.push("version");
121        }
122        if self.project.is_none() {
123            missing.push("project");
124        }
125        missing
126    }
127
128    pub fn fetch_or_create(self) -> Result<Release> {
129        if !self.can_create() {
130            anyhow::bail!(
131                "Tried to create a release while missing key fields: {}",
132                self.missing().join(", ")
133            )
134        }
135        let version = self.version.as_ref().unwrap();
136        let project = self.project.as_ref().unwrap();
137        if let Some(release) = Release::lookup(project, version)? {
138            Ok(release)
139        } else {
140            self.create_release()
141        }
142    }
143
144    pub fn create_release(self) -> Result<Release> {
145        // The way to encode this kind of thing in the type system is a thing called "Type-state". It's cool,
146        // and if you're reading this and thinking "hmm this feels kind of gross and fragile", you should
147        // google "rust type state pattern". The only problem is it's a lot of boilerplate, so I didn't do it.
148        if !self.can_create() {
149            anyhow::bail!(
150                "Tried to create a release while missing key fields: {}",
151                self.missing().join(", ")
152            )
153        }
154        let version = self.version.unwrap();
155        let project = self.project.unwrap();
156        let metadata = self.metadata;
157
158        let hash_id = content_hash([project.as_bytes(), version.as_bytes()]);
159
160        let token = &context().token;
161        let request = CreateReleaseRequest {
162            metadata,
163            hash_id,
164            version,
165            project,
166        };
167
168        let url = format!(
169            "{}/api/environments/{}/error_tracking/releases",
170            token.get_host(),
171            token.env_id
172        );
173
174        let client = &context().client;
175
176        let response = client
177            .post(&url)
178            .header("Authorization", format!("Bearer {}", token.token))
179            .header("Content-Type", "application/json")
180            .json(&request)
181            .send()?;
182
183        if response.status().is_success() {
184            let response = response.json::<Release>()?;
185            info!(
186                "Release {} of {} created successfully! {}",
187                request.version, request.project, response.id
188            );
189            Ok(response)
190        } else {
191            let e = response.text()?;
192            Err(anyhow::anyhow!("Failed to create release: {e}"))
193        }
194    }
195}