posthog_cli/api/
releases.rs1use 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#[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) }
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), 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 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}