diff --git a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md index e68b26d7..74af4da5 100644 --- a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md +++ b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md @@ -61,8 +61,7 @@ Likewise, the latest snapshot associated with a given development branch can be **--token=\:** -*A GitHub authentiation token to use for any GitHub API requests.* - +*A GitHub authentication token to use for any GitHub API requests.* This is useful to avoid GitHub's low rate limits. If an installation fails with an "unauthorized" status code, it likely means the rate limit has been hit. @@ -93,6 +92,51 @@ written to this file as commands that can be run after the installation. +## list-available + +List toolchains available for install. + +``` +swiftly list-available [] [--version] [--help] +``` + +**toolchain-selector:** + +*A filter to use when listing toolchains.* + + +The toolchain selector determines which toolchains to list. If no selector is provided, all available release toolchains will be listed: + + $ swiftly list-available + +The available toolchains associated with a given major version can be listed by specifying the major version as the selector: + + $ swiftly list-available 5 + +Likewise, the available toolchains associated with a given minor version can be listed by specifying the minor version as the selector: + + $ swiftly list-available 5.2 + +The installed snapshots for a given devlopment branch can be listed by specifying the branch as the selector: + + $ swiftly list-available main-snapshot + $ swiftly list-available 6.0-snapshot + +Note that listing available snapshots before 6.0 is unsupported. + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + ## use Set the active toolchain. If no toolchain is provided, print the currently in-use toolchain, if any. diff --git a/Package.swift b/Package.swift index 2c9a3e84..ec58b627 100644 --- a/Package.swift +++ b/Package.swift @@ -93,7 +93,6 @@ let package = Package( dependencies: ["Swiftly"], exclude: ghApiCacheExcludedResources, resources: ghApiCacheResources + [ - .embedInCode("gh-api-cache/swift-releases-page1.json"), .embedInCode("mock-signing-key-private.pgp"), ] ), diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index 7a96a5ec..776b723a 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -436,15 +436,15 @@ public struct Linux: Platform { switch choice { case "1": - return PlatformDefinition(name: "ubuntu2204", nameFull: "ubuntu22.04", namePretty: "Ubuntu 22.04") + return PlatformDefinition.ubuntu2204 case "2": - return PlatformDefinition(name: "ubuntu2004", nameFull: "ubuntu20.04", namePretty: "Ubuntu 20.04") + return PlatformDefinition.ubuntu2004 case "3": - return PlatformDefinition(name: "ubuntu1804", nameFull: "ubuntu18.04", namePretty: "Ubuntu 18.04") + return PlatformDefinition.ubuntu1804 case "4": - return PlatformDefinition(name: "ubi9", nameFull: "ubi9", namePretty: "RHEL 9") + return PlatformDefinition.rhel9 case "5": - return PlatformDefinition(name: "amazonlinux2", nameFull: "amazonlinux2", namePretty: "Amazon Linux 2") + return PlatformDefinition.amazonlinux2 default: fatalError("Installation canceled") } @@ -455,15 +455,15 @@ public struct Linux: Platform { if let platform = platform { switch platform { case "ubuntu22.04": - return PlatformDefinition(name: "ubuntu2204", nameFull: "ubuntu22.04", namePretty: "Ubuntu 22.04") + return PlatformDefinition.ubuntu2204 case "ubuntu20.04": - return PlatformDefinition(name: "ubuntu2004", nameFull: "ubuntu20.04", namePretty: "Ubuntu 20.04") + return PlatformDefinition.ubuntu2004 case "ubuntu18.04": - return PlatformDefinition(name: "ubuntu1804", nameFull: "ubuntu18.04", namePretty: "Ubuntu 18.04") + return PlatformDefinition.ubuntu1804 case "amazonlinux2": - return PlatformDefinition(name: "amazonlinux2", nameFull: "amazonlinux2", namePretty: "Amazon Linux 2") + return PlatformDefinition.amazonlinux2 case "rhel9": - return PlatformDefinition(name: "ubi9", nameFull: "ubi9", namePretty: "RHEL 9") + return PlatformDefinition.rhel9 default: fatalError("Unrecognized platform \(platform)") } diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index a7c0dd2f..978cab8b 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -208,7 +208,7 @@ public struct MacOS: Platform { public func detectPlatform(disableConfirmation _: Bool, platform _: String?) async -> PlatformDefinition { // No special detection required on macOS platform - PlatformDefinition(name: "xcode", nameFull: "osx", namePretty: "macOS") + PlatformDefinition.macOS } public func getShell() async throws -> String { diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index 63f82c1d..25d8f095 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -47,9 +47,8 @@ struct Install: SwiftlyCommand { var use: Bool = false @Option(help: ArgumentHelp( - "A GitHub authentiation token to use for any GitHub API requests.", + "A GitHub authentication token to use for any GitHub API requests.", discussion: """ - This is useful to avoid GitHub's low rate limits. If an installation fails with an \"unauthorized\" status code, it likely means the rate limit has been hit. """ @@ -76,9 +75,9 @@ struct Install: SwiftlyCommand { try validateSwiftly() let selector = try ToolchainSelector(parsing: self.version) - SwiftlyCore.httpClient.githubToken = self.token - let toolchainVersion = try await self.resolve(selector: selector) var config = try Config.load() + SwiftlyCore.httpClient.githubToken = self.token + let toolchainVersion = try await self.resolve(config: config, selector: selector) let postInstallScript = try await Self.execute( version: toolchainVersion, &config, @@ -227,12 +226,12 @@ struct Install: SwiftlyCommand { /// Utilize the GitHub API along with the provided selector to select a toolchain for install. /// TODO: update this to use an official swift.org API - func resolve(selector: ToolchainSelector) async throws -> ToolchainVersion { + func resolve(config: Config, selector: ToolchainSelector) async throws -> ToolchainVersion { switch selector { case .latest: SwiftlyCore.print("Fetching the latest stable Swift release...") - guard let release = try await SwiftlyCore.httpClient.getReleaseToolchains(limit: 1).first else { + guard let release = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1).first else { throw Error(message: "couldn't get latest releases") } return .stable(release) @@ -251,7 +250,7 @@ struct Install: SwiftlyCommand { SwiftlyCore.print("Fetching the latest stable Swift \(major).\(minor) release...") // If a patch was not provided, perform a lookup to get the latest patch release // of the provided major/minor version pair. - let toolchain = try await SwiftlyCore.httpClient.getReleaseToolchains(limit: 1) { release in + let toolchain = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in release.major == major && release.minor == minor }.first @@ -269,7 +268,7 @@ struct Install: SwiftlyCommand { SwiftlyCore.print("Fetching the latest \(branch) branch snapshot...") // If a date was not provided, perform a lookup to find the most recent snapshot // for the given branch. - let snapshot = try await SwiftlyCore.httpClient.getSnapshotToolchains(limit: 1) { snapshot in + let snapshot = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch, limit: 1) { snapshot in snapshot.branch == branch }.first diff --git a/Sources/Swiftly/ListAvailable.swift b/Sources/Swiftly/ListAvailable.swift index d56fe2d8..55f8ce08 100644 --- a/Sources/Swiftly/ListAvailable.swift +++ b/Sources/Swiftly/ListAvailable.swift @@ -11,7 +11,7 @@ struct ListAvailable: SwiftlyCommand { discussion: """ The toolchain selector determines which toolchains to list. If no selector \ - is provided, all available toolchains will be listed: + is provided, all available release toolchains will be listed: $ swiftly list-available @@ -28,9 +28,9 @@ struct ListAvailable: SwiftlyCommand { The installed snapshots for a given devlopment branch can be listed by specifying the branch as the selector: $ swiftly list-available main-snapshot - $ swiftly list-available 5.7-snapshot + $ swiftly list-available 6.0-snapshot - Note that listing available snapshots is currently unsupported. It will be introduced in a future release. + Note that listing available snapshots before 6.0 is unsupported. """ )) var toolchainSelector: String? @@ -45,18 +45,24 @@ struct ListAvailable: SwiftlyCommand { try ToolchainSelector(parsing: input) } - if let selector { - guard !selector.isSnapshotSelector() else { - SwiftlyCore.print("Listing available snapshots is not currently supported.") - return + let config = try Config.load() + + let tc: [ToolchainVersion] + + switch selector { + case let .snapshot(branch, _): + if case let .release(major, _) = branch, major < 6 { + throw Error(message: "Listing available snapshots previous to 6.0 is not supported.") } - } - let toolchains = try await SwiftlyCore.httpClient.getReleaseToolchains() - .map(ToolchainVersion.stable) - .filter { selector?.matches(toolchain: $0) ?? true } + tc = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch).map { ToolchainVersion.snapshot($0) } + case .stable, .latest: + tc = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) } + default: + tc = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) } + } - let config = try Config.load() + let toolchains = tc.filter { selector?.matches(toolchain: $0) ?? true } let installedToolchains = Set(config.listInstalledToolchains(selector: selector)) let activeToolchain = config.inUse @@ -100,13 +106,6 @@ struct ListAvailable: SwiftlyCommand { for toolchain in toolchains where toolchain.isStableRelease() { printToolchain(toolchain) } - - // print("") - // print("Available snapshot toolchains") - // print("-----------------------------") - // for toolchain in toolchains where toolchain.isSnapshot() { - // printToolchain(toolchain) - // } } } } diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index ce141b37..c65d1d7e 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -23,6 +23,7 @@ public struct Swiftly: SwiftlyCommand { subcommands: [ Install.self, + ListAvailable.self, Use.self, Uninstall.self, List.self, diff --git a/Sources/Swiftly/Update.swift b/Sources/Swiftly/Update.swift index 4ba2e3a9..6d958ca2 100644 --- a/Sources/Swiftly/Update.swift +++ b/Sources/Swiftly/Update.swift @@ -90,7 +90,7 @@ struct Update: SwiftlyCommand { return } - guard let newToolchain = try await self.lookupNewToolchain(parameters) else { + guard let newToolchain = try await self.lookupNewToolchain(config, parameters) else { SwiftlyCore.print("\(parameters.oldToolchain) is already up to date") return } @@ -181,10 +181,10 @@ struct Update: SwiftlyCommand { /// Tries to find a toolchain version that meets the provided parameters, if one exists. /// This does not download the toolchain, but it does query the GitHub API to find the suitable toolchain. - private func lookupNewToolchain(_ bounds: UpdateParameters) async throws -> ToolchainVersion? { + private func lookupNewToolchain(_ config: Config, _ bounds: UpdateParameters) async throws -> ToolchainVersion? { switch bounds { case let .stable(old, range): - return try await SwiftlyCore.httpClient.getReleaseToolchains(limit: 1) { release in + return try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in switch range { case .latest: return release > old @@ -195,7 +195,7 @@ struct Update: SwiftlyCommand { } }.first.map(ToolchainVersion.stable) case let .snapshot(old): - return try await SwiftlyCore.httpClient.getSnapshotToolchains(limit: 1) { snapshot in + return try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: old.branch, limit: 1) { snapshot in snapshot.branch == old.branch && snapshot.date > old.date }.first.map(ToolchainVersion.snapshot) } diff --git a/Sources/SwiftlyCore/HTTPClient+GitHubAPI.swift b/Sources/SwiftlyCore/HTTPClient+GitHubAPI.swift index 6620e0af..376c61a5 100644 --- a/Sources/SwiftlyCore/HTTPClient+GitHubAPI.swift +++ b/Sources/SwiftlyCore/HTTPClient+GitHubAPI.swift @@ -57,17 +57,6 @@ extension SwiftlyHTTPClient { } } - /// Get a list of releases on the apple/swift GitHub repository. - /// The releases are returned in "pages" of `perPage` releases (default 100). The page argument specifies the - /// page number. - /// - /// The results are returned in lexicographic order. - public func getReleases(page: Int, perPage: Int = 100) async throws -> [GitHubTag] { - let url = "https://api.github.com/repos/apple/swift/releases?per_page=\(perPage)&page=\(page)" - let releases: [GitHubRelease] = try await self.getFromGitHub(url: url) - return releases.filter { !$0.prerelease }.map { $0.toGitHubTag() } - } - /// Get a list of tags on the apple/swift GitHub repository. /// The tags are returned in pages of 100. The page argument specifies the page number. /// @@ -78,17 +67,6 @@ extension SwiftlyHTTPClient { } } -/// Model of a GitHub REST API release object. -/// See: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases -public struct GitHubRelease: Decodable { - fileprivate let name: String - fileprivate let prerelease: Bool - - fileprivate func toGitHubTag() -> GitHubTag { - GitHubTag(name: self.name, commit: nil) - } -} - /// Model of a GitHub REST API tag/release object. public struct GitHubTag: Decodable { internal struct Commit: Decodable { diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 9718e05b..397d7c7a 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -22,6 +22,110 @@ private func makeRequest(url: String) -> HTTPClientRequest { return request } +struct SwiftOrgSwiftlyRelease: Codable { + var name: String +} + +struct SwiftOrgPlatform: Codable { + var name: String + var archs: [String] + + /// platform is a mapping from the 'name' field of the swift.org platform object + /// to swiftly's PlatformDefinition, if possible. + var platform: PlatformDefinition? { + // NOTE: some of these platforms are represented on swift.org metadata, but not supported by swiftly and so they don't have constants in PlatformDefinition + switch self.name { + case "Ubuntu 14.04": + PlatformDefinition(name: "ubuntu1404", nameFull: "ubuntu14.04", namePretty: "Ubuntu 14.04") + case "Ubuntu 15.10": + PlatformDefinition(name: "ubuntu1510", nameFull: "ubuntu15.10", namePretty: "Ubuntu 15.10") + case "Ubuntu 16.04": + PlatformDefinition(name: "ubuntu1604", nameFull: "ubuntu16.04", namePretty: "Ubuntu 16.04") + case "Ubuntu 16.10": + PlatformDefinition(name: "ubuntu1610", nameFull: "ubuntu16.10", namePretty: "Ubuntu 16.10") + case "Ubuntu 18.04": + PlatformDefinition(name: "ubuntu1804", nameFull: "ubuntu18.04", namePretty: "Ubuntu 18.04") + case "Ubuntu 20.04": + PlatformDefinition.ubuntu2004 + case "Amazon Linux 2": + PlatformDefinition.amazonlinux2 + case "CentOS 8": + PlatformDefinition(name: "centos8", nameFull: "centos8", namePretty: "CentOS 8") + case "CentOS 7": + PlatformDefinition(name: "centos7", nameFull: "centos7", namePretty: "CentOS 7") + case "Windows 10": + PlatformDefinition(name: "win10", nameFull: "windows10", namePretty: "Windows 10") + case "Ubuntu 22.04": + PlatformDefinition.ubuntu2204 + case "Red Hat Universal Base Image 9": + PlatformDefinition.rhel9 + case "Ubuntu 23.10": + PlatformDefinition(name: "ubuntu2310", nameFull: "ubuntu23.10", namePretty: "Ubuntu 23.10") + case "Ubuntu 24.04": + PlatformDefinition(name: "ubuntu2404", nameFull: "ubuntu24.04", namePretty: "Ubuntu 24.04") + case "Debian 12": + PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian 12") + case "Fedora 39": + PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39") + default: + nil + } + } + + func matches(_ platform: PlatformDefinition) -> Bool { + guard let myPlatform = self.platform else { + return false + } + + return myPlatform.name == platform.name + } +} + +public struct SwiftOrgRelease: Codable { + var name: String + var platforms: [SwiftOrgPlatform] + + var stableName: String { + let components = self.name.components(separatedBy: ".") + if components.count == 2 { + return self.name + ".0" + } else { + return self.name + } + } +} + +public struct SwiftOrgSnapshotList: Codable { + var aarch64: [SwiftOrgSnapshot]? + var x86_64: [SwiftOrgSnapshot]? + var universal: [SwiftOrgSnapshot]? +} + +public struct SwiftOrgSnapshot: Codable { + var dir: String + + private static let snapshotRegex: Regex<(Substring, Substring?, Substring?, Substring)> = + try! Regex("swift(?:-(\\d+)\\.(\\d+))?-DEVELOPMENT-SNAPSHOT-(\\d{4}-\\d{2}-\\d{2})") + + internal func parseSnapshot() throws -> ToolchainVersion.Snapshot? { + guard let match = try? Self.snapshotRegex.firstMatch(in: self.dir) else { + return nil + } + + let branch: ToolchainVersion.Snapshot.Branch + if let majorString = match.output.1, let minorString = match.output.2 { + guard let major = Int(majorString), let minor = Int(minorString) else { + throw Error(message: "malformatted release branch: \"\(majorString).\(minorString)\"") + } + branch = .release(major: major, minor: minor) + } else { + branch = .main + } + + return ToolchainVersion.Snapshot(branch: branch, date: String(match.output.3)) + } +} + /// HTTPClient wrapper used for interfacing with various REST APIs and downloading things. public struct SwiftlyHTTPClient { private struct Response { @@ -68,15 +172,43 @@ public struct SwiftlyHTTPClient { /// Return an array of released Swift versions that match the given filter, up to the provided /// limit (default unlimited). - /// - /// TODO: retrieve these directly from swift.org instead of through GitHub. public func getReleaseToolchains( + platform: PlatformDefinition, + arch a: String? = nil, limit: Int? = nil, filter: ((ToolchainVersion.StableRelease) -> Bool)? = nil ) async throws -> [ToolchainVersion.StableRelease] { - let filterMap = { (gh: GitHubTag) -> ToolchainVersion.StableRelease? in - guard let release = try gh.parseStableRelease() else { - return nil + let arch = if a == nil { +#if arch(x86_64) + "x86_64" +#elseif arch(arm64) + "aarch64" +#else + #error("Unsupported processor architecture") +#endif + } else { + a! + } + + let url = "https://swift.org/api/v1/install/releases.json" + let swiftOrgReleases: [SwiftOrgRelease] = try await self.getFromJSON(url: url, type: [SwiftOrgRelease].self) + + var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try swiftOrgReleases.compactMap { swiftOrgRelease in + if platform.name != PlatformDefinition.macOS.name { + // If the platform isn't xcode then verify that there is an offering for this platform name and arch + guard let swiftOrgPlatform = swiftOrgRelease.platforms.first(where: { $0.matches(platform) }) else { + return nil + } + + guard swiftOrgPlatform.archs.contains(arch) else { + return nil + } + } + + guard let version = try? ToolchainVersion(parsing: swiftOrgRelease.stableName), + case let .stable(release) = version + else { + throw Error(message: "error parsing swift.org release version: \(swiftOrgRelease.stableName)") } if let filter { @@ -88,35 +220,96 @@ public struct SwiftlyHTTPClient { return release } - return try await self.mapGitHubTags(limit: limit, filterMap: filterMap) { page in - try await self.getReleases(page: page) + swiftOrgFiltered.sort(by: >) + + return if let limit = limit { + Array(swiftOrgFiltered.prefix(limit)) + } else { + swiftOrgFiltered } } /// Return an array of Swift snapshots that match the given filter, up to the provided /// limit (default unlimited). - /// - /// TODO: retrieve these directly from swift.org instead of through GitHub. public func getSnapshotToolchains( + platform: PlatformDefinition, + arch a: String? = nil, + branch: ToolchainVersion.Snapshot.Branch, limit: Int? = nil, filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil ) async throws -> [ToolchainVersion.Snapshot] { - let filter = { (gh: GitHubTag) -> ToolchainVersion.Snapshot? in - guard let snapshot = try gh.parseSnapshot() else { - return nil + // Fall back to using GitHub API's for snapshots on branches older than 6.0 + if case let .release(major, _) = branch, major < 6 { + let filter = { (gh: GitHubTag) -> ToolchainVersion.Snapshot? in + guard let snapshot = try gh.parseSnapshot() else { + return nil + } + + if let filter { + guard filter(snapshot) else { + return nil + } + } + + return snapshot + } + + return try await self.mapGitHubTags(limit: limit, filterMap: filter) { page in + try await self.getTags(page: page) } + } + + let arch = if a == nil { +#if arch(x86_64) + "x86_64" +#elseif arch(arm64) + "aarch64" +#else + #error("Unsupported processor architecture") +#endif + } else { + a! + } + + let platformName = if platform.name == PlatformDefinition.macOS.name { + "macos" + } else { + platform.name + } + + let url = "https://swift.org/api/v1/install/dev/\(branch.name)/\(platformName).json" + // For a particular branch and platform the snapshots are listed underneath their architecture + let swiftOrgSnapshotArchs: SwiftOrgSnapshotList = try await self.getFromJSON(url: url, type: SwiftOrgSnapshotList.self) + + // These are the available snapshots for the branch, platform, and architecture + let swiftOrgSnapshots = if platform.name == PlatformDefinition.macOS.name { + swiftOrgSnapshotArchs.universal ?? [SwiftOrgSnapshot]() + } else if arch == "aarch64" { + swiftOrgSnapshotArchs.aarch64 ?? [SwiftOrgSnapshot]() + } else if arch == "x86_64" { + swiftOrgSnapshotArchs.x86_64 ?? [SwiftOrgSnapshot]() + } else { + [SwiftOrgSnapshot]() + } + + // Convert these into toolchain snapshot versions that match the filter + var matchingSnapshots = try swiftOrgSnapshots.map { try $0.parseSnapshot() }.compactMap { $0 }.filter { toolchainVersion in if let filter { - guard filter(snapshot) else { - return nil + guard filter(toolchainVersion) else { + return false } } - return snapshot + return true } - return try await self.mapGitHubTags(limit: limit, filterMap: filter) { page in - try await self.getTags(page: page) + matchingSnapshots.sort(by: >) + + return if let limit = limit { + Array(matchingSnapshots.prefix(limit)) + } else { + matchingSnapshots } } diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 2eb0f833..32d2e13a 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -20,6 +20,13 @@ public struct PlatformDefinition: Codable, Equatable { self.nameFull = nameFull self.namePretty = namePretty } + + public static let macOS = PlatformDefinition(name: "xcode", nameFull: "osx", namePretty: "macOS") + public static let ubuntu2204 = PlatformDefinition(name: "ubuntu2204", nameFull: "ubuntu22.04", namePretty: "Ubuntu 22.04") + public static let ubuntu2004 = PlatformDefinition(name: "ubuntu2004", nameFull: "ubuntu20.04", namePretty: "Ubuntu 20.04") + public static let ubuntu1804 = PlatformDefinition(name: "ubuntu1804", nameFull: "ubuntu18.04", namePretty: "Ubuntu 18.04") + public static let rhel9 = PlatformDefinition(name: "ubi9", nameFull: "ubi9", namePretty: "RHEL 9") + public static let amazonlinux2 = PlatformDefinition(name: "amazonlinux2", nameFull: "amazonlinux2", namePretty: "Amazon Linux 2") } public protocol Platform { diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index 4185a377..e72155ae 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -6,9 +6,9 @@ final class HTTPClientTests: SwiftlyTests { func testGet() async throws { // GIVEN: we have a swiftly http client // WHEN: we make get request for a particular type of JSON - var releases: [GitHubRelease] = try await SwiftlyCore.httpClient.getFromJSON( - url: "https://api.github.com/repos/apple/swift/releases?per_page=100&page=1", - type: [GitHubRelease].self, + var releases: [SwiftOrgRelease] = try await SwiftlyCore.httpClient.getFromJSON( + url: "https://swift.org/api/v1/install/releases.json", + type: [SwiftOrgRelease].self, headers: [:] ) // THEN: we get a decoded JSON response @@ -19,8 +19,8 @@ final class HTTPClientTests: SwiftlyTests { var exceptionThrown = false do { releases = try await SwiftlyCore.httpClient.getFromJSON( - url: "https://api.github.com/repos/apple/swift/releases2", - type: [GitHubRelease].self, + url: "https://swift.org/api/v1/install/releases-invalid.json", + type: [SwiftOrgRelease].self, headers: [:] ) } catch { @@ -34,8 +34,8 @@ final class HTTPClientTests: SwiftlyTests { exceptionThrown = false do { releases = try await SwiftlyCore.httpClient.getFromJSON( - url: "https://inavlid.github.com/repos/apple/swift/releases", - type: [GitHubRelease].self, + url: "https://invalid.swift.org/api/v1/install/releases.json", + type: [SwiftOrgRelease].self, headers: [:] ) } catch { @@ -45,35 +45,38 @@ final class HTTPClientTests: SwiftlyTests { XCTAssertTrue(exceptionThrown) } - func testGetFromGitHub() async throws { - // GIVEN: we have a swiftly http client with github capability - // WHEN: we ask for the first page of releases with page size 5 - var releases = try await SwiftlyCore.httpClient.getReleases(page: 1, perPage: 5) - // THEN: we get five releases - XCTAssertEqual(5, releases.count) + func testGetMetdataFromSwiftOrg() async throws { + let supportedPlatforms = [ + PlatformDefinition.macOS, + PlatformDefinition.ubuntu2204, + PlatformDefinition.ubuntu2004, + // PlatformDefinition.ubuntu1804, // There are no releases for Ubuntu 18.04 in the branches being tested below + PlatformDefinition.rhel9, + PlatformDefinition.amazonlinux2, + ] - let firstRelease = releases[0] + let branches = [ + ToolchainVersion.Snapshot.Branch.main, + ToolchainVersion.Snapshot.Branch.release(major: 6, minor: 0), // This is available in swift.org API + ToolchainVersion.Snapshot.Branch.release(major: 5, minor: 9), // This is only available using GH API + ] - // GIVEN: we have a swiftly http client with github capability - // WHEN: we ask for the second page of releases with page size 5 - releases = try await SwiftlyCore.httpClient.getReleases(page: 2, perPage: 5) - // THEN: we get five different releases - XCTAssertEqual(5, releases.count) - XCTAssertTrue(releases[0].name != firstRelease.name) + for arch in ["x86_64", "aarch64"] { + for platform in supportedPlatforms { + // GIVEN: we have a swiftly http client with swift.org metadata capability + // WHEN: we ask for the first five releases of a supported platform in a supported arch + let releases = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: platform, arch: arch, limit: 5) + // THEN: we get five releases + XCTAssertEqual(5, releases.count) - // GIVEN: we have a swiftly http client with github capability - // WHEN: we ask for the first page of tags - var tags = try await SwiftlyCore.httpClient.getTags(page: 1) - // THEN: we get a collection of tags - XCTAssertTrue(tags.count > 0) - - let firstTag = tags[0] - - // GIVEN: we have a swiftly http client with github capability - // WHEN: we ask for the second page of tags - tags = try await SwiftlyCore.httpClient.getTags(page: 2) - // THEN: we get a different collection of tags - XCTAssertTrue(tags.count > 0) - XCTAssertTrue(tags[0].name != firstTag.name) + for branch in branches { + // GIVEN: we have a swiftly http client with swift.org metadata capability + // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch + let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: arch, branch: branch, limit: 5) + // THEN: we get five snapshots + XCTAssertEqual(5, snapshots.count) + } + } + } } } diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 3fed966a..9f469765 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -560,40 +560,18 @@ public class MockToolchainDownloader: HTTPRequestExecutor { if url.host == "download.swift.org" { return try self.makeToolchainDownloadResponse(from: url) } else if url.host == "api.github.com" { - if url.path == "/repos/apple/swift/releases" { - return try self.makeGitHubReleasesAPIResponse(from: url) - } else if url.path == "/repos/apple/swift/tags" { + if url.path == "/repos/apple/swift/tags" { return try self.makeGitHubTagsAPIResponse(from: url) } else { throw SwiftlyTestError(message: "unxpected github API request URL: \(request.url)") } - } else if request.url == "https://swift.org/keys/all-keys.asc" { + } else if url.host == "swift.org" { return try await self.delegate.execute(request, timeout: timeout) } else { - throw SwiftlyTestError(message: "unmocked URL: \(request.url)") + throw SwiftlyTestError(message: "unmocked URL: \(request)") } } - private func makeGitHubReleasesAPIResponse(from url: URL) throws -> HTTPClientResponse { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - throw SwiftlyTestError(message: "unexpected github url: \(url)") - } - - guard let queryItems = components.queryItems else { - return HTTPClientResponse(body: .bytes(ByteBuffer(data: Data(PackageResources.swift_releases_page1_json)))) - } - - guard let page = queryItems.first(where: { $0.name == "page" }) else { - return HTTPClientResponse(body: .bytes(ByteBuffer(data: Data(PackageResources.swift_releases_page1_json)))) - } - - if page.value != "1" { - return HTTPClientResponse(body: .bytes(ByteBuffer(data: Data(Array("[]".utf8))))) - } - - return HTTPClientResponse(body: .bytes(ByteBuffer(data: Data(PackageResources.swift_releases_page1_json)))) - } - private func makeGitHubTagsAPIResponse(from url: URL) throws -> HTTPClientResponse { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { throw SwiftlyTestError(message: "unexpected github url: \(url)") diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index 91d7d321..81af093b 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -43,14 +43,14 @@ final class UpdateTests: SwiftlyTests { func testUpdateLatestToLatest() async throws { try await self.withTestHome { try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: .stable(major: 5, minor: 0, patch: 0)) + try await self.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) var update = try self.parseCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await update.run() let config = try Config.load() let inUse = config.inUse!.asStableRelease! - XCTAssertGreaterThan(inUse, .init(major: 5, minor: 0, patch: 0)) + XCTAssertGreaterThan(inUse, .init(major: 5, minor: 9, patch: 0)) try await validateInstalledToolchains( [config.inUse!], description: "Updating toolchain should properly install new toolchain and uninstall old" @@ -64,7 +64,7 @@ final class UpdateTests: SwiftlyTests { func testUpdateToLatestMinor() async throws { try await self.withTestHome { try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: .stable(major: 5, minor: 0, patch: 0)) + try await self.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) var update = try self.parseCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await update.run() @@ -86,16 +86,16 @@ final class UpdateTests: SwiftlyTests { func testUpdateToLatestPatch() async throws { try await self.withTestHome { try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "5.0.0") + try await self.installMockedToolchain(selector: "5.9.0") - var update = try self.parseCommand(Update.self, ["update", "-y", "5.0.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + var update = try self.parseCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await update.run() let config = try Config.load() let inUse = config.inUse!.asStableRelease! XCTAssertEqual(inUse.major, 5) - XCTAssertEqual(inUse.minor, 0) + XCTAssertEqual(inUse.minor, 9) XCTAssertGreaterThan(inUse.patch, 0) try await validateInstalledToolchains( @@ -111,16 +111,16 @@ final class UpdateTests: SwiftlyTests { func testUpdateInUse() async throws { try await self.withTestHome { try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "5.0.0") + try await self.installMockedToolchain(selector: "5.9.0") var update = try self.parseCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await update.run() let config = try Config.load() let inUse = config.inUse!.asStableRelease! - XCTAssertGreaterThan(inUse, .init(major: 5, minor: 0, patch: 0)) + XCTAssertGreaterThan(inUse, .init(major: 5, minor: 9, patch: 0)) XCTAssertEqual(inUse.major, 5) - XCTAssertEqual(inUse.minor, 0) + XCTAssertEqual(inUse.minor, 9) XCTAssertGreaterThan(inUse.patch, 0) try await self.validateInstalledToolchains( @@ -170,20 +170,20 @@ final class UpdateTests: SwiftlyTests { func testUpdateSelectsLatestMatchingStableRelease() async throws { try await self.withTestHome { try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "5.0.1") - try await self.installMockedToolchain(selector: "5.0.0") + try await self.installMockedToolchain(selector: "5.9.1") + try await self.installMockedToolchain(selector: "5.9.0") - var update = try self.parseCommand(Update.self, ["update", "-y", "5.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + var update = try self.parseCommand(Update.self, ["update", "-y", "5.9", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await update.run() let config = try Config.load() let inUse = config.inUse!.asStableRelease! XCTAssertEqual(inUse.major, 5) - XCTAssertEqual(inUse.minor, 0) + XCTAssertEqual(inUse.minor, 9) XCTAssertGreaterThan(inUse.patch, 1) try await self.validateInstalledToolchains( - [config.inUse!, .init(major: 5, minor: 0, patch: 0)], + [config.inUse!, .init(major: 5, minor: 9, patch: 0)], description: "update with ambiguous selector should update the latest matching toolchain" ) } diff --git a/Tests/SwiftlyTests/gh-api-cache/swift-releases-page1.json b/Tests/SwiftlyTests/gh-api-cache/swift-releases-page1.json deleted file mode 100644 index f64c982e..00000000 --- a/Tests/SwiftlyTests/gh-api-cache/swift-releases-page1.json +++ /dev/null @@ -1,690 +0,0 @@ -[ - { - "author": { - }, - "tag_name": "swift-5.10.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.10.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.10-RELEASE", - "target_commitish": "main", - "name": "Swift 5.10 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.9.2-RELEASE", - "target_commitish": "main", - "name": "Swift 5.9.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.9.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.9.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.9-RELEASE", - "target_commitish": "main", - "name": "Swift 5.9 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.8.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.8.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.8-RELEASE", - "target_commitish": "main", - "name": "Swift 5.8 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.7.3-RELEASE", - "target_commitish": "main", - "name": "Swift 5.7.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.7.2-RELEASE", - "target_commitish": "main", - "name": "Swift 5.7.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.7.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.7.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.7-RELEASE", - "target_commitish": "main", - "name": "Swift 5.7 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.6.3-RELEASE", - "target_commitish": "main", - "name": "Swift 5.6.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.6.2-RELEASE", - "target_commitish": "main", - "name": "Swift 5.6.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.6.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.6.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.6-RELEASE", - "target_commitish": "main", - "name": "Swift 5.6 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.5.3-RELEASE", - "target_commitish": "main", - "name": "Swift 5.5.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.5.2-RELEASE", - "target_commitish": "main", - "name": "Swift 5.5.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.5.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.5.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.5-RELEASE", - "target_commitish": "main", - "name": "Swift 5.5 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.4.3-RELEASE", - "target_commitish": "main", - "name": "Swift 5.4.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.4.2-RELEASE", - "target_commitish": "main", - "name": "Swift 5.4.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.4.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.4.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.4-RELEASE", - "target_commitish": "main", - "name": "Swift 5.4 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.3.3-RELEASE", - "target_commitish": "main", - "name": "Swift 5.3.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.3.2-RELEASE", - "target_commitish": "main", - "name": "Swift 5.3.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.3.1-RELEASE", - "target_commitish": "main", - "name": "Swift 5.3.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.3-RELEASE", - "target_commitish": "master", - "name": "Swift 5.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.2.5-RELEASE", - "target_commitish": "master", - "name": "Swift 5.2.5 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.2.4-RELEASE", - "target_commitish": "master", - "name": "Swift 5.2.4 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.2.3-RELEASE", - "target_commitish": "master", - "name": "Swift 5.2.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.2.2-RELEASE", - "target_commitish": "master", - "name": "Swift 5.2.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.2.1-RELEASE", - "target_commitish": "master", - "name": "Swift 5.2.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-5.2-RELEASE", - "target_commitish": "master", - "name": "Swift 5.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.1.5-RELEASE", - "target_commitish": "master", - "name": "Swift 5.1.5 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.1.2-RELEASE", - "target_commitish": "master", - "name": "Swift 5.1.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.1-RELEASE", - "target_commitish": "master", - "name": "Swift 5.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.0.3-RELEASE", - "target_commitish": "master", - "name": "Swift 5.0.3 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.0.2-RELEASE", - "target_commitish": "master", - "name": "Swift 5.0.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-5.0-RELEASE", - "target_commitish": "master", - "name": "", - "draft": false, - "prerelease": false, - "assets": [ - - ], - "reactions": { - } - }, - { - "author": { - }, - "tag_name": "swift-4.2.2-RELEASE", - "target_commitish": "master", - "name": "", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-4.2.1-RELEASE", - "target_commitish": "master", - "name": "Swift 4.2.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-4.2-RELEASE", - "target_commitish": "master", - "name": "Swift 4.2 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-4.1.1-RELEASE", - "target_commitish": "main", - "name": "Swift 4.1.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-4.1-RELEASE", - "target_commitish": "main", - "name": "Swift 4.1 Release", - "draft": false, - "prerelease": false, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0.1-PREVIEW-3", - "target_commitish": "main", - "name": "Swift 3.0.1 Preview 3", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0.1-PREVIEW-2", - "target_commitish": "main", - "name": "Swift 3.0.1 Preview 2", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0.1-PREVIEW-1", - "target_commitish": "main", - "name": "Swift 3.0.1 Preview 1", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0-PREVIEW-6", - "target_commitish": "main", - "name": "Swift 3.0 Preview 6", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0-PREVIEW-5", - "target_commitish": "main", - "name": "Swift 3.0 Preview 5", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0-PREVIEW-4", - "target_commitish": "main", - "name": "Swift 3.0 Preview 4", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0-PREVIEW-3", - "target_commitish": "main", - "name": "Swift 3.0 Preview 3", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0-PREVIEW-2", - "target_commitish": "main", - "name": "Swift 3.0 Preview 2", - "draft": false, - "prerelease": true, - "assets": [ - - ], - }, - { - "author": { - }, - "tag_name": "swift-3.0-PREVIEW-1", - "target_commitish": "main", - "name": "Swift 3.0 Preview 1", - "draft": false, - "prerelease": true, - "assets": [ - - ], - } -]