Skip to main content

pipedash_plugin_api/
build_watch.rs

1use serde::{
2    Deserialize,
3    Serialize,
4};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum BuildWatchRunStatus {
9    Pending,
10    Running,
11    Success,
12    Failed,
13    Cancelled,
14    Skipped,
15}
16
17impl BuildWatchRunStatus {
18    pub fn is_terminal(self) -> bool {
19        !matches!(self, Self::Pending | Self::Running)
20    }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum BuildWatchMonitoring {
26    Watching,
27    Completed,
28    Interrupted,
29    Stopped,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "lowercase")]
34pub enum BuildWatchStopReason {
35    Stopped,
36    Interrupted,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct BuildWatchSnapshot {
42    pub session_id: String,
43    pub pipeline_id: String,
44    pub pipeline_name: String,
45    pub provider_name: String,
46    pub provider_type: String,
47    pub run_number: i64,
48    pub branch: Option<String>,
49    pub status: BuildWatchRunStatus,
50    pub started_at: i64,
51    pub concluded_at: Option<i64>,
52    pub updated_at: i64,
53    pub monitoring: BuildWatchMonitoring,
54    pub url: String,
55}
56
57impl BuildWatchSnapshot {
58    pub fn run_url(pipeline_id: &str, run_number: i64) -> String {
59        const HEX: &[u8; 16] = b"0123456789ABCDEF";
60        let mut url = String::with_capacity(pipeline_id.len() + 64);
61        url.push_str("pipedash://pipelines/");
62        for byte in pipeline_id.bytes() {
63            if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
64                url.push(char::from(byte));
65            } else {
66                url.push('%');
67                url.push(char::from(HEX[usize::from(byte >> 4)]));
68                url.push(char::from(HEX[usize::from(byte & 15)]));
69            }
70        }
71        url.push_str("/runs/");
72        url.push_str(&run_number.to_string());
73        url
74    }
75
76    pub fn validate(&self) -> Result<(), String> {
77        let valid_text = [
78            (&self.session_id, 128),
79            (&self.pipeline_id, 2048),
80            (&self.pipeline_name, 512),
81            (&self.provider_name, 512),
82            (&self.provider_type, 128),
83        ]
84        .into_iter()
85        .all(|(text, limit)| {
86            !text.trim().is_empty() && text.len() <= limit && !text.chars().any(char::is_control)
87        });
88        let valid_branch = self
89            .branch
90            .as_ref()
91            .is_none_or(|branch| branch.len() <= 1024 && !branch.chars().any(char::is_control));
92        let valid_lifecycle = match self.monitoring {
93            BuildWatchMonitoring::Watching => !self.status.is_terminal(),
94            BuildWatchMonitoring::Completed => self.status.is_terminal(),
95            BuildWatchMonitoring::Interrupted | BuildWatchMonitoring::Stopped => true,
96        };
97        if !valid_text
98            || !valid_branch
99            || !(1..=9_007_199_254_740_991).contains(&self.run_number)
100            || self.started_at < 0
101            || self.updated_at < 0
102            || self.concluded_at.is_some_and(|end| end < self.started_at)
103            || !valid_lifecycle
104            || self.url.len() > 8192
105            || self.url != Self::run_url(&self.pipeline_id, self.run_number)
106        {
107            return Err("Invalid build watch snapshot".to_string());
108        }
109        let size = serde_json::to_vec(self)
110            .map_err(|error| error.to_string())?
111            .len();
112        if size > 3500 {
113            return Err("The build details exceed the watch size limit".to_string());
114        }
115        Ok(())
116    }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct BuildWatchStatus {
122    pub supported: bool,
123    pub available: bool,
124    pub reason: Option<String>,
125    pub watch: Option<BuildWatchSnapshot>,
126}
127
128#[cfg(test)]
129mod tests;