import Foundation struct ReleaseInfo: Identifiable { let id = UUID() let version: VersionNumber let versionString: String let notes: String let downloadURL: URL let releaseURL: URL let prerelease: Bool } struct VersionNumber: Comparable, CustomStringConvertible { let rawValue: String private let components: [Int] init(_ raw: String) { self.rawValue = raw self.components = raw .split(separator: ".") .map { Int($0) ?? 0 } } var description: String { rawValue } static func < (lhs: VersionNumber, rhs: VersionNumber) -> Bool { let maxCount = max(lhs.components.count, rhs.components.count) for index in 0.. ReleaseInfo { let releasesURL = baseURL .appendingPathComponent("repos") .appendingPathComponent(owner) .appendingPathComponent(repo) .appendingPathComponent("releases") var request = URLRequest(url: releasesURL) request.addValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { throw UpdateServiceError.invalidResponse } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let releases = try decoder.decode([APIRelease].self, from: data) let filtered = releases .filter { !$0.draft } .filter { includePrerelease || !$0.prerelease } .sorted(by: { $0.createdAt > $1.createdAt }) guard let release = filtered.first else { throw UpdateServiceError.noReleasesAvailable } guard let asset = preferredAsset(from: release.assets), let downloadURL = asset.browserDownloadURL else { throw UpdateServiceError.missingDownloadAsset } let cleanTag = release.tagName.trimmingCharacters(in: .whitespacesAndNewlines) let version = cleanTag.hasPrefix("v") ? String(cleanTag.dropFirst()) : cleanTag return ReleaseInfo( version: VersionNumber(version), versionString: version, notes: release.body ?? "", downloadURL: downloadURL, releaseURL: release.htmlURL ?? releasesURL, prerelease: release.prerelease ) } private func preferredAsset(from assets: [APIRelease.Asset]) -> APIRelease.Asset? { return assets .sorted { lhs, rhs in priority(for: lhs.name) > priority(for: rhs.name) } .first(where: { $0.browserDownloadURL != nil }) } private func priority(for name: String) -> Int { if name.lowercased().hasSuffix(".dmg") { return 3 } if name.lowercased().hasSuffix(".zip") { return 2 } return 1 } }