import Foundation import SwiftUI /// One row of `build list`. nonisolated struct Build: Decodable, Sendable, Hashable, Identifiable { let number: Int64 let job: String let status: String let sha: String let ref: String let createdAt: Date let finishedAt: Date? /// The built commit's subject, which `build list` resolves since /// gitbay v1.33.0. Absent when the commit is gone. let subject: String? /// `build show` only (krz/gitbay#266). `failedStep` is 1-based and /// absent when the runner named none. var durationS: Int64? = nil var steps: [String]? = nil var failedStep: Int? = nil var failedReason: String? = nil enum CodingKeys: String, CodingKey { case number, job, status, sha, ref, subject, steps case createdAt = "created_at" case finishedAt = "finished_at" case durationS = "duration_s" case failedStep = "failed_step" case failedReason = "failed_reason" } var id: Int64 { number } var shortSHA: String { String(sha.prefix(10)) } /// What a row leads with. A sha alone does not say what was built /// (krz/gitbay#241). var title: String { if let subject, !subject.isEmpty { return "#\(number) \(subject)" } return "#\(number) \(shortSHA)" } } /// `build jobs` — what a trigger can name. A repo with no CI config has /// none, which is an empty state rather than an error. nonisolated struct CIJob: Decodable, Sendable, Hashable, Identifiable { let name: String let schedule: String? let tags: String? var id: String { name } /// Why this job runs, in the words the server uses. var trigger: String { if let schedule, !schedule.isEmpty { return "schedule \(schedule)" } if let tags, !tags.isEmpty { return "tags \(tags)" } return "on push" } } extension Build { /// The list row and the detail header must agree on what a status /// looks like; the strings come from the registry. var statusIcon: String { switch status { case "success": "checkmark.circle.fill" case "failure", "error": "xmark.circle.fill" case "running": "circle.dotted" case "queued", "pending": "clock" default: "questionmark.circle" } } /// How long a build ran, as the web's "ran" puts it. static func ran(seconds: Int64?) -> String? { guard let seconds, seconds > 0 else { return nil } return Duration.seconds(seconds) .formatted(.units(allowed: [.hours, .minutes, .seconds], width: .narrow)) } var statusColor: Color { switch status { case "success": .gbOK case "failure", "error": .gbBad case "running", "queued", "pending": .gbWarn default: .secondary } } }