Skip to main content

pipedash_plugin_bitbucket/
plugin.rs

1use std::collections::HashMap;
2
3use pipedash_plugin_api::support::{
4    resume_position,
5    string_parameter,
6};
7use pipedash_plugin_api::*;
8
9use crate::log_dialect::BitbucketLogDialect;
10use crate::{
11    client,
12    config,
13    mapper,
14    metadata,
15    types,
16};
17
18mod catalog;
19
20#[derive(serde::Serialize, serde::Deserialize)]
21struct LogPosition {
22    pipeline: String,
23    step: String,
24    offset: u64,
25}
26
27plugin_struct!(
28    BitbucketPlugin,
29    client::BitbucketClient,
30    metadata::create_metadata();
31    status_source: Option<StatusSource> = None
32);
33
34plugin_impl!(BitbucketPlugin [
35    metadata,
36    status_source,
37    fetch_organizations,
38    ids_with_ci,
39    fetch_available_pipelines,
40    fetch_available_pipelines_filtered,
41    fetch_pipelines,
42] {
43    fn initialize(
44        &mut self, _provider_id: i64, config: HashMap<String, String>,
45        http_client: Option<std::sync::Arc<reqwest::Client>>,
46    ) -> PluginResult<()> {
47        let (email, api_token) = config::get_auth(&config)?;
48        let api_url = config::get_api_url();
49
50        let credentials = format!("{}:{}", email, api_token);
51        let encoded = base64::Engine::encode(
52            &base64::engine::general_purpose::STANDARD,
53            credentials.as_bytes(),
54        );
55        let auth_value = format!("Basic {}", encoded);
56
57        let client = require_client(http_client)?;
58
59        self.status_source = Some(config::status_source()?);
60        self.client = Some(client::BitbucketClient::new(client, api_url, auth_value));
61        self.config = config;
62
63        Ok(())
64    }
65
66    async fn validate_credentials(&self) -> PluginResult<bool> {
67        let client = self.client()?;
68        client.get_user().await?;
69        Ok(true)
70    }
71
72    async fn fetch_run_history(
73        &self, pipeline_id: &str, limit: usize,
74    ) -> PluginResult<Vec<PipelineRun>> {
75        let (workspace, repo_slug) = config::parse_pipeline_id(pipeline_id)?;
76        let client = self.client()?;
77
78        let pipelines = client.list_pipelines(&workspace, &repo_slug, limit).await?;
79
80        Ok(pipelines
81            .iter()
82            .map(|p| mapper::map_pipeline_run(p, &workspace, &repo_slug))
83            .collect())
84    }
85
86    async fn fetch_run_details(
87        &self, pipeline_id: &str, run_number: i64,
88    ) -> PluginResult<PipelineRun> {
89        let (workspace, repo_slug) = config::parse_pipeline_id(pipeline_id)?;
90
91        let pipeline = self
92            .client()?
93            .find_pipeline_by_build_number(&workspace, &repo_slug, run_number)
94            .await?;
95
96        Ok(mapper::map_pipeline_run(&pipeline, &workspace, &repo_slug))
97    }
98
99    async fn fetch_log_nodes(&self, run: RunRef<'_>) -> PluginResult<Vec<LogNode>> {
100        let (workspace, repo_slug) = config::parse_pipeline_id(run.pipeline_id)?;
101        let client = self.client()?;
102
103        let pipeline = client
104            .find_pipeline_by_build_number(&workspace, &repo_slug, run.run_number)
105            .await?;
106        let steps = client
107            .list_steps(&workspace, &repo_slug, &pipeline.uuid)
108            .await?;
109
110        Ok(mapper::steps_to_log_nodes(&steps))
111    }
112
113    async fn read_log(
114        &self, run: RunRef<'_>, node_id: &str, from: Option<&LogCursor>, sink: &mut LogSink,
115    ) -> PluginResult<LogRead> {
116        let (workspace, repo_slug) = config::parse_pipeline_id(run.pipeline_id)?;
117        config::check_step_uuid(node_id)?;
118        let client = self.client()?;
119
120        let pipeline = client
121            .find_pipeline_by_build_number(&workspace, &repo_slug, run.run_number)
122            .await?;
123
124        let position = resume_position::<LogPosition>(from, sink, |position| {
125            position.pipeline == pipeline.uuid && position.step == node_id
126        })?;
127        let (mode, offset) = client
128            .stream_step_log(
129                &workspace,
130                &repo_slug,
131                &pipeline.uuid,
132                node_id,
133                position.map(|p| p.offset),
134                sink,
135            )
136            .await?;
137        Ok(LogRead {
138            mode,
139            cursor: Some(LogCursor::new(&LogPosition {
140                pipeline: pipeline.uuid,
141                step: node_id.to_string(),
142                offset,
143            })?),
144        })
145    }
146
147    fn log_dialect(&self) -> &'static dyn LogDialect {
148        &BitbucketLogDialect
149    }
150
151    async fn fetch_workflow_parameters(
152        &self, _workflow_id: &str,
153    ) -> PluginResult<Vec<WorkflowParameter>> {
154        Ok(vec![
155            string_parameter(
156                "ref",
157                "Branch",
158                "Branch name to run pipeline on",
159                Some("main".to_string()),
160                true,
161            ),
162            string_parameter(
163                "custom_pipeline",
164                "Custom Pipeline",
165                "Name of custom pipeline to run (optional)",
166                None,
167                false,
168            ),
169        ])
170    }
171
172    async fn trigger_pipeline(&self, params: TriggerParams) -> PluginResult<TriggerResult> {
173        let (workspace, repo_slug) = config::parse_pipeline_id(&params.workflow_id)?;
174        let client = self.client()?;
175
176        let ref_name = params
177            .inputs
178            .as_ref()
179            .and_then(|i| i.get("ref"))
180            .and_then(|v| v.as_str())
181            .unwrap_or("main")
182            .to_string();
183
184        let custom_pipeline = params
185            .inputs
186            .as_ref()
187            .and_then(|i| i.get("custom_pipeline"))
188            .and_then(|v| v.as_str())
189            .filter(|s| !s.is_empty());
190
191        let request = types::TriggerPipelineRequest {
192            target: types::TriggerTarget {
193                target_type: "pipeline_ref_target".to_string(),
194                ref_name,
195                ref_type: "branch".to_string(),
196                selector: custom_pipeline.map(|name| types::TriggerSelector {
197                    selector_type: "custom".to_string(),
198                    pattern: Some(name.to_string()),
199                }),
200            },
201        };
202
203        client
204            .trigger_pipeline(&workspace, &repo_slug, request)
205            .await?;
206
207        Ok(TriggerResult::default())
208    }
209
210    async fn cancel_run(&self, pipeline_id: &str, run_number: i64) -> PluginResult<()> {
211        let (workspace, repo_slug) = config::parse_pipeline_id(pipeline_id)?;
212        let client = self.client()?;
213
214        let pipeline = client
215            .find_pipeline_by_build_number(&workspace, &repo_slug, run_number)
216            .await?;
217
218        let state = pipeline.state.name.as_str();
219        if state == "PAUSED" || state == "HALTED" {
220            return Err(PluginError::ApiError(format!(
221                "Cannot cancel pipeline in '{}' state. Pipelines waiting for manual approval \
222                 must be cancelled directly in Bitbucket UI.",
223                state
224            )));
225        }
226
227        if state == "COMPLETED" {
228            return Err(PluginError::ApiError(
229                "Cannot cancel a completed pipeline.".to_string(),
230            ));
231        }
232
233        if state == "IN_PROGRESS" {
234            let pipeline_is_paused = pipeline
235                .state
236                .stage
237                .as_ref()
238                .map(|s| s.name == "PAUSED")
239                .unwrap_or(false);
240
241            if pipeline_is_paused {
242                return Err(PluginError::ApiError(
243                    "Cannot cancel pipeline waiting for manual approval. \
244                     Please cancel directly in Bitbucket UI."
245                        .to_string(),
246                ));
247            }
248
249            let steps = client
250                .list_steps(&workspace, &repo_slug, &pipeline.uuid)
251                .await?;
252
253            let has_paused_step = steps.iter().any(|step| {
254                step.state.name == "PENDING"
255                    && step
256                        .state
257                        .stage
258                        .as_ref()
259                        .map(|s| s.name == "PAUSED")
260                        .unwrap_or(false)
261            });
262
263            if has_paused_step {
264                return Err(PluginError::ApiError(
265                    "Cannot cancel pipeline with steps waiting for manual approval. \
266                     Please cancel directly in Bitbucket UI."
267                        .to_string(),
268                ));
269            }
270        }
271
272        client
273            .stop_pipeline(&workspace, &repo_slug, &pipeline.uuid)
274            .await
275    }
276
277
278});
279
280#[cfg(test)]
281mod tests;