1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8pub enum Section {
9 #[default]
10 PullRequests,
11 WorkItems,
12 Pipelines,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum ProviderType {
18 Demo,
19 GitHub,
20 AzureDevOps,
21 Linear,
22 GitLab,
23 Bitbucket,
24 Jira,
25}
26
27impl ProviderType {
28 pub fn as_str(&self) -> &'static str {
29 match self {
30 ProviderType::Demo => "Demo",
31 ProviderType::GitHub => "GitHub",
32 ProviderType::AzureDevOps => "AzureDevOps",
33 ProviderType::Linear => "Linear",
34 ProviderType::GitLab => "GitLab",
35 ProviderType::Bitbucket => "Bitbucket",
36 ProviderType::Jira => "Jira",
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum PullRequestStatus {
43 Open,
44 Draft,
45 Merged,
46 Closed,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum ReviewVote {
52 Rejected,
53 WaitingForAuthor,
54 NoVote,
55 ApprovedWithSuggestions,
56 Approved,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum WorkItemStateCategory {
61 Triage,
62 Backlog,
63 Unstarted,
64 Started,
65 Completed,
66 Canceled,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum PipelineRunStatus {
71 Queued,
72 Running,
73 Waiting,
76 Succeeded,
77 PartiallySucceeded,
78 Failed,
79 Canceled,
80 Skipped,
83}
84
85impl PipelineRunStatus {
86 pub fn is_active(self) -> bool {
89 matches!(self, Self::Queued | Self::Running | Self::Waiting)
90 }
91
92 pub fn with_pending_approval(self, pending: bool) -> Self {
95 if pending && self.is_active() {
96 Self::Waiting
97 } else {
98 self
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
105pub enum CheckStatus {
106 #[default]
107 None,
108 Pending,
109 Passed,
110 Failed,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
115pub enum MergeableState {
116 #[default]
117 Unknown,
118 Mergeable,
119 Blocked,
120 Conflicting,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum FileChangeKind {
125 Added,
126 Modified,
127 Deleted,
128 Renamed,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct User {
135 pub id: String,
136 pub display_name: String,
137 pub handle: Option<String>,
138 pub avatar_url: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct Repository {
143 pub id: String,
144 pub name: String,
145 pub full_name: Option<String>,
146 pub default_branch: Option<String>,
147 pub url: Option<String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct Comment {
152 pub id: String,
153 pub author: User,
154 pub body: String,
155 pub created_at: Option<DateTime<Utc>>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct CommentThread {
160 pub id: String,
161 pub comments: Vec<Comment>,
162 pub file_path: Option<String>,
163 pub line: Option<i64>,
164 pub is_resolved: bool,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169pub enum TimelineEventKind {
170 Approved,
171 ChangesRequested,
172 Reviewed,
173 Commented,
174 Merged,
175 Closed,
176 Reopened,
177 StateChanged,
178 Assigned,
179 Labeled,
180 Committed,
181 Other,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct TimelineEvent {
189 pub actor: Option<User>,
190 pub kind: TimelineEventKind,
191 pub summary: String,
193 pub at: Option<DateTime<Utc>>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct Reviewer {
198 pub user: User,
199 pub vote: ReviewVote,
200 pub is_required: bool,
201}
202
203#[derive(Debug, Clone, Default, Serialize, Deserialize)]
204pub struct CheckSummary {
205 pub successful: u32,
206 pub in_progress: u32,
207 pub failed: u32,
208 pub neutral: u32,
209}
210
211impl CheckSummary {
212 pub fn total(&self) -> u32 {
213 self.successful + self.in_progress + self.failed + self.neutral
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct CheckRun {
220 pub name: String,
221 pub status: CheckStatus,
222 pub url: Option<String>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct Commit {
228 pub sha: String,
229 pub message: String,
230 pub author: String,
231 pub date: Option<DateTime<Utc>>,
232 pub url: Option<String>,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
237pub enum DiffSide {
238 Old,
239 New,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct LineComment {
245 pub path: String,
246 pub line: i64,
247 pub side: DiffSide,
248 pub body: String,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct FileChange {
253 pub path: String,
254 pub kind: FileChangeKind,
255 pub additions: i64,
256 pub deletions: i64,
257 pub patch: Option<String>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct PullRequest {
263 pub id: String,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub repository: Option<String>,
268 pub number: Option<i64>,
269 pub title: String,
270 pub description: Option<String>,
271 pub author: User,
272 pub status: PullRequestStatus,
273 pub is_draft: bool,
274 pub source_ref: Option<String>,
275 pub target_ref: Option<String>,
276 pub reviewers: Vec<Reviewer>,
277 pub labels: Vec<String>,
278 pub checks: CheckStatus,
279 pub check_summary: Option<CheckSummary>,
280 pub mergeable: MergeableState,
281 pub changed_files: i64,
282 pub additions: i64,
283 pub deletions: i64,
284 pub created_at: Option<DateTime<Utc>>,
285 pub updated_at: Option<DateTime<Utc>>,
286 pub url: Option<String>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct WorkItem {
291 pub id: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub repository: Option<String>,
297 pub identifier: Option<String>,
298 pub title: String,
299 pub description: Option<String>,
300 pub state: String,
301 pub state_category: WorkItemStateCategory,
302 pub work_item_type: Option<String>,
303 pub assignee: Option<User>,
304 pub created_at: Option<DateTime<Utc>>,
305 pub updated_at: Option<DateTime<Utc>>,
306 pub url: Option<String>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct PipelineDefinition {
311 pub id: String,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub repository: Option<String>,
316 pub name: String,
317 pub path: Option<String>,
318 pub url: Option<String>,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct PipelineStep {
323 pub name: String,
324 pub status: PipelineRunStatus,
325 pub started_at: Option<DateTime<Utc>>,
326 pub finished_at: Option<DateTime<Utc>>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct PipelineJob {
331 pub id: String,
332 pub name: String,
333 pub status: PipelineRunStatus,
334 pub started_at: Option<DateTime<Utc>>,
335 pub finished_at: Option<DateTime<Utc>>,
336 pub steps: Vec<PipelineStep>,
337 pub url: Option<String>,
339 pub problem: Option<String>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct PipelineStage {
346 pub name: String,
347 pub status: PipelineRunStatus,
348 pub jobs: Vec<PipelineJob>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct PipelineRun {
353 pub id: String,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub repository: Option<String>,
358 pub definition_id: String,
359 pub number: Option<i64>,
360 pub name: Option<String>,
361 pub title: Option<String>,
364 pub status: PipelineRunStatus,
365 pub triggered_by: Option<User>,
366 pub branch: Option<String>,
367 pub commit_sha: Option<String>,
368 pub started_at: Option<DateTime<Utc>>,
369 pub finished_at: Option<DateTime<Utc>>,
370 pub url: Option<String>,
371 pub stages: Vec<PipelineStage>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub event: Option<String>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub attempt: Option<u32>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub pull_request: Option<i64>,
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
388pub enum AnnotationLevel {
389 Failure,
390 Warning,
391 Notice,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct PipelineAnnotation {
398 pub level: AnnotationLevel,
399 pub message: String,
400 pub title: Option<String>,
402 pub path: Option<String>,
404 pub line: Option<u32>,
405 pub job_id: Option<String>,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct PipelineArtifact {
412 pub id: String,
413 pub name: String,
414 pub size_bytes: Option<u64>,
415 pub expires_at: Option<DateTime<Utc>>,
416 pub url: Option<String>,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423pub struct PipelineApproval {
424 pub id: String,
427 pub name: String,
429 pub can_respond: bool,
431 #[serde(default = "holds_by_default")]
435 pub blocks_run: bool,
436}
437
438fn holds_by_default() -> bool {
439 true
440}
441
442impl PullRequest {
445 pub fn item_ref(&self) -> crate::provider::ItemRef {
446 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
447 }
448}
449
450impl WorkItem {
451 pub fn item_ref(&self) -> crate::provider::ItemRef {
452 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
453 }
454}
455
456impl PipelineRun {
457 pub fn item_ref(&self) -> crate::provider::ItemRef {
458 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
459 }
460
461 pub fn definition_ref(&self) -> crate::provider::ItemRef {
463 crate::provider::ItemRef::maybe(self.repository.clone(), self.definition_id.clone())
464 }
465
466 pub fn has_running_work(&self) -> bool {
469 let running = PipelineRunStatus::Running;
470 self.stages.iter().any(|s| {
471 s.status == running || s.jobs.iter().any(|j| j.status == running || j.steps.iter().any(|t| t.status == running))
472 })
473 }
474
475 pub fn shown_status(&self, blocking_gate: bool) -> PipelineRunStatus {
481 let held = blocking_gate || self.stages.iter().any(|s| s.status == PipelineRunStatus::Waiting);
482 self.status.with_pending_approval(held && !self.has_running_work())
483 }
484}
485
486impl PipelineDefinition {
487 pub fn item_ref(&self) -> crate::provider::ItemRef {
488 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
489 }
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
494pub enum ApprovalDecision {
495 Approve,
496 Reject,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
501pub enum NotificationKind {
502 ReviewRequested,
504 Mention,
506 Assigned,
508 CiFailed,
510 Comment,
512 StateChange,
514 Other,
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
520pub enum NotificationItemType {
521 PullRequest,
522 WorkItem,
523 Pipeline,
524 Other,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529pub struct Notification {
530 pub id: String,
532 pub kind: NotificationKind,
533 pub item_type: NotificationItemType,
534 pub item_id: Option<String>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub repository: Option<String>,
541 pub title: String,
543 pub context: String,
545 pub url: Option<String>,
547 pub unread: bool,
548 pub updated_at: Option<DateTime<Utc>>,
549}
550
551#[cfg(test)]
552mod pipeline_status_tests {
553 use super::PipelineRunStatus::{self, *};
554
555 #[test]
556 fn waiting_is_in_flight_and_skipped_is_not() {
557 for s in [Queued, Running, Waiting] {
558 assert!(s.is_active(), "{s:?}");
559 }
560 for s in [Succeeded, PartiallySucceeded, Failed, Canceled, Skipped] {
561 assert!(!s.is_active(), "{s:?}");
562 }
563 }
564
565 #[test]
566 fn a_pending_gate_holds_only_an_active_run() {
567 assert_eq!(Running.with_pending_approval(true), Waiting);
568 assert_eq!(Queued.with_pending_approval(true), Waiting);
569 assert_eq!(Running.with_pending_approval(false), Running);
570 assert_eq!(Succeeded.with_pending_approval(true), Succeeded);
572 assert_eq!(Failed.with_pending_approval(true), Failed);
573 }
574
575 fn run_with(status: PipelineRunStatus, stages: Vec<super::PipelineStage>) -> super::PipelineRun {
576 super::PipelineRun {
577 event: None, attempt: None, pull_request: None, repository: None, id: "1".into(), definition_id: "d".into(),
578 number: None, name: None, title: None, status, triggered_by: None, branch: None, commit_sha: None,
579 started_at: None, finished_at: None, url: None, stages,
580 }
581 }
582
583 fn stage(status: PipelineRunStatus) -> super::PipelineStage {
584 super::PipelineStage { name: "s".into(), status, jobs: vec![] }
585 }
586
587 #[test]
588 fn a_run_reads_waiting_only_when_a_gate_holds_it_and_nothing_runs() {
589 assert_eq!(run_with(Running, vec![]).shown_status(true), Waiting);
591 assert_eq!(run_with(Running, vec![]).shown_status(false), Running);
592 assert_eq!(run_with(Running, vec![stage(Succeeded), stage(Running), stage(Queued)]).shown_status(true), Running);
594 assert_eq!(run_with(Running, vec![stage(Succeeded), stage(Waiting)]).shown_status(false), Waiting);
596 assert_eq!(run_with(Canceled, vec![stage(Waiting)]).shown_status(true), Canceled);
598 }
599
600 #[test]
601 fn an_approval_from_an_older_cache_still_holds_its_run() {
602 let a: super::PipelineApproval = serde_json::from_str(r#"{ "id": "g", "name": "Prod", "can_respond": true }"#).unwrap();
603 assert!(a.blocks_run);
604 }
605
606 #[test]
607 fn new_states_round_trip_by_name() {
608 for s in [Waiting, Skipped] {
609 let json = serde_json::to_string(&s).unwrap();
610 assert_eq!(serde_json::from_str::<PipelineRunStatus>(&json).unwrap(), s);
611 }
612 assert_eq!(serde_json::to_string(&Waiting).unwrap(), "\"Waiting\"");
613 }
614}