pipedash_plugin_bitbucket/
plugin.rs1use 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(¶ms.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;