bitbucket_server_rs/api/
build_status_post.rs

1//! # Build Status POST API
2//!
3//! This module provides functionality to post build status updates to Bitbucket Server.
4//! It allows setting the status of a build for a specific commit, which can be used
5//! to integrate CI/CD systems with Bitbucket Server.
6
7use crate::api::build_status::{BuildStatusState, TestResults};
8use crate::api::Api;
9use crate::client::{ApiRequest, ApiResponse, Client};
10use chrono::{serde::ts_seconds_option, DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13/// The payload for posting a build status update.
14///
15/// This struct represents the data that will be sent to the Bitbucket Server API
16/// when posting a build status update for a commit.
17#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct BuildStatusPostPayload {
20    /// The string referring to this branch plan/job.
21    ///
22    /// This is a unique identifier for the build status within the context of the repository.
23    pub key: String,
24    
25    /// The build status state (SUCCESSFUL, FAILED, INPROGRESS, CANCELLED, or UNKNOWN).
26    pub state: BuildStatusState,
27    
28    /// URL referring to the build result page in the CI tool.
29    ///
30    /// This URL will be linked from the Bitbucket Server UI.
31    pub url: String,
32    
33    /// A unique identifier for this particular run of a plan.
34    ///
35    /// This can be used to track specific build runs.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub build_number: Option<String>,
38    
39    /// The date when the build status was added.
40    ///
41    /// If not provided, the current time will be used.
42    #[serde(skip_serializing_if = "Option::is_none", with = "ts_seconds_option")]
43    pub date_added: Option<DateTime<Utc>>,
44    
45    /// A description of the build result.
46    ///
47    /// This can provide additional context about the build status.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub description: Option<String>,
50    
51    /// Duration of a completed build in milliseconds.
52    ///
53    /// This can be used to track build performance.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub duration: Option<u64>,
56    
57    /// A short string that describes the build plan.
58    ///
59    /// This is displayed in the Bitbucket Server UI.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub name: Option<String>,
62    
63    /// The identifier for the plan or job that ran the branch plan that produced this build status.
64    ///
65    /// This can be used to group related builds.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub parent: Option<String>,
68    
69    /// The fully qualified git reference e.g. refs/heads/master.
70    ///
71    /// This can be used to associate the build status with a specific branch or tag.
72    #[serde(skip_serializing_if = "Option::is_none", rename = "ref")]
73    pub reference: Option<String>,
74    
75    /// A summary of the passed, failed and skipped tests.
76    ///
77    /// This can be used to provide test result information.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub test_results: Option<TestResults>,
80}
81
82/// Request builder for posting a build status update.
83///
84/// This struct is used to build and send requests to post build status updates.
85#[derive(Debug)]
86pub struct BuildStatusPost {
87    /// The HTTP client to use for making requests
88    client: Client,
89    
90    /// The key of the project containing the repository
91    project_key: String,
92    
93    /// The ID of the commit to post the build status for
94    commit_id: String,
95    
96    /// The slug of the repository
97    repository_slug: String,
98    
99    /// The build status payload to post
100    build_status: BuildStatusPostPayload,
101}
102
103impl ApiRequest for BuildStatusPost {
104    // response has no content
105    type Output = ();
106
107    /// Sends the request to post a build status update.
108    ///
109    /// # Returns
110    ///
111    /// A Result indicating success or failure.
112    async fn send(&self) -> ApiResponse<Self::Output> {
113        let request_uri = format!(
114            "api/latest/projects/{}/repos/{}/commits/{}/builds",
115            self.project_key, self.repository_slug, self.commit_id
116        );
117
118        self.client
119            .post::<Self>(
120                &request_uri,
121                &serde_json::to_string(&self.build_status).unwrap(),
122            )
123            .await
124    }
125}
126
127impl Api {
128    /// Creates a request to post a build status update for a commit.
129    ///
130    /// This method returns a builder that can be used to send a request
131    /// to post a build status update for a commit.
132    ///
133    /// # Arguments
134    ///
135    /// * `project_key` - The key of the project containing the repository
136    /// * `repository_slug` - The slug of the repository
137    /// * `commit_id` - The ID of the commit to post the build status for
138    /// * `build_status` - The build status payload to post
139    ///
140    /// # Returns
141    ///
142    /// A builder for sending the request
143    ///
144    /// # Example
145    ///
146    /// ```no_run
147    /// use bitbucket_server_rs::client::{new, ApiRequest};
148    /// use bitbucket_server_rs::api::build_status::BuildStatusState;
149    /// use bitbucket_server_rs::api::build_status_post::BuildStatusPostPayload;
150    ///
151    /// #[tokio::main]
152    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
153    ///     let client = new("https://bitbucket-server/rest", "API_TOKEN");
154    ///
155    ///     // Create build status payload
156    ///     let build_status = BuildStatusPostPayload {
157    ///         key: "build-123".to_string(),
158    ///         state: BuildStatusState::Successful,
159    ///         url: "https://ci.example.com/build/123".to_string(),
160    ///         description: Some("Build passed successfully".to_string()),
161    ///         name: Some("CI Build".to_string()),
162    ///         ..Default::default()
163    ///     };
164    ///
165    ///     // Post the build status
166    ///     let response = client
167    ///         .api()
168    ///         .build_status_post(
169    ///             "PROJECT_KEY",
170    ///             "REPOSITORY_SLUG",
171    ///             "COMMIT_ID",
172    ///             &build_status
173    ///         )
174    ///         .send()
175    ///         .await?;
176    ///
177    ///     println!("Build status posted successfully");
178    ///
179    ///     Ok(())
180    /// }
181    /// ```
182    ///
183    /// # Notes
184    ///
185    /// * The authenticated user must have REPO_READ permission for the repository that this build
186    ///   status is for. The request can also be made with anonymous 2-legged OAuth.
187    ///
188    /// See [Bitbucket Data Center REST API Docs](https://developer.atlassian.com/server/bitbucket/rest/v811/api-group-builds-and-deployments/#api-api-latest-projects-projectkey-repos-repositoryslug-commits-commitid-builds-post)
189    pub fn build_status_post(
190        self,
191        project_key: &str,
192        repository_slug: &str,
193        commit_id: &str,
194        build_status: &BuildStatusPostPayload
195    ) -> BuildStatusPost {
196        BuildStatusPost {
197            client: self.client,
198            project_key: project_key.to_owned(),
199            commit_id: commit_id.to_owned(),
200            repository_slug: repository_slug.to_owned(),
201            build_status: build_status.to_owned()
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn it_can_serialize() {
212        let build_status = BuildStatusPostPayload {
213            key: "KEY".to_string(),
214            state: BuildStatusState::Successful,
215            url: "URL".to_string(),
216            build_number: Some("1".to_string()),
217            date_added: Some(
218                chrono::DateTime::parse_from_rfc3339("2025-01-30T01:02:03Z")
219                    .unwrap()
220                    .with_timezone(&Utc),
221            ),
222            description: Some("DESCRIPTION".to_string()),
223            duration: Some(12),
224            name: Some("NAME".to_string()),
225            parent: Some("PARENT".to_string()),
226            reference: Some("REF".to_string()),
227            test_results: Some(TestResults {
228                failed: 2,
229                successful: 3,
230                skipped: 1,
231            }),
232        };
233
234        let json = serde_json::to_string(&build_status).unwrap();
235        assert_eq!(
236            json,
237            r#"{"key":"KEY","state":"SUCCESSFUL","url":"URL","buildNumber":"1","dateAdded":1738198923,"description":"DESCRIPTION","duration":12,"name":"NAME","parent":"PARENT","ref":"REF","testResults":{"failed":2,"successful":3,"skipped":1}}"#
238        );
239    } // it_can_serialize
240
241    #[test]
242    fn it_can_serialize_partially() {
243        let build_status = BuildStatusPostPayload {
244            key: "KEY".to_string(),
245            state: BuildStatusState::Successful,
246            url: "URL".to_string(),
247            build_number: None,
248            date_added: None,
249            description: None,
250            duration: None,
251            name: None,
252            parent: None,
253            reference: None,
254            test_results: None,
255        };
256
257        let json = serde_json::to_string(&build_status).unwrap();
258        assert_eq!(json, r#"{"key":"KEY","state":"SUCCESSFUL","url":"URL"}"#);
259    } // it_can_serialize_partially
260}