diff --git a/AppUpdater.swift b/AppUpdater.swift index d403136..1c9fbb9 100644 --- a/AppUpdater.swift +++ b/AppUpdater.swift @@ -755,13 +755,18 @@ struct Release: Decodable, Comparable { } } + /// The DMG named `-.dmg` for this release. The version in + /// the name is parsed the way the tag is, so `MyApp-2.1.dmg` matches a + /// `v2.1` tag even though the tag prints as `2.1.0`. func viableAsset(forRepo repo: String) -> Asset? { - assets.first { asset in - let prefix = "\(repo.lowercased())-\(tagName)" + let prefix = "\(repo.lowercased())-" + return assets.first { asset in let name = (asset.name as NSString).deletingPathExtension .lowercased() - - return name == prefix && asset.contentType == .dmg + guard asset.contentType == .dmg, name.hasPrefix(prefix) else { + return false + } + return Version(tolerant: name.dropFirst(prefix.count)) == tagName } } diff --git a/README.md b/README.md index 874fd0e..b88284e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ $ ditto "$SWIFT_BIN/AppUpdater_AppUpdater.bundle" \ ## Release layout AppUpdater only accepts DMG assets. Name each asset -`-.dmg`, for example `MyApp-2.1.0.dmg`. +`-.dmg`, for example `MyApp-2.1.0.dmg`. The version is +read the way the tag is, so a `v2.1` release may name its asset `MyApp-2.1.dmg`. The mounted DMG must contain one top-level app. Its filename must match the installed app, including case. A release for `MyApp.app` therefore contains: diff --git a/Tests/AppUpdaterTests/AppUpdaterTests.swift b/Tests/AppUpdaterTests/AppUpdaterTests.swift index 2b40a27..56bb186 100644 --- a/Tests/AppUpdaterTests/AppUpdaterTests.swift +++ b/Tests/AppUpdaterTests/AppUpdaterTests.swift @@ -356,6 +356,46 @@ final class AppUpdaterTests: XCTestCase { } } + @MainActor + func testCheckMatchesAssetNamedWithShortVersion() async throws { + let releases = try [ + release("v2.1", prerelease: false, assetName: "AppUpdater-2.1.dmg"), + ] + let updater = AppUpdater( + owner: "mxcl", + repo: "AppUpdater", + currentVersion: { Version(2, 0, 0) }, + fetchReleases: { releases }, + prepareAsset: { asset in preparedUpdate(assetName: asset.name) } + ) + + let update = try await updater.check() + + XCTAssertEqual(update?.version, "2.1.0") + XCTAssertEqual(update?.assetName, "AppUpdater-2.1.dmg") + } + + @MainActor + func testCheckDoesNotMatchAssetForAnotherVersion() async throws { + let releases = try [ + release("v2.1", prerelease: false, assetName: "AppUpdater-2.1.1.dmg"), + ] + let updater = AppUpdater( + owner: "mxcl", + repo: "AppUpdater", + currentVersion: { Version(2, 0, 0) }, + fetchReleases: { releases }, + prepareAsset: { _ in + XCTFail("update should not run") + return preparedUpdate() + } + ) + + let update = try await updater.check() + + XCTAssertNil(update) + } + @MainActor func testCheckDoesNotUpdateWithoutMatchingAsset() async throws { let releases = try [ @@ -1447,7 +1487,9 @@ final class AppUpdaterTests: XCTestCase { } """.data(using: .utf8)! - return try JSONDecoder().decode(Release.self, from: json) + let decoder = JSONDecoder() + decoder.userInfo[.decodingMethod] = DecodingMethod.tolerant + return try decoder.decode(Release.self, from: json) } private func temporaryDirectory() throws -> URL {