gitbay/Builds/BuildModels.swift
87 lines · 2838 bytes
1import Foundation
2import SwiftUI
3
4/// One row of `build list`.
5nonisolated struct Build: Decodable, Sendable, Hashable, Identifiable {
6 let number: Int64
7 let job: String
8 let status: String
9 let sha: String
10 let ref: String
11 let createdAt: Date
12 let finishedAt: Date?
13 /// The built commit's subject, which `build list` resolves since
14 /// gitbay v1.33.0. Absent when the commit is gone.
15 let subject: String?
16 /// `build show` only (krz/gitbay#266). `failedStep` is 1-based and
17 /// absent when the runner named none.
18 var durationS: Int64? = nil
19 var steps: [String]? = nil
20 var failedStep: Int? = nil
21 var failedReason: String? = nil
22
23 enum CodingKeys: String, CodingKey {
24 case number, job, status, sha, ref, subject, steps
25 case createdAt = "created_at"
26 case finishedAt = "finished_at"
27 case durationS = "duration_s"
28 case failedStep = "failed_step"
29 case failedReason = "failed_reason"
30 }
31
32 var id: Int64 { number }
33 var shortSHA: String { String(sha.prefix(10)) }
34 /// What a row leads with. A sha alone does not say what was built
35 /// (krz/gitbay#241).
36 var title: String {
37 if let subject, !subject.isEmpty { return "#\(number) \(subject)" }
38 return "#\(number) \(shortSHA)"
39 }
40}
41
42/// `build jobs` — what a trigger can name. A repo with no CI config has
43/// none, which is an empty state rather than an error.
44nonisolated struct CIJob: Decodable, Sendable, Hashable, Identifiable {
45 let name: String
46 let schedule: String?
47 let tags: String?
48
49 var id: String { name }
50
51 /// Why this job runs, in the words the server uses.
52 var trigger: String {
53 if let schedule, !schedule.isEmpty { return "schedule \(schedule)" }
54 if let tags, !tags.isEmpty { return "tags \(tags)" }
55 return "on push"
56 }
57}
58
59extension Build {
60 /// The list row and the detail header must agree on what a status
61 /// looks like; the strings come from the registry.
62 var statusIcon: String {
63 switch status {
64 case "success": "checkmark.circle.fill"
65 case "failure", "error": "xmark.circle.fill"
66 case "running": "circle.dotted"
67 case "queued", "pending": "clock"
68 default: "questionmark.circle"
69 }
70 }
71
72 /// How long a build ran, as the web's "ran" puts it.
73 static func ran(seconds: Int64?) -> String? {
74 guard let seconds, seconds > 0 else { return nil }
75 return Duration.seconds(seconds)
76 .formatted(.units(allowed: [.hours, .minutes, .seconds], width: .narrow))
77 }
78
79 var statusColor: Color {
80 switch status {
81 case "success": .gbOK
82 case "failure", "error": .gbBad
83 case "running", "queued", "pending": .gbWarn
84 default: .secondary
85 }
86 }
87}