github_bot_sdk/client/workflow.rs
1// Workflow and workflow run operations for GitHub API
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::client::InstallationClient;
7use crate::error::ApiError;
8
9/// GitHub Actions workflow.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Workflow {
12 /// Unique workflow identifier
13 pub id: u64,
14
15 /// Node ID for GraphQL API
16 pub node_id: String,
17
18 /// Workflow name
19 pub name: String,
20
21 /// Workflow file path
22 pub path: String,
23
24 /// Workflow state
25 pub state: String, // "active", "disabled_manually", "disabled_inactivity"
26
27 /// Creation timestamp
28 pub created_at: DateTime<Utc>,
29
30 /// Last update timestamp
31 pub updated_at: DateTime<Utc>,
32
33 /// Workflow URL
34 pub url: String,
35
36 /// Workflow HTML URL
37 pub html_url: String,
38
39 /// Workflow badge URL
40 pub badge_url: String,
41}
42
43/// GitHub Actions workflow run.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct WorkflowRun {
46 /// Unique workflow run identifier
47 pub id: u64,
48
49 /// Node ID for GraphQL API
50 pub node_id: String,
51
52 /// Workflow run name
53 pub name: String,
54
55 /// Workflow run number
56 pub run_number: u64,
57
58 /// Event that triggered the workflow
59 pub event: String,
60
61 /// Workflow run status
62 pub status: String, // "queued", "in_progress", "completed"
63
64 /// Workflow run conclusion (if completed)
65 pub conclusion: Option<String>, // "success", "failure", "cancelled", "skipped", etc.
66
67 /// Workflow ID
68 pub workflow_id: u64,
69
70 /// Head branch
71 pub head_branch: String,
72
73 /// Head commit SHA
74 pub head_sha: String,
75
76 /// Creation timestamp
77 pub created_at: DateTime<Utc>,
78
79 /// Last update timestamp
80 pub updated_at: DateTime<Utc>,
81
82 /// Workflow run URL
83 pub url: String,
84
85 /// Workflow run HTML URL
86 pub html_url: String,
87}
88
89/// Request to trigger a workflow.
90#[derive(Debug, Clone, Serialize)]
91pub struct TriggerWorkflowRequest {
92 /// Git reference (branch or tag)
93 #[serde(rename = "ref")]
94 pub git_ref: String,
95
96 /// Workflow inputs (key-value pairs)
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub inputs: Option<std::collections::HashMap<String, String>>,
99}
100
101impl InstallationClient {
102 // ========================================================================
103 // Workflow Operations
104 // ========================================================================
105
106 /// List workflows in a repository.
107 ///
108 /// Retrieves all GitHub Actions workflows for a repository.
109 ///
110 /// # Arguments
111 ///
112 /// * `owner` - Repository owner
113 /// * `repo` - Repository name
114 ///
115 /// # Returns
116 ///
117 /// Returns vector of workflows.
118 ///
119 /// # Errors
120 ///
121 /// * `ApiError::NotFound` - Repository does not exist
122 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
123 ///
124 /// # Example
125 ///
126 /// ```no_run
127 /// # use github_bot_sdk::client::InstallationClient;
128 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
129 /// let workflows = client.list_workflows("owner", "repo").await?;
130 /// for workflow in workflows {
131 /// println!("Workflow: {} ({})", workflow.name, workflow.state);
132 /// }
133 /// # Ok(())
134 /// # }
135 /// ```
136 pub async fn list_workflows(&self, owner: &str, repo: &str) -> Result<Vec<Workflow>, ApiError> {
137 let path = format!("/repos/{}/{}/actions/workflows", owner, repo);
138 let response = self.get(&path).await?;
139
140 let status = response.status();
141 if !status.is_success() {
142 return Err(match status.as_u16() {
143 404 => ApiError::NotFound,
144 403 => ApiError::AuthorizationFailed,
145 401 => ApiError::AuthenticationFailed,
146 _ => {
147 let message = response
148 .text()
149 .await
150 .unwrap_or_else(|_| "Unknown error".to_string());
151 ApiError::HttpError {
152 status: status.as_u16(),
153 message,
154 }
155 }
156 });
157 }
158
159 #[derive(Deserialize)]
160 struct WorkflowsResponse {
161 workflows: Vec<Workflow>,
162 }
163
164 let workflows_response: WorkflowsResponse =
165 response.json().await.map_err(ApiError::from)?;
166 Ok(workflows_response.workflows)
167 }
168
169 /// Get a specific workflow by ID.
170 ///
171 /// Retrieves details about a single workflow.
172 ///
173 /// # Arguments
174 ///
175 /// * `owner` - Repository owner
176 /// * `repo` - Repository name
177 /// * `workflow_id` - Workflow ID
178 ///
179 /// # Returns
180 ///
181 /// Returns the `Workflow` with the specified ID.
182 ///
183 /// # Errors
184 ///
185 /// * `ApiError::NotFound` - Workflow does not exist
186 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
187 ///
188 /// # Example
189 ///
190 /// ```no_run
191 /// # use github_bot_sdk::client::InstallationClient;
192 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
193 /// let workflow = client.get_workflow("owner", "repo", 123456).await?;
194 /// println!("Workflow: {} at {}", workflow.name, workflow.path);
195 /// # Ok(())
196 /// # }
197 /// ```
198 pub async fn get_workflow(
199 &self,
200 owner: &str,
201 repo: &str,
202 workflow_id: u64,
203 ) -> Result<Workflow, ApiError> {
204 let path = format!(
205 "/repos/{}/{}/actions/workflows/{}",
206 owner, repo, workflow_id
207 );
208 let response = self.get(&path).await?;
209
210 let status = response.status();
211 if !status.is_success() {
212 return Err(match status.as_u16() {
213 404 => ApiError::NotFound,
214 403 => ApiError::AuthorizationFailed,
215 401 => ApiError::AuthenticationFailed,
216 _ => {
217 let message = response
218 .text()
219 .await
220 .unwrap_or_else(|_| "Unknown error".to_string());
221 ApiError::HttpError {
222 status: status.as_u16(),
223 message,
224 }
225 }
226 });
227 }
228
229 response.json().await.map_err(ApiError::from)
230 }
231
232 /// Trigger a workflow run.
233 ///
234 /// Manually triggers a workflow run using workflow_dispatch event.
235 ///
236 /// # Arguments
237 ///
238 /// * `owner` - Repository owner
239 /// * `repo` - Repository name
240 /// * `workflow_id` - Workflow ID
241 /// * `request` - Trigger parameters (ref and optional inputs)
242 ///
243 /// # Returns
244 ///
245 /// Returns `Ok(())` on successful trigger.
246 ///
247 /// # Errors
248 ///
249 /// * `ApiError::NotFound` - Workflow does not exist
250 /// * `ApiError::InvalidRequest` - Workflow not configured for manual dispatch
251 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
252 ///
253 /// # Example
254 ///
255 /// ```no_run
256 /// # use github_bot_sdk::client::{InstallationClient, TriggerWorkflowRequest};
257 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
258 /// let request = TriggerWorkflowRequest {
259 /// git_ref: "main".to_string(),
260 /// inputs: None,
261 /// };
262 /// client.trigger_workflow("owner", "repo", 123456, request).await?;
263 /// println!("Workflow triggered");
264 /// # Ok(())
265 /// # }
266 /// ```
267 pub async fn trigger_workflow(
268 &self,
269 owner: &str,
270 repo: &str,
271 workflow_id: u64,
272 request: TriggerWorkflowRequest,
273 ) -> Result<(), ApiError> {
274 let path = format!(
275 "/repos/{}/{}/actions/workflows/{}/dispatches",
276 owner, repo, workflow_id
277 );
278 let response = self.post(&path, &request).await?;
279
280 let status = response.status();
281 if !status.is_success() {
282 return Err(match status.as_u16() {
283 404 => ApiError::NotFound,
284 403 => ApiError::AuthorizationFailed,
285 401 => ApiError::AuthenticationFailed,
286 422 => {
287 let message = response
288 .text()
289 .await
290 .unwrap_or_else(|_| "Validation error".to_string());
291 ApiError::InvalidRequest { message }
292 }
293 _ => {
294 let message = response
295 .text()
296 .await
297 .unwrap_or_else(|_| "Unknown error".to_string());
298 ApiError::HttpError {
299 status: status.as_u16(),
300 message,
301 }
302 }
303 });
304 }
305
306 Ok(())
307 }
308
309 // ========================================================================
310 // Workflow Run Operations
311 // ========================================================================
312
313 /// List workflow runs for a workflow.
314 ///
315 /// Retrieves workflow runs for a specific workflow.
316 ///
317 /// # Arguments
318 ///
319 /// * `owner` - Repository owner
320 /// * `repo` - Repository name
321 /// * `workflow_id` - Workflow ID
322 ///
323 /// # Returns
324 ///
325 /// Returns vector of workflow runs.
326 ///
327 /// # Errors
328 ///
329 /// * `ApiError::NotFound` - Workflow does not exist
330 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
331 ///
332 /// # Example
333 ///
334 /// ```no_run
335 /// # use github_bot_sdk::client::InstallationClient;
336 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
337 /// let runs = client.list_workflow_runs("owner", "repo", 123456).await?;
338 /// for run in runs {
339 /// println!("Run #{}: {} ({})", run.run_number, run.status, run.conclusion.unwrap_or_default());
340 /// }
341 /// # Ok(())
342 /// # }
343 /// ```
344 pub async fn list_workflow_runs(
345 &self,
346 owner: &str,
347 repo: &str,
348 workflow_id: u64,
349 ) -> Result<Vec<WorkflowRun>, ApiError> {
350 let path = format!(
351 "/repos/{}/{}/actions/workflows/{}/runs",
352 owner, repo, workflow_id
353 );
354 let response = self.get(&path).await?;
355
356 let status = response.status();
357 if !status.is_success() {
358 return Err(match status.as_u16() {
359 404 => ApiError::NotFound,
360 403 => ApiError::AuthorizationFailed,
361 401 => ApiError::AuthenticationFailed,
362 _ => {
363 let message = response
364 .text()
365 .await
366 .unwrap_or_else(|_| "Unknown error".to_string());
367 ApiError::HttpError {
368 status: status.as_u16(),
369 message,
370 }
371 }
372 });
373 }
374
375 #[derive(Deserialize)]
376 struct WorkflowRunsResponse {
377 workflow_runs: Vec<WorkflowRun>,
378 }
379
380 let runs_response: WorkflowRunsResponse = response.json().await.map_err(ApiError::from)?;
381 Ok(runs_response.workflow_runs)
382 }
383
384 /// Get a specific workflow run by ID.
385 ///
386 /// Retrieves details about a single workflow run.
387 ///
388 /// # Arguments
389 ///
390 /// * `owner` - Repository owner
391 /// * `repo` - Repository name
392 /// * `run_id` - Workflow run ID
393 ///
394 /// # Returns
395 ///
396 /// Returns the `WorkflowRun` with the specified ID.
397 ///
398 /// # Errors
399 ///
400 /// * `ApiError::NotFound` - Workflow run does not exist
401 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
402 ///
403 /// # Example
404 ///
405 /// ```no_run
406 /// # use github_bot_sdk::client::InstallationClient;
407 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
408 /// let run = client.get_workflow_run("owner", "repo", 987654).await?;
409 /// println!("Run #{}: {}", run.run_number, run.status);
410 /// # Ok(())
411 /// # }
412 /// ```
413 pub async fn get_workflow_run(
414 &self,
415 owner: &str,
416 repo: &str,
417 run_id: u64,
418 ) -> Result<WorkflowRun, ApiError> {
419 let path = format!("/repos/{}/{}/actions/runs/{}", owner, repo, run_id);
420 let response = self.get(&path).await?;
421
422 let status = response.status();
423 if !status.is_success() {
424 return Err(match status.as_u16() {
425 404 => ApiError::NotFound,
426 403 => ApiError::AuthorizationFailed,
427 401 => ApiError::AuthenticationFailed,
428 _ => {
429 let message = response
430 .text()
431 .await
432 .unwrap_or_else(|_| "Unknown error".to_string());
433 ApiError::HttpError {
434 status: status.as_u16(),
435 message,
436 }
437 }
438 });
439 }
440
441 response.json().await.map_err(ApiError::from)
442 }
443
444 /// Cancel a workflow run.
445 ///
446 /// Cancels a workflow run that is in progress.
447 ///
448 /// # Arguments
449 ///
450 /// * `owner` - Repository owner
451 /// * `repo` - Repository name
452 /// * `run_id` - Workflow run ID
453 ///
454 /// # Returns
455 ///
456 /// Returns `Ok(())` on successful cancellation.
457 ///
458 /// # Errors
459 ///
460 /// * `ApiError::NotFound` - Workflow run does not exist
461 /// * `ApiError::InvalidRequest` - Workflow run cannot be cancelled
462 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
463 ///
464 /// # Example
465 ///
466 /// ```no_run
467 /// # use github_bot_sdk::client::InstallationClient;
468 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
469 /// client.cancel_workflow_run("owner", "repo", 987654).await?;
470 /// println!("Workflow run cancelled");
471 /// # Ok(())
472 /// # }
473 /// ```
474 pub async fn cancel_workflow_run(
475 &self,
476 owner: &str,
477 repo: &str,
478 run_id: u64,
479 ) -> Result<(), ApiError> {
480 let path = format!("/repos/{}/{}/actions/runs/{}/cancel", owner, repo, run_id);
481 let response = self.post(&path, &serde_json::json!({})).await?;
482
483 let status = response.status();
484 if !status.is_success() {
485 return Err(match status.as_u16() {
486 404 => ApiError::NotFound,
487 403 => ApiError::AuthorizationFailed,
488 401 => ApiError::AuthenticationFailed,
489 422 => {
490 let message = response
491 .text()
492 .await
493 .unwrap_or_else(|_| "Validation error".to_string());
494 ApiError::InvalidRequest { message }
495 }
496 _ => {
497 let message = response
498 .text()
499 .await
500 .unwrap_or_else(|_| "Unknown error".to_string());
501 ApiError::HttpError {
502 status: status.as_u16(),
503 message,
504 }
505 }
506 });
507 }
508
509 Ok(())
510 }
511
512 /// Re-run a workflow run.
513 ///
514 /// Re-runs a completed workflow run.
515 ///
516 /// # Arguments
517 ///
518 /// * `owner` - Repository owner
519 /// * `repo` - Repository name
520 /// * `run_id` - Workflow run ID
521 ///
522 /// # Returns
523 ///
524 /// Returns `Ok(())` on successful re-run trigger.
525 ///
526 /// # Errors
527 ///
528 /// * `ApiError::NotFound` - Workflow run does not exist
529 /// * `ApiError::InvalidRequest` - Workflow run cannot be re-run
530 /// * `ApiError::AuthorizationFailed` - Insufficient permissions
531 ///
532 /// # Example
533 ///
534 /// ```no_run
535 /// # use github_bot_sdk::client::InstallationClient;
536 /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
537 /// client.rerun_workflow_run("owner", "repo", 987654).await?;
538 /// println!("Workflow run re-triggered");
539 /// # Ok(())
540 /// # }
541 /// ```
542 pub async fn rerun_workflow_run(
543 &self,
544 owner: &str,
545 repo: &str,
546 run_id: u64,
547 ) -> Result<(), ApiError> {
548 let path = format!("/repos/{}/{}/actions/runs/{}/rerun", owner, repo, run_id);
549 let response = self.post(&path, &serde_json::json!({})).await?;
550
551 let status = response.status();
552 if !status.is_success() {
553 return Err(match status.as_u16() {
554 404 => ApiError::NotFound,
555 403 => ApiError::AuthorizationFailed,
556 401 => ApiError::AuthenticationFailed,
557 422 => {
558 let message = response
559 .text()
560 .await
561 .unwrap_or_else(|_| "Validation error".to_string());
562 ApiError::InvalidRequest { message }
563 }
564 _ => {
565 let message = response
566 .text()
567 .await
568 .unwrap_or_else(|_| "Unknown error".to_string());
569 ApiError::HttpError {
570 status: status.as_u16(),
571 message,
572 }
573 }
574 });
575 }
576
577 Ok(())
578 }
579}
580
581#[cfg(test)]
582#[path = "workflow_tests.rs"]
583mod tests;