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 #[serde(default = "resolvable_by_default")]
169 pub is_resolvable: bool,
170}
171
172fn resolvable_by_default() -> bool {
175 true
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum TimelineEventKind {
181 Approved,
182 ChangesRequested,
183 Reviewed,
184 Commented,
185 Merged,
186 Closed,
187 Reopened,
188 StateChanged,
189 Assigned,
190 Labeled,
191 Committed,
192 Other,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TimelineEvent {
200 pub actor: Option<User>,
201 pub kind: TimelineEventKind,
202 pub summary: String,
204 pub at: Option<DateTime<Utc>>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct Reviewer {
209 pub user: User,
210 pub vote: ReviewVote,
211 pub is_required: bool,
212}
213
214#[derive(Debug, Clone, Default, Serialize, Deserialize)]
215pub struct CheckSummary {
216 pub successful: u32,
217 pub in_progress: u32,
218 pub failed: u32,
219 pub neutral: u32,
220}
221
222impl CheckSummary {
223 pub fn total(&self) -> u32 {
224 self.successful + self.in_progress + self.failed + self.neutral
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct CheckRun {
231 pub name: String,
232 pub status: CheckStatus,
233 pub url: Option<String>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Commit {
239 pub sha: String,
240 pub message: String,
241 pub author: String,
242 pub date: Option<DateTime<Utc>>,
243 pub url: Option<String>,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
248pub enum DiffSide {
249 Old,
250 New,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct LineComment {
256 pub path: String,
257 pub line: i64,
258 pub side: DiffSide,
259 pub body: String,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct FileChange {
264 pub path: String,
265 pub kind: FileChangeKind,
266 pub additions: i64,
267 pub deletions: i64,
268 pub patch: Option<String>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct PullRequest {
274 pub id: String,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub repository: Option<String>,
279 pub number: Option<i64>,
280 pub title: String,
281 pub description: Option<String>,
282 pub author: User,
283 pub status: PullRequestStatus,
284 pub is_draft: bool,
285 pub source_ref: Option<String>,
286 pub target_ref: Option<String>,
287 pub reviewers: Vec<Reviewer>,
288 pub labels: Vec<String>,
289 pub checks: CheckStatus,
290 pub check_summary: Option<CheckSummary>,
291 pub mergeable: MergeableState,
292 pub changed_files: i64,
293 pub additions: i64,
294 pub deletions: i64,
295 pub created_at: Option<DateTime<Utc>>,
296 pub updated_at: Option<DateTime<Utc>>,
297 pub url: Option<String>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct WorkItem {
302 pub id: String,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub repository: Option<String>,
308 pub identifier: Option<String>,
309 pub title: String,
310 pub description: Option<String>,
311 pub state: String,
312 pub state_category: WorkItemStateCategory,
313 pub work_item_type: Option<String>,
314 pub assignee: Option<User>,
315 pub created_at: Option<DateTime<Utc>>,
316 pub updated_at: Option<DateTime<Utc>>,
317 pub url: Option<String>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct PipelineDefinition {
322 pub id: String,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub repository: Option<String>,
327 pub name: String,
328 pub path: Option<String>,
329 pub url: Option<String>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct PipelineStep {
334 pub name: String,
335 pub status: PipelineRunStatus,
336 pub started_at: Option<DateTime<Utc>>,
337 pub finished_at: Option<DateTime<Utc>>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct PipelineJob {
342 pub id: String,
343 pub name: String,
344 pub status: PipelineRunStatus,
345 pub started_at: Option<DateTime<Utc>>,
346 pub finished_at: Option<DateTime<Utc>>,
347 pub steps: Vec<PipelineStep>,
348 pub url: Option<String>,
350 pub problem: Option<String>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct PipelineStage {
357 pub name: String,
358 pub status: PipelineRunStatus,
359 pub jobs: Vec<PipelineJob>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct PipelineRun {
364 pub id: String,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub repository: Option<String>,
369 pub definition_id: String,
370 pub number: Option<i64>,
371 pub name: Option<String>,
372 pub title: Option<String>,
375 pub status: PipelineRunStatus,
376 pub triggered_by: Option<User>,
377 pub branch: Option<String>,
378 pub commit_sha: Option<String>,
379 pub started_at: Option<DateTime<Utc>>,
380 pub finished_at: Option<DateTime<Utc>>,
381 pub url: Option<String>,
382 pub stages: Vec<PipelineStage>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub event: Option<String>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub attempt: Option<u32>,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub pull_request: Option<i64>,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
399pub enum AnnotationLevel {
400 Failure,
401 Warning,
402 Notice,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct PipelineAnnotation {
409 pub level: AnnotationLevel,
410 pub message: String,
411 pub title: Option<String>,
413 pub path: Option<String>,
415 pub line: Option<u32>,
416 pub job_id: Option<String>,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct PipelineArtifact {
423 pub id: String,
424 pub name: String,
425 pub size_bytes: Option<u64>,
426 pub expires_at: Option<DateTime<Utc>>,
427 pub url: Option<String>,
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434pub struct PipelineApproval {
435 pub id: String,
438 pub name: String,
440 pub can_respond: bool,
442 #[serde(default = "holds_by_default")]
446 pub blocks_run: bool,
447}
448
449fn holds_by_default() -> bool {
450 true
451}
452
453impl PullRequest {
456 pub fn item_ref(&self) -> crate::provider::ItemRef {
457 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
458 }
459}
460
461impl WorkItem {
462 pub fn item_ref(&self) -> crate::provider::ItemRef {
463 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
464 }
465}
466
467impl PipelineRun {
468 pub fn item_ref(&self) -> crate::provider::ItemRef {
469 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
470 }
471
472 pub fn definition_ref(&self) -> crate::provider::ItemRef {
474 crate::provider::ItemRef::maybe(self.repository.clone(), self.definition_id.clone())
475 }
476
477 pub fn has_running_work(&self) -> bool {
480 let running = PipelineRunStatus::Running;
481 self.stages.iter().any(|s| {
482 s.status == running || s.jobs.iter().any(|j| j.status == running || j.steps.iter().any(|t| t.status == running))
483 })
484 }
485
486 pub fn shown_status(&self, blocking_gate: bool) -> PipelineRunStatus {
492 let held = blocking_gate || self.stages.iter().any(|s| s.status == PipelineRunStatus::Waiting);
493 self.status.with_pending_approval(held && !self.has_running_work())
494 }
495}
496
497impl PipelineDefinition {
498 pub fn item_ref(&self) -> crate::provider::ItemRef {
499 crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
500 }
501}
502
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
505pub enum ApprovalDecision {
506 Approve,
507 Reject,
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
512pub enum NotificationKind {
513 ReviewRequested,
515 Mention,
517 Assigned,
519 CiFailed,
521 Comment,
523 StateChange,
525 Other,
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
531pub enum NotificationItemType {
532 PullRequest,
533 WorkItem,
534 Pipeline,
535 Other,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
540pub struct Notification {
541 pub id: String,
543 pub kind: NotificationKind,
544 pub item_type: NotificationItemType,
545 pub item_id: Option<String>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub repository: Option<String>,
552 pub title: String,
554 pub context: String,
556 pub url: Option<String>,
558 pub unread: bool,
559 pub updated_at: Option<DateTime<Utc>>,
560}
561
562#[cfg(test)]
563mod pipeline_status_tests {
564 use super::PipelineRunStatus::{self, *};
565
566 #[test]
567 fn waiting_is_in_flight_and_skipped_is_not() {
568 for s in [Queued, Running, Waiting] {
569 assert!(s.is_active(), "{s:?}");
570 }
571 for s in [Succeeded, PartiallySucceeded, Failed, Canceled, Skipped] {
572 assert!(!s.is_active(), "{s:?}");
573 }
574 }
575
576 #[test]
577 fn a_pending_gate_holds_only_an_active_run() {
578 assert_eq!(Running.with_pending_approval(true), Waiting);
579 assert_eq!(Queued.with_pending_approval(true), Waiting);
580 assert_eq!(Running.with_pending_approval(false), Running);
581 assert_eq!(Succeeded.with_pending_approval(true), Succeeded);
583 assert_eq!(Failed.with_pending_approval(true), Failed);
584 }
585
586 fn run_with(status: PipelineRunStatus, stages: Vec<super::PipelineStage>) -> super::PipelineRun {
587 super::PipelineRun {
588 event: None, attempt: None, pull_request: None, repository: None, id: "1".into(), definition_id: "d".into(),
589 number: None, name: None, title: None, status, triggered_by: None, branch: None, commit_sha: None,
590 started_at: None, finished_at: None, url: None, stages,
591 }
592 }
593
594 fn stage(status: PipelineRunStatus) -> super::PipelineStage {
595 super::PipelineStage { name: "s".into(), status, jobs: vec![] }
596 }
597
598 #[test]
599 fn a_run_reads_waiting_only_when_a_gate_holds_it_and_nothing_runs() {
600 assert_eq!(run_with(Running, vec![]).shown_status(true), Waiting);
602 assert_eq!(run_with(Running, vec![]).shown_status(false), Running);
603 assert_eq!(run_with(Running, vec![stage(Succeeded), stage(Running), stage(Queued)]).shown_status(true), Running);
605 assert_eq!(run_with(Running, vec![stage(Succeeded), stage(Waiting)]).shown_status(false), Waiting);
607 assert_eq!(run_with(Canceled, vec![stage(Waiting)]).shown_status(true), Canceled);
609 }
610
611 #[test]
612 fn an_approval_from_an_older_cache_still_holds_its_run() {
613 let a: super::PipelineApproval = serde_json::from_str(r#"{ "id": "g", "name": "Prod", "can_respond": true }"#).unwrap();
614 assert!(a.blocks_run);
615 }
616
617 #[test]
618 fn new_states_round_trip_by_name() {
619 for s in [Waiting, Skipped] {
620 let json = serde_json::to_string(&s).unwrap();
621 assert_eq!(serde_json::from_str::<PipelineRunStatus>(&json).unwrap(), s);
622 }
623 assert_eq!(serde_json::to_string(&Waiting).unwrap(), "\"Waiting\"");
624 }
625}