1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
import Foundation
// MARK: - Enums
/// Status of a build job.
enum JobStatus: String, Codable, Sendable {
case pending = "PENDING"
case queued = "QUEUED"
case running = "RUNNING"
case success = "SUCCESS"
case failed = "FAILED"
case cancelled = "CANCELLED"
case timeout = "TIMEOUT"
/// Whether the job can be cancelled.
var isCancellable: Bool {
switch self {
case .pending, .queued, .running: true
default: false
}
}
/// Whether the job has reached a terminal state and no longer changes.
var isTerminal: Bool {
switch self {
case .success, .failed, .cancelled, .timeout: true
case .pending, .queued, .running: false
}
}
}
/// Status of a single build task within a job.
enum TaskStatus: String, Codable, Sendable {
case pending = "PENDING"
case running = "RUNNING"
case success = "SUCCESS"
case failed = "FAILED"
case skipped = "SKIPPED"
}
// MARK: - Build Task
/// A single task within a build job.
struct BuildTask: Codable, Sendable, Identifiable, Equatable {
private(set) var ordinal: Int?
let name: String
let status: TaskStatus
let log: BuildLog?
var id: String {
if let ordinal {
return "\(ordinal):\(name)"
}
return [name, log?.fullURL, status.rawValue]
.compactMap { $0 }
.joined(separator: "::")
}
var logCacheKey: String {
id
}
func withOrdinal(_ ordinal: Int) -> BuildTask {
var task = self
task.ordinal = ordinal
return task
}
private enum CodingKeys: String, CodingKey {
case name, status, log
}
}
// MARK: - Job Summary (for list view)
/// Lightweight job model matching the fields returned by the jobs list query.
struct JobSummary: Codable, Sendable, Identifiable, Hashable {
let id: Int
let created: Date
let updated: Date
let status: JobStatus
let note: String?
let tags: [String]
let visibility: Visibility?
let image: String?
let tasks: [JobTaskSummary]
/// Number of completed (success) tasks.
var completedTaskCount: Int {
tasks.filter { $0.status == .success }.count
}
/// Display label: note if available, otherwise tags joined.
var displayLabel: String {
if let note, !note.isEmpty {
return note
}
if !tags.isEmpty {
return tags.joined(separator: ", ")
}
return "Job #\(id)"
}
}
/// Minimal task info for the list query.
struct JobTaskSummary: Codable, Sendable, Hashable {
let name: String
let status: TaskStatus
}
// MARK: - Job Detail (for detail view)
/// Full job model with all fields for the detail view.
struct JobDetail: Codable, Sendable, Equatable {
let id: Int
let created: Date
let updated: Date
let status: JobStatus
let note: String?
let tags: [String]
let visibility: Visibility?
let image: String?
let manifest: String?
var tasks: [BuildTask]
let artifacts: [BuildArtifact]
let log: BuildLog?
let owner: Entity
}
/// A downloadable artifact emitted by a build job.
struct BuildArtifact: Codable, Sendable, Identifiable, Equatable {
let id: Int
let created: Date
let path: String
let size: Int
let url: URL?
var filename: String {
(path as NSString).lastPathComponent
}
var isDownloadable: Bool {
url != nil
}
}
/// The log associated with a build job.
struct BuildLog: Codable, Sendable, Equatable {
let fullURL: String
}
// MARK: - Job Group
/// A group of related build jobs.
struct JobGroup: Codable, Sendable, Identifiable {
let id: Int
let created: Date
let note: String?
let jobs: [JobSummary]
}
|