summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 21:17:24 -0500
committerChristian Cleberg <[email protected]>2026-04-12 21:17:24 -0500
commit7548bfb595d853c682653c0dc21bb3bf17af80d5 (patch)
treed5bc8ac6e70a1666439fb551459f3b36d8a566b3
parentd89f2d52b623aad98df687865b263adeee7f5de9 (diff)
downloadhutch-7548bfb595d853c682653c0dc21bb3bf17af80d5.tar.gz
hutch-7548bfb595d853c682653c0dc21bb3bf17af80d5.tar.bz2
hutch-7548bfb595d853c682653c0dc21bb3bf17af80d5.zip
feat: add build artifact listing
Implements: https://todo.sr.ht/~ccleberg/hutch/31
-rw-r--r--Hutch/Models/Builds.swift19
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift63
-rw-r--r--Hutch/Views/Builds/BuildDetailViewModel.swift2
3 files changed, 84 insertions, 0 deletions
diff --git a/Hutch/Models/Builds.swift b/Hutch/Models/Builds.swift
index 8de4235..d666398 100644
--- a/Hutch/Models/Builds.swift
+++ b/Hutch/Models/Builds.swift
@@ -122,10 +122,29 @@ struct JobDetail: Codable, Sendable, Equatable {
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 {
+ let pathComponents = path.split(separator: "/")
+ return pathComponents.last.map(String.init) ?? path
+ }
+
+ var isDownloadable: Bool {
+ url != nil
+ }
+}
+
/// The log associated with a build job.
struct BuildLog: Codable, Sendable, Equatable {
let fullURL: String
diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift
index 2f0bc6a..1ec9f64 100644
--- a/Hutch/Views/Builds/BuildDetailView.swift
+++ b/Hutch/Views/Builds/BuildDetailView.swift
@@ -4,6 +4,7 @@ struct BuildDetailView: View {
let jobId: Int
@Environment(AppState.self) private var appState
+ @Environment(\.openURL) private var openURL
@State private var viewModel: BuildDetailViewModel?
@State private var rebuiltJobId: Int?
@State private var selectedTaskName: String?
@@ -181,6 +182,23 @@ struct BuildDetailView: View {
}
}
+ if !job.artifacts.isEmpty {
+ Section {
+ ForEach(job.artifacts) { artifact in
+ BuildArtifactRow(artifact: artifact) {
+ guard let url = artifact.url else { return }
+ openURL(url)
+ }
+ }
+ } header: {
+ Text("Artifacts")
+ } footer: {
+ if job.artifacts.contains(where: { !$0.isDownloadable }) {
+ Text("Artifacts without a download URL are no longer available for download.")
+ }
+ }
+ }
+
// Per-task logs
if !job.tasks.isEmpty {
ForEach(job.tasks) { task in
@@ -285,6 +303,51 @@ struct BuildDetailView: View {
}
}
+private struct BuildArtifactRow: View {
+ let artifact: BuildArtifact
+ let onDownload: () -> Void
+
+ var body: some View {
+ HStack(alignment: .top, spacing: 12) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(artifact.filename)
+ .font(.subheadline.weight(.medium))
+
+ if artifact.path != artifact.filename {
+ Text(artifact.path)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .textSelection(.enabled)
+ }
+
+ Text(metadataText)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer(minLength: 12)
+
+ Button {
+ onDownload()
+ } label: {
+ Image(systemName: artifact.isDownloadable ? "arrow.down.circle" : "archivebox")
+ .imageScale(.large)
+ }
+ .disabled(!artifact.isDownloadable)
+ .accessibilityLabel(artifact.isDownloadable ? "Download \(artifact.filename)" : "\(artifact.filename) is unavailable")
+ }
+ }
+
+ private var metadataText: String {
+ var parts = [artifact.size.formattedByteCount]
+ parts.append("Created \(artifact.created.relativeDescription)")
+ if !artifact.isDownloadable {
+ parts.append("Unavailable")
+ }
+ return parts.joined(separator: " • ")
+ }
+}
+
private struct EditResubmitBuildSheet: View {
let viewModel: BuildDetailViewModel
let job: JobDetail
diff --git a/Hutch/Views/Builds/BuildDetailViewModel.swift b/Hutch/Views/Builds/BuildDetailViewModel.swift
index 803ab33..9566e0d 100644
--- a/Hutch/Views/Builds/BuildDetailViewModel.swift
+++ b/Hutch/Views/Builds/BuildDetailViewModel.swift
@@ -86,6 +86,7 @@ final class BuildDetailViewModel {
image
manifest
tasks { name status log { fullURL } }
+ artifacts { id created path size url }
log { fullURL }
owner { canonicalName }
}
@@ -237,6 +238,7 @@ final class BuildDetailViewModel {
status: .cancelled, note: job.note, tags: job.tags,
visibility: job.visibility, image: job.image,
manifest: job.manifest, tasks: job.tasks,
+ artifacts: job.artifacts,
log: job.log, owner: job.owner
)
stopAutoRefresh()