Commit 1d2aedc471

1d2aedc4715279096aae7387888973d08ba75d4d

parent: 00205e7daa

Unsigned

cmc <hello@cleberg.net> · 2026-03-19 00:34 UTC

stabilize build task identity and log cache keys

Layout: unified · split

Hutch/Models/Builds.swift +25 −2
@@ -34,10 +34,33 @@ enum TaskStatus: String, Codable, Sendable {
34 34
35/// A single task within a build job. 35/// A single task within a build job.
36struct BuildTask: Codable, Sendable, Identifiable { 36struct BuildTask: Codable, Sendable, Identifiable {
37 var id: String { name } 37 private(set) var ordinal: Int?
38 let name: String 38 let name: String
39 let status: TaskStatus 39 let status: TaskStatus
40 let log: BuildLog? 40 let log: BuildLog?
41
42 var id: String {
43 if let ordinal {
44 return "\(ordinal):\(name)"
45 }
46 return [name, log?.fullURL, status.rawValue]
47 .compactMap { $0 }
48 .joined(separator: "::")
49 }
50
51 var logCacheKey: String {
52 id
53 }
54
55 func withOrdinal(_ ordinal: Int) -> BuildTask {
56 var task = self
57 task.ordinal = ordinal
58 return task
59 }
60
61 private enum CodingKeys: String, CodingKey {
62 case name, status, log
63 }
41} 64}
42 65
43// MARK: - Job Summary (for list view) 66// MARK: - Job Summary (for list view)
@@ -90,7 +113,7 @@ struct JobDetail: Codable, Sendable {
90 let visibility: Visibility? 113 let visibility: Visibility?
91 let image: String? 114 let image: String?
92 let manifest: String? 115 let manifest: String?
93 let tasks: [BuildTask] 116 var tasks: [BuildTask]
94 let log: BuildLog? 117 let log: BuildLog?
95 let owner: Entity 118 let owner: Entity
96} 119}
Hutch/Views/Builds/BuildDetailView.swift +2 −2
@@ -312,13 +312,13 @@ private struct TaskLogSection: View {
312 312
313 var body: some View { 313 var body: some View {
314 DisclosureGroup(isExpanded: $isExpanded) { 314 DisclosureGroup(isExpanded: $isExpanded) {
315 if viewModel.loadingTaskLogs.contains(task.name) { 315 if viewModel.loadingTaskLogs.contains(task.logCacheKey) {
316 HStack { 316 HStack {
317 Spacer() 317 Spacer()
318 ProgressView("Loading log…") 318 ProgressView("Loading log…")
319 Spacer() 319 Spacer()
320 } 320 }
321 } else if let logText = viewModel.taskLogs[task.name] { 321 } else if let logText = viewModel.taskLogs[task.logCacheKey] {
322 ScrollView(.horizontal, showsIndicators: false) { 322 ScrollView(.horizontal, showsIndicators: false) {
323 Text(logText) 323 Text(logText)
324 .font(.caption2.monospaced()) 324 .font(.caption2.monospaced())
Hutch/Views/Builds/BuildDetailViewModel.swift +11 −6
@@ -104,7 +104,11 @@ final class BuildDetailViewModel {
104 variables: ["id": jobId], 104 variables: ["id": jobId],
105 responseType: JobDetailResponse.self 105 responseType: JobDetailResponse.self
106 ) 106 )
107 job = result.job 107 var loadedJob = result.job
108 loadedJob.tasks = loadedJob.tasks.enumerated().map { index, task in
109 task.withOrdinal(index)
110 }
111 job = loadedJob
108 } catch { 112 } catch {
109 self.error = error.localizedDescription 113 self.error = error.localizedDescription
110 } 114 }
@@ -113,19 +117,20 @@ final class BuildDetailViewModel {
113 } 117 }
114 118
115 func loadTaskLog(task: BuildTask) async { 119 func loadTaskLog(task: BuildTask) async {
120 let cacheKey = task.logCacheKey
116 guard let log = task.log, 121 guard let log = task.log,
117 let logURL = URL(string: log.fullURL), 122 let logURL = URL(string: log.fullURL),
118 !loadingTaskLogs.contains(task.name), 123 !loadingTaskLogs.contains(cacheKey),
119 taskLogs[task.name] == nil else { return } 124 taskLogs[cacheKey] == nil else { return }
120 loadingTaskLogs.insert(task.name) 125 loadingTaskLogs.insert(cacheKey)
121 126
122 do { 127 do {
123 taskLogs[task.name] = try await client.fetchText(url: logURL) 128 taskLogs[cacheKey] = try await client.fetchText(url: logURL)
124 } catch { 129 } catch {
125 self.error = error.localizedDescription 130 self.error = error.localizedDescription
126 } 131 }
127 132
128 loadingTaskLogs.remove(task.name) 133 loadingTaskLogs.remove(cacheKey)
129 } 134 }
130 135
131 func cancelJob() async { 136 func cancelJob() async {
HutchTests/BuildTaskTests.swift added +27
@@ -0,0 +1,27 @@
1import Foundation
2import Testing
3@testable import Hutch
4
5struct BuildTaskTests {
6
7 @Test
8 @MainActor
9 func duplicateTaskNamesProduceDistinctIDsAndLogCacheKeys() {
10 let firstTask = BuildTask(
11 name: "test",
12 status: .failed,
13 log: BuildLog(fullURL: "https://builds.sr.ht/job/1/task/1")
14 ).withOrdinal(0)
15
16 let secondTask = BuildTask(
17 name: "test",
18 status: .failed,
19 log: BuildLog(fullURL: "https://builds.sr.ht/job/1/task/2")
20 ).withOrdinal(1)
21
22 #expect(firstTask.id == "0:test")
23 #expect(secondTask.id == "1:test")
24 #expect(firstTask.id != secondTask.id)
25 #expect(firstTask.logCacheKey != secondTask.logCacheKey)
26 }
27}