From 970750c5c338f39bf80112abfcb709ffe5be71be Mon Sep 17 00:00:00 2001 From: amddg44 Date: Thu, 7 Dec 2023 12:22:08 +0000 Subject: [PATCH 1/7] Changes to Credit Card Autofill behaviour (#1928) Task/Issue URL: https://app.asana.com/0/1175293949586521/1206085730384194/f Tech Design URL: CC: Description: Updates Credit Card autofill to always require device authentication before filling forms (with 10 second grace period) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++-- .../ContentOverlayViewController.swift | 2 +- DuckDuckGo/Common/Localizables/UserText.swift | 4 +- .../DeviceAuthenticator.swift | 40 ++++++++++++++++++- LocalPackages/Account/Package.swift | 2 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- 8 files changed, 49 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f00c7d5792..ebb3785bc3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -12752,7 +12752,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 92.0.3; + version = 92.0.4; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7f7e993789..8a81b789e2 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "5ca0b3d915ab73de43744db40edf41c1d5060034", - "version" : "92.0.3" + "revision" : "33e55105acc9d6d69ae1326fd5c506cefb89d5cc", + "version" : "92.0.4" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "93677cc02cfe650ce7f417246afd0e8e972cd83e", - "version" : "10.0.0" + "revision" : "dbecae0df07650a21b5632a92fa2e498c96af7b5", + "version" : "10.0.1" } }, { diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 26eb6e4733..c30ef3ff36 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -302,7 +302,7 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { } public func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler handler: @escaping (Bool) -> Void) { - DeviceAuthenticator.shared.authenticateUser(reason: .autofill) { authenticationResult in + DeviceAuthenticator.shared.authenticateUser(reason: .autofillCreditCards) { authenticationResult in handler(authenticationResult.authenticated) } } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b47f3770a1..d8cd6eb1c7 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -374,8 +374,8 @@ struct UserText { static let autofillAutoLock = NSLocalizedString("autofill.auto-lock", value: "Auto-lock", comment: "Autofill settings section title") static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock Autofill after computer is idle for", comment: "Autofill auto-lock setting") static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock Autofill", comment: "Autofill auto-lock setting") - static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "Anyone with access to your device will be able to use and modify your Autofill data if not locked.", comment: "Autofill disabled auto-lock warning") - static let autolockLocksFormFill = NSLocalizedString("autofill.autolock-locks-form-filling", value: "Also lock access to Login and Credit Card form fill.", comment: "Lock form filling when auto-lock is active text") + static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, saved credit cards always require authentication.", comment: "Autofill disabled auto-lock warning") + static let autolockLocksFormFill = NSLocalizedString("autofill.autolock-locks-form-filling", value: "Also lock password form fill", comment: "Lock form filling when auto-lock is active text") static let downloadsLocation = NSLocalizedString("downloads.location", value: "Location", comment: "Downloads directory location") diff --git a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift index 66cea43926..e2bc62376d 100644 --- a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift +++ b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift @@ -34,13 +34,14 @@ final class DeviceAuthenticator: UserAuthenticating { enum AuthenticationReason { case autofill + case autofillCreditCards case changeLoginsSettings case unlockLogins case exportLogins var localizedDescription: String { switch self { - case .autofill: return UserText.pmAutoLockPromptAutofill + case .autofill, .autofillCreditCards: return UserText.pmAutoLockPromptAutofill case .changeLoginsSettings: return UserText.pmAutoLockPromptChangeLoginsSettings case .unlockLogins: return UserText.pmAutoLockPromptUnlockLogins case .exportLogins: return UserText.pmAutoLockPromptExportLogins @@ -50,6 +51,7 @@ final class DeviceAuthenticator: UserAuthenticating { internal enum Constants { static var intervalBetweenIdleChecks: TimeInterval = 1 + static var intervalBetweenCreditCardAutofillChecks: TimeInterval = 10 } static var deviceSupportsBiometrics: Bool { @@ -91,6 +93,7 @@ final class DeviceAuthenticator: UserAuthenticating { private let queue = DispatchQueue(label: "Device Authenticator Queue") private var timer: Timer? + private var timerCreditCard: Timer? private var _isAuthenticating: Bool = false private var _deviceIsLocked: Bool = false @@ -144,7 +147,7 @@ final class DeviceAuthenticator: UserAuthenticating { } func authenticateUser(reason: AuthenticationReason, result: @escaping (DeviceAuthenticationResult) -> Void) { - guard requiresAuthentication else { + guard (reason == .autofillCreditCards && creditCardTimeIntervalExpired()) || requiresAuthentication else { result(.success) return } @@ -162,6 +165,7 @@ final class DeviceAuthenticator: UserAuthenticating { if authenticationResult.authenticated { // Now that the user has unlocked the device, begin the idle timer again. self.beginIdleCheckTimer() + self.beginCreditCardAutofillTimer() } result(authenticationResult) @@ -233,4 +237,36 @@ final class DeviceAuthenticator: UserAuthenticating { } } + // MARK: - Credit Card Autofill Timer + + private func beginCreditCardAutofillTimer() { + os_log("Beginning credit card autofill timer", log: .autoLock) + + self.timerCreditCard?.invalidate() + self.timerCreditCard = nil + + let timer = Timer(timeInterval: Constants.intervalBetweenCreditCardAutofillChecks, repeats: false) { [weak self] _ in + guard let self = self else { + return + } + self.cancelCreditCardAutofillTimer() + } + + self.timerCreditCard = timer + RunLoop.current.add(timer, forMode: .common) + } + + private func cancelCreditCardAutofillTimer() { + os_log("Cancelling credit card autofill timer", log: .autoLock) + self.timerCreditCard?.invalidate() + self.timerCreditCard = nil + } + + private func creditCardTimeIntervalExpired() -> Bool { + guard let timer = timerCreditCard else { + return true + } + return timer.timeInterval >= Constants.intervalBetweenCreditCardAutofillChecks + } + } diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Account/Package.swift index 544e58a429..afd6a09948 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "92.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "92.0.4"), .package(path: "../Purchase") ], targets: [ diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index b2cdb16d66..86b64b960f 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "92.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "92.0.4"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index b125f71166..78e3fea053 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "92.0.3"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "92.0.4"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions") ], From 9aad2715d75e3a5408bf50f81af198637858b56d Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 7 Dec 2023 10:30:28 -0300 Subject: [PATCH 2/7] Instantiate a new DBP WebView every time we open the DBP tab (#1930) Task/Issue URL: https://app.asana.com/0/1204167627774280/1206111587274630/f **Description**: Instantiate a new DBP WebView every time we open the DBP tab --- DuckDuckGo/Tab/View/BrowserTabViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index b122d9dcca..15503f8677 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -448,6 +448,7 @@ final class BrowserTabViewController: NSViewController { bookmarksViewController?.removeCompletely() #if DBP dataBrokerProtectionHomeViewController?.removeCompletely() + dataBrokerProtectionHomeViewController = nil #endif if includingWebView { self.removeWebViewFromHierarchy() From 5f30e5e3a0e3d00ece8a9d5b19900fd6c00cfb5b Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 7 Dec 2023 10:34:43 -0300 Subject: [PATCH 3/7] Make a broker as scanned when all profile queries have a lastRunDate (#1937) --- .../Sources/DataBrokerProtection/UI/UIMapper.swift | 4 ++-- .../DataBrokerProtectionTests/MapperToUITests.swift | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 44e2f2bc2a..1b37f90407 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -249,9 +249,9 @@ fileprivate extension Array where Element == BrokerProfileQueryData { guard let broker = self.first?.dataBroker else { return 0 } let areAllQueriesDeprecated = allSatisfy { $0.profileQuery.deprecated } - let areAllQueriesNotStartedScanning = allSatisfy { $0.scanOperationData.lastRunDate == nil } + let didAllQueriesFinished = allSatisfy { $0.scanOperationData.lastRunDate != nil } - if areAllQueriesDeprecated || areAllQueriesNotStartedScanning { + if areAllQueriesDeprecated || !didAllQueriesFinished { return 0 } else { return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index ead4b1aa0f..4d1ff6b8f8 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -45,9 +45,9 @@ final class MapperToUITests: XCTestCase { XCTAssertEqual(result.scanProgress.totalScans, 2) } - func testWhenAScanRanOnOneBroker_thenCurrentScansReflectsThatScansWereDoneOnThatBroker() { + func testWhenAScanRanOnAllProfileQueriesOnTheSameBroker_thenCurrentScansReflectsThatScansWereDoneOnThatBroker() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1"), + .mock(dataBrokerName: "Broker #1", lastRunDate: Date()), .mock(dataBrokerName: "Broker #1", lastRunDate: Date()), .mock(dataBrokerName: "Broker #2") ] @@ -205,10 +205,15 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToCurrentScans() { let brokerWithMirrorSiteRemovedAndWithScan = BrokerProfileQueryData.mock( + dataBrokerName: "Broker #2", lastRunDate: Date(), mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] ) - let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteRemovedAndWithScan] + let brokerProfileQueryData: [BrokerProfileQueryData] = [ + .mock(dataBrokerName: "Broker #1"), + .mock(dataBrokerName: "Broker #1"), + brokerWithMirrorSiteRemovedAndWithScan + ] let result = sut.initialScanState(brokerProfileQueryData) From 7cb93ede6b599ec180c1dfb5c79a044b839e6939 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 7 Dec 2023 15:44:53 +0100 Subject: [PATCH 4/7] NetP: Geoswitching Pixels (#1933) Task/Issue URL: https://app.asana.com/0/414235014887631/1206107031155633/f **Description**: **Steps to test this PR**: 1. None really; just check it compiles (which CI will do). The only changed type is not used from macOS. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) From 2bd8ca8663b388d62126822c8c26be7e39fdd547 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Thu, 7 Dec 2023 17:01:00 +0100 Subject: [PATCH 5/7] Last-minute copy tweak (#1940) Task/Issue URL: https://app.asana.com/0/0/1206111958518305/f Tech Design URL: CC: **Description**: Just a last-minute copy tweak during ship review. **Steps to test this PR**: NA --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) Signed-off-by: Emanuele Feliziani --- DuckDuckGo/Common/Localizables/UserText.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index d8cd6eb1c7..a6a9e13d7f 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -374,7 +374,7 @@ struct UserText { static let autofillAutoLock = NSLocalizedString("autofill.auto-lock", value: "Auto-lock", comment: "Autofill settings section title") static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock Autofill after computer is idle for", comment: "Autofill auto-lock setting") static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock Autofill", comment: "Autofill auto-lock setting") - static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, saved credit cards always require authentication.", comment: "Autofill disabled auto-lock warning") + static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication.", comment: "Autofill disabled auto-lock warning") static let autolockLocksFormFill = NSLocalizedString("autofill.autolock-locks-form-filling", value: "Also lock password form fill", comment: "Lock form filling when auto-lock is active text") From fb52a9c1b51376aa82c4f1a51cdb82d6644f743e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Dec 2023 21:25:15 +0100 Subject: [PATCH 6/7] Always use black and white colors for the QR code (#1941) Task/Issue URL: https://app.asana.com/0/1201493110486074/1205142491450702/f Description: This is to increase compatibility with Android devices. --- .../SyncUI/Sources/SyncUI/Views/internal/QRCodeView.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/QRCodeView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/QRCodeView.swift index fb6f3153fc..5eaaa40614 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/QRCodeView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/internal/QRCodeView.swift @@ -20,8 +20,6 @@ import SwiftUI import CoreImage struct QRCode: View { - @Environment(\.colorScheme) var colorScheme - let string: String let size: CGSize @@ -58,8 +56,8 @@ struct QRCode: View { } let colorParameters: [String: Any] = [ - "inputColor0": CIColor(color: colorScheme == .light ? NSColor.black : NSColor.white)!, - "inputColor1": CIColor(color: NSColor.clear)! + "inputColor0": CIColor(color: NSColor.black)!, + "inputColor1": CIColor(color: NSColor.white)! ] let coloredImage = outputImage.applyingFilter("CIFalseColor", parameters: colorParameters) From b4d3a3e243c8f55be44f678e3356b5abb2066054 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 8 Dec 2023 10:13:15 +0100 Subject: [PATCH 7/7] Unify build number between App Store and DMG releases (#1929) Task/Issue URL: https://app.asana.com/0/1201037661562251/1206111130949152/f Description: * DMG target now uses the same build number as App Store target. * To support internal releases using the same marketing version, DMG filenames now include build number. * appcastManager and archive.sh were updated to support it. * set_version.sh has been deleted and replaced by fastlane lane: bundle exec fastlane mac set_version version:1.70.0 * New fastlane lane was added for creating subsequent internal releases: bundle exec fastlane mac bump_internal_release --- .swiftlint.yml | 1 + Configuration/App/AppTargetsBase.xcconfig | 2 - Configuration/AppStore.xcconfig | 2 +- ...ldNumber.xcconfig => BuildNumber.xcconfig} | 0 Configuration/Common.xcconfig | 1 + .../Extensions/ExtensionBase.xcconfig | 2 - DuckDuckGo.xcodeproj/project.pbxproj | 4 +- Gemfile | 1 + Gemfile.lock | 5 + fastlane/Fastfile | 136 ++++++++++-- fastlane/README.md | 16 ++ scripts/README.md | 79 ------- scripts/appcast_manager/appcastManager.swift | 86 ++++---- scripts/archive.sh | 16 +- scripts/helpers/version.sh | 25 ++- scripts/sparkle-sandbox.sh | 200 ------------------ set_version.sh | 16 -- 17 files changed, 220 insertions(+), 372 deletions(-) rename Configuration/{AppStoreBuildNumber.xcconfig => BuildNumber.xcconfig} (100%) delete mode 100755 scripts/sparkle-sandbox.sh delete mode 100755 set_version.sh diff --git a/.swiftlint.yml b/.swiftlint.yml index 8473c3c6af..3058699143 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -76,3 +76,4 @@ excluded: - UITests - vendor - DerivedData + - .ruby-lsp diff --git a/Configuration/App/AppTargetsBase.xcconfig b/Configuration/App/AppTargetsBase.xcconfig index 68e7f7a026..75739c6c7b 100644 --- a/Configuration/App/AppTargetsBase.xcconfig +++ b/Configuration/App/AppTargetsBase.xcconfig @@ -28,8 +28,6 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES CODE_SIGN_STYLE[sdk=*] = Manual CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic -CURRENT_PROJECT_VERSION = $(MARKETING_VERSION) - ENABLE_HARDENED_RUNTIME = YES INFOPLIST_FILE = DuckDuckGo/Info.plist diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index 804cd0af5f..d309999bd3 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -14,7 +14,7 @@ // limitations under the License. // -#include "AppStoreBuildNumber.xcconfig" +#include "BuildNumber.xcconfig" MAIN_BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.mobile.ios diff --git a/Configuration/AppStoreBuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig similarity index 100% rename from Configuration/AppStoreBuildNumber.xcconfig rename to Configuration/BuildNumber.xcconfig diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index aba8624707..e83abad09c 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -14,6 +14,7 @@ // #include "Version.xcconfig" +#include "BuildNumber.xcconfig" COMBINE_HIDPI_IMAGES = YES diff --git a/Configuration/Extensions/ExtensionBase.xcconfig b/Configuration/Extensions/ExtensionBase.xcconfig index 076f1bfc2c..4e613539ea 100644 --- a/Configuration/Extensions/ExtensionBase.xcconfig +++ b/Configuration/Extensions/ExtensionBase.xcconfig @@ -20,8 +20,6 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES CODE_SIGN_STYLE[sdk=*] = Manual -CURRENT_PROJECT_VERSION = $(MARKETING_VERSION) - ENABLE_HARDENED_RUNTIME = YES PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ebb3785bc3..e6116d27fe 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3214,7 +3214,7 @@ 3767190128E724B2003A2A15 /* DuckPlayerURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerURLExtension.swift; sourceTree = ""; }; 376C4DB828A1A48A00CC0F5B /* FirePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewModelTests.swift; sourceTree = ""; }; 376CC8B4296EB630006B63A7 /* AppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppStore.xcconfig; sourceTree = ""; }; - 376CC8B5296EBA8F006B63A7 /* AppStoreBuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppStoreBuildNumber.xcconfig; sourceTree = ""; }; + 376CC8B5296EBA8F006B63A7 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = ""; }; 37717E66296B5A20002FAEDF /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = ""; }; 3775912C29AAC72700E26367 /* SyncPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferences.swift; sourceTree = ""; }; 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManagementDialogViewController.swift; sourceTree = ""; }; @@ -4757,8 +4757,8 @@ 37717E66296B5A20002FAEDF /* Global.xcconfig */, 378B5888295CF2A4002C0CC0 /* Common.xcconfig */, 378B5887295CF2A4002C0CC0 /* Version.xcconfig */, + 376CC8B5296EBA8F006B63A7 /* BuildNumber.xcconfig */, 376CC8B4296EB630006B63A7 /* AppStore.xcconfig */, - 376CC8B5296EBA8F006B63A7 /* AppStoreBuildNumber.xcconfig */, 7B5291892A169BC90022E406 /* DeveloperID.xcconfig */, 378C76D8296842FD0092E949 /* App */, 4B4D604E2A0B293C00BCD287 /* Extensions */, diff --git a/Gemfile b/Gemfile index 6a591a7207..08d3071c2b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ source 'https://rubygems.org' gem 'fastlane', '2.217.0' +gem 'httparty' diff --git a/Gemfile.lock b/Gemfile.lock index d857156940..494e999a3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,6 +146,9 @@ GEM highline (2.0.3) http-cookie (1.0.5) domain_name (~> 0.5) + httparty (0.21.0) + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) httpclient (2.8.3) jmespath (1.6.2) json (2.6.3) @@ -153,6 +156,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) + multi_xml (0.6.0) multipart-post (2.3.0) nanaimo (0.3.0) naturally (2.2.1) @@ -208,6 +212,7 @@ PLATFORMS DEPENDENCIES fastlane (= 2.217.0) + httparty BUNDLED WITH 2.3.26 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3b166e71af..4f28672b0d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,5 +1,8 @@ # frozen_string_literal: true +fastlane_require 'httparty' +fastlane_require 'rexml/document' + UI.abort_with_message!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? ######################################################################## @@ -8,8 +11,9 @@ UI.abort_with_message!('Please run fastlane via `bundle exec`') unless FastlaneC DEFAULT_BRANCH = 'main' RELEASE_BRANCH = 'release' PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__)) +INFO_PLIST = File.join(PROJECT_ROOT_FOLDER, 'DuckDuckGo/Info.plist') VERSION_CONFIG_PATH = File.join(PROJECT_ROOT_FOLDER, 'Configuration/Version.xcconfig') -APP_STORE_BUILD_NUMBER_CONFIG_PATH = File.join(PROJECT_ROOT_FOLDER, 'Configuration/AppStoreBuildNumber.xcconfig') +BUILD_NUMBER_CONFIG_PATH = File.join(PROJECT_ROOT_FOLDER, 'Configuration/BuildNumber.xcconfig') VERSION_CONFIG_DEFINITION = 'MARKETING_VERSION' BUILD_NUMBER_CONFIG_DEFINITION = 'CURRENT_PROJECT_VERSION' UPGRADABLE_EMBEDDED_FILES = [ @@ -99,23 +103,27 @@ platform :mac do # # - Cuts a new release branch # - Updates submodules and embedded files + # - Pushes changes to remote # # @option [String] version (default: nil) Marketing version string - # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt # @option [Boolean] resume (default: false) If true, the lane can run from a release/ branch and will run dedicated prechecks. + # @option [Boolean] force (default: false) Don't ask for confirmation. # desc 'Executes the release preparation work in the repository' lane :code_freeze do |options| begin options[:resume]? macos_codefreeze_resume_prechecks : macos_codefreeze_prechecks new_version = validate_new_version(options) - app_store_build_number = increment_app_store_build_number(options) + build_number = increment_current_build_number(options) macos_create_release_branch(version: new_version) unless options[:resume] macos_update_embedded_files macos_update_version_and_build_number_config( version: new_version, - build_number: app_store_build_number + build_number: build_number, + force: options[:force] ) + sh('git', 'push') + rescue => exception if exception.message == "Tests have failed" UI.user_error! %{Tests have failed. @@ -131,6 +139,75 @@ platform :mac do end end + # Bumps build number for the current version and updates embedded files. + # Pushes changes to remote. + # + # - Should be called on an existing internal release branch. + # - Also runs unit tests after updating embedded files. + # + # @option [Boolean] force (default: false) Don't ask for confirmation. + # + desc 'Prepares new internal release on top of an existing one' + lane :bump_internal_release do |options| + begin + unless git_branch.start_with?(RELEASE_BRANCH) + UI.abort_with_message!("Incorrect branch. Branch name must start with '#{RELEASE_BRANCH}/'.") + end + + force = options[:force].nil? ? false : options[:force] + current_version = macos_current_version + current_build_number = macos_current_build_number + build_number = increment_current_build_number(options) + + UI.important("Current version is #{current_version} (#{current_build_number}).") + UI.important("Will update to #{current_version} (#{build_number}).") + + unless force + unless UI.confirm("Do you want to continue?") + UI.abort_with_message!('Aborted by user.') + end + end + + macos_update_embedded_files + macos_update_version_and_build_number_config( + version: current_version, + build_number: build_number, + force: true + ) + sh('git', 'push') + + rescue => exception + if exception.message == "Tests have failed" + UI.user_error! %{Tests have failed. +* If you believe the failing test is flaky, please retry the same fastlane command. +* If the failure looks legitimate, try to fix it, commit the fix (be sure to only + include the files you've changed while making a fix and leave other changed files + unmodified), and run the command again appending `resume:true`. + } + else + raise exception + end + end + end + + # Updates marketing version to the specified one and increments build number by 1. + # + # @option [String] version Marketing version string. + # @option [Boolean] force (default: false) Don't ask for confirmation. + # + desc 'Executes the release preparation work in the repository' + lane :set_version do |options| + unless options[:version] + UI.user_error! 'You must provide a version.' + end + new_version = validate_new_version(options) + build_number = increment_current_build_number(options) + macos_update_version_and_build_number_config( + version: new_version, + build_number: build_number + ) + end + ################################################# # Helper functions ################################################# @@ -229,30 +306,52 @@ platform :mac do end end - # Calculates the new version or validate the provided one, if it exists + # Calculates the new version or validates the provided one, if it exists # and prompts the user to confirm # # @option [String] version (default: nil) Marketing version string + # @option [Boolean] force (default: false) Don't ask for confirmation. # private_lane :validate_new_version do |options| current_version = macos_current_version user_version = format_user_version(options[:version]) new_version = user_version.nil? ? macos_bump_minor_version(current_version) : user_version - unless UI.confirm( - "Current version is #{current_version}.\nNew version is #{new_version}.\nDo you want to continue?" - ) - UI.abort_with_message!('Aborted by user.') + force = options[:force].nil? ? false : options[:force] + unless force + unless UI.confirm( + "Current version is #{current_version}.\nNew version is #{new_version}.\nDo you want to continue?" + ) + UI.abort_with_message!('Aborted by user.') + end end new_version end # Checks current build number and increments it by 1. # - desc 'Increment App Store build number' - private_lane :increment_app_store_build_number do + desc 'Increment build number' + private_lane :increment_current_build_number do + macos_current_build_number = [fetch_testflight_build_number, fetch_appcast_build_number].max macos_current_build_number + 1 end + private_lane :fetch_testflight_build_number do |options| + build_number = latest_testflight_build_number( + api_key: get_api_key, + username: get_username(options), + platform: 'osx' + ) + build_number + end + + private_lane :fetch_appcast_build_number do |options| + url = sh("plutil -extract SUFeedURL raw #{INFO_PLIST}").chomp + xml = HTTParty.get(url).body + xml_data = REXML::Document.new(xml) + versions = xml_data.get_elements('//rss/channel/item/sparkle:version').map { |e| e.text.split('.')[0].to_i } + versions.max + end + # Checks out a new branch from the current commit and pushes it # # @option [String] version (default: nil) Marketing version string @@ -279,7 +378,7 @@ platform :mac do # - Runs automated tests # - Commits and pushes # - private_lane :macos_update_embedded_files do + private_lane :macos_update_embedded_files do |options| sh("cd #{PROJECT_ROOT_FOLDER} && ./scripts/update_embedded.sh") # Verify no unexpected files were modified @@ -300,36 +399,35 @@ platform :mac do modified_files.each { |modified_file| sh('git', 'add', modified_file.to_s) } sh('git', 'commit', '-m', 'Update embedded files') ensure_git_status_clean - sh('git', 'push') end # Updates version in the config file # - # @option [String] version (default: nil) Marketing version string + # @option [String] version Marketing version string + # @option [String] build_number Build number # private_lane :macos_update_version_and_build_number_config do |options| version = options[:version] build_number = options[:build_number] File.write(VERSION_CONFIG_PATH, "#{VERSION_CONFIG_DEFINITION} = #{version}\n") - File.write(APP_STORE_BUILD_NUMBER_CONFIG_PATH, "#{BUILD_NUMBER_CONFIG_DEFINITION} = #{build_number}\n") + File.write(BUILD_NUMBER_CONFIG_PATH, "#{BUILD_NUMBER_CONFIG_DEFINITION} = #{build_number}\n") git_commit( path: [ VERSION_CONFIG_PATH, - APP_STORE_BUILD_NUMBER_CONFIG_PATH + BUILD_NUMBER_CONFIG_PATH ], message: "Bump version to #{version} (#{build_number})" ) - sh('git', 'push') end - # Reads App Store build number from the config file + # Reads build number from the config file # # @return [String] build number read from the file, or nil in case of failure # def macos_current_build_number current_build_number = 0 - file_data = File.read(APP_STORE_BUILD_NUMBER_CONFIG_PATH).split("\n") + file_data = File.read(BUILD_NUMBER_CONFIG_PATH).split("\n") file_data.each do |line| current_build_number = line.split('=')[1].strip.to_i if line.start_with?(BUILD_NUMBER_CONFIG_DEFINITION) end diff --git a/fastlane/README.md b/fastlane/README.md index 234080e25a..5c1f6c9f7f 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -55,6 +55,22 @@ Updates App Store metadata Executes the release preparation work in the repository +### mac bump_internal_release + +```sh +[bundle exec] fastlane mac bump_internal_release +``` + +Prepares new internal release on top of an existing one + +### mac set_version + +```sh +[bundle exec] fastlane mac set_version +``` + +Executes the release preparation work in the repository + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/scripts/README.md b/scripts/README.md index 65f1529ac2..6a0d82c3b4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -2,7 +2,6 @@ * [archive.sh](#archivesh-create-notarized-application-build) * [find-private-symbols.sh](#find-private-symbolssh-check-a-binary-for-private-api-usage) -* [sparkle-sandbox.sh](#sparkle-sandboxsh-test-automatic-updates-via-sparkle-locally) * [update-embedded.sh](#update-embeddedsh-update-embedded-tracker-data-set-and-privacy-config) ## `archive.sh`: Create notarized application build @@ -98,84 +97,6 @@ To check for private API symbols in the app: $ ./scripts/find_private_symbols.sh DuckDuckGo.app/Contents/MacOS/DuckDuckGo -## `sparkle-sandbox.sh`: Test automatic updates via Sparkle locally - -This script imitates real-life scenario of automatic app update via Sparkle. - -We use [Sparkle](https://sparkle-project.org/) to notify users about app -updates. The working principle is as follows: -* we upload our app's DMGs to an online storage -* we generate an XML file (in RSS feed format) called `appcast.xml` and keep it - online alongside the DMGs -* the app uses Sparkle framework to check the `appcast.xml` file for updates - and presents a pop-up window to users whenever there is an update available. - -For this to work, we need an online storage, and we only have a single, -production storage. This script helps with that by creating a temporary online -storage from your local machine and crafting two application builds that -would use this temporary storage for reading update info. - -### High-level script walkthrough - -1. Create a directory called `cdn` under main directory of the repository. -1. Run `ngrok` to create a HTTPS tunnel from your local machine's cdn - directory to the internet. -1. Update `SUFeedURL` entry in the app `Info.plist` file. -1. Make a notarized Product Review build and put the DMG in the `cdn` directory. -1. Bump the version by 0.0.1 and repeat the step above. -1. Generate `appcast.xml` file for these two DMG images. -1. Wait here and let the user test the setup. -1. Stop `ngrok` when the user is done testing. - -### Features - -1. Ngrok tunnel creates a semi-random URL that is available publicly. - To increase security, the script generates random Basic Auth credentials and - makes `ngrok` require them for authentication. -1. Script can be paused before building both app versions, so that you can make - changes to files, or even switch branches. The script itself is copied over - to a temporary directory so that you could switch to an old branch that - didn't have this script. - -**Important:** When testing a new feature, be sure to use interactive mode (`-i`) -and create the first build from the main branch (to simulate upgrading from -a previous release). - -### Requirements - -#### ngrok - -You will need `ngrok` version 3 or above to run this script. To create file -tunnels, you need to be a registered ngrok user which means you need to create -a free account. Go to https://ngrok.com and follow instructions to sign up and -set up ngrok installation. Most importantly, you need to provide the _authtoken_ -so that the app recognizes your user. - -#### Apple - -The script calls `archive.sh` to create notarized builds, so all the -requirements related to Apple account and app-specific password apply here too. -Asana access token is not required, but `create-dmg` and `jq` are required. - -### Sparkle - -To generate `appcast.xml` files the `generate_appcast` binary from Sparkle release is required. It should be present in `$PATH`. - -### Usage - -Run the script with 2 builds off the current branch: - - $ ./scripts/sparkle-sandbox.sh - -Run in interactive mode: - - $ ./scripts/sparkle-sandbox.sh -i - -Display all available parameters: - - $ ./scripts/sparkle-sandbox.sh -h - - ## `update-embedded.sh`: Update embedded Tracker Data Set and Privacy Config This script checks app's source code for ETag values of Tracker Data Set diff --git a/scripts/appcast_manager/appcastManager.swift b/scripts/appcast_manager/appcastManager.swift index 82cd04c7b3..3dac7586eb 100755 --- a/scripts/appcast_manager/appcastManager.swift +++ b/scripts/appcast_manager/appcastManager.swift @@ -72,7 +72,7 @@ NAME SYNOPSIS appcastManager --release-to-internal-channel --dmg --release-notes - appcastManager --release-to-public-channel --version [--release-notes ] + appcastManager --release-to-public-channel --version [--release-notes ] appcastManager --release-hotfix-to-public-channel --dmg --release-notes appcastManager --help @@ -85,9 +85,10 @@ DESCRIPTION appcastManager --release-to-internal-channel --dmg /path/to/app.dmg --release-notes /path/to/notes.txt --release-to-public-channel - Releases an app update to the public channel. Requires the version number to be released. Optionally, a path to the release notes can be provided. + Releases an app update to the public channel. Requires the identifier of the version to be released + (marketing version + build number, dot-separated). Optionally, a path to the release notes can be provided. Example: - appcastManager --release-to-public-channel --version 1.2.3 --release-notes /path/to/notes.txt + appcastManager --release-to-public-channel --version 1.2.3.45 --release-notes /path/to/notes.txt --release-hotfix-to-public-channel Releases a hotfix app update to the public channel. Requires a path to the DMG file and a path to the release notes. @@ -122,50 +123,64 @@ case .releaseToInternalChannel, .releaseHotfixToPublicChannel: handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURL) // Extract version number from DMG file name - let versions = getVersionFromDMGFileName(dmgURL: dmgURL) + let versionNumber = getVersionNumberFromDMGFileName(dmgURL: dmgURL) // Differentiate between the two actions if arguments.action == .releaseToInternalChannel { - runGenerateAppcast(withVersions: versions, channel: "internal-channel") + runGenerateAppcast(with: versionNumber, channel: "internal-channel") } else { - runGenerateAppcast(withVersions: versions) + runGenerateAppcast(with: versionNumber) } case .releaseToPublicChannel: - guard let version = arguments.parameters["--version"] else { + guard let versionIdentifier = arguments.parameters["--version"] else { print("Missing required version parameter for action '--release-to-public-channel'") exit(1) } + let versionNumber = extractVersionNumber(from: versionIdentifier) + print("Action: Release to public channel") - print("Version: \(version)") + print("Version: \(versionIdentifier)") performCommonChecksAndOperations() - // Verify version - if !verifyVersion(version: version, atDirectory: specificDir) { + guard let dmgFileName = findDMG(for: versionIdentifier, in: specificDir) else { + print("Version \(versionIdentifier) does not exist in the downloaded appcast items.") exit(1) } + print("Verified: Version \(versionIdentifier) exists in the downloaded appcast items: \(dmgFileName)") // Handle release notes if provided if let releaseNotesPath = arguments.parameters["--release-notes"] { print("Release Notes Path: \(releaseNotesPath)") - let dmgURLForPublic = specificDir.appendingPathComponent(getDmgFilename(for: version)) + let dmgURLForPublic = specificDir.appendingPathComponent(dmgFileName) handleReleaseNotesFile(path: releaseNotesPath, updatesDirectoryURL: specificDir, dmgURL: dmgURLForPublic) } else { print("No new release notes provided. Keeping existing release notes.") } // Process appcast content - if !processAppcastRemovingVersion(version: version, appcastFilePath: appcastFilePath) { + guard processAppcast(removing: versionNumber, appcastFilePath: appcastFilePath) else { exit(1) } + print("Version \(versionIdentifier) removed from the appcast.") - runGenerateAppcast(withVersions: version, rolloutInterval: "43200") + runGenerateAppcast(with: versionNumber, rolloutInterval: "43200") } // MARK: - Common +func extractVersionNumber(from versionIdentifier: String) -> String { + let components = versionIdentifier.components(separatedBy: ".") + guard components.count == 4 else { + print("Invalid version identifier format. Expected 'X.Y.Z.B'") + exit(1) + } + let versionNumber = components[3] + return versionNumber +} + func performCommonChecksAndOperations() { // Check if generate_appcast is recent guard checkSparkleToolRecency(toolName: "generate_appcast"), @@ -184,10 +199,6 @@ func performCommonChecksAndOperations() { AppcastDownloader().download() } -func getDmgFilename(for version: String) -> String { - return "duckduckgo-\(version).dmg" -} - // MARK: - Checking the recency of Sparkle tools func checkSparkleToolRecency(toolName: String) -> Bool { @@ -429,18 +440,21 @@ func handleReleaseNotesFile(path: String, updatesDirectoryURL: URL, dmgURL: URL) } } -func getVersionFromDMGFileName(dmgURL: URL) -> String { +func getVersionNumberFromDMGFileName(dmgURL: URL) -> String { // Extract version number from DMG file name let filename = dmgURL.lastPathComponent let components = filename.components(separatedBy: "-") guard components.count >= 2 else { - print("Invalid DMG file name format. Expected 'duckduckgo-X.Y.Z.dmg'") + print("Invalid DMG file name format. Expected 'duckduckgo-X.Y.Z.B.dmg'") exit(1) } let versionWithExtension = components[1] let versionComponents = versionWithExtension.components(separatedBy: ".dmg") - let versions = versionComponents[0] - return versions + let versionSubcomponents = versionComponents[0].split(separator: ".").map(String.init) + if versionSubcomponents.count > 3 { + return String(versionSubcomponents[3]) + } + return String(versionComponents[0]) } // MARK: - Handling of DMG Files @@ -462,31 +476,27 @@ func handleDMGFile(dmgPath: String, updatesDirectoryURL: URL) -> URL? { } } -func verifyVersion(version: String, atDirectory dir: URL) -> Bool { - let expectedDMGFileName = getDmgFilename(for: version) - let expectedDMGFilePath = dir.appendingPathComponent(expectedDMGFileName).path - if FileManager.default.fileExists(atPath: expectedDMGFilePath) { - print("Verified: Version \(version) exists in the downloaded appcast items.") - return true - } else { - print("Version \(version) does not exist in the downloaded appcast items.") - return false +func findDMG(for versionIdentifier: String, in dir: URL) -> String? { + let fileURL = dir.appending(component: "duckduckgo-\(versionIdentifier).dmg") + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory), isDirectory.boolValue == false else { + return nil } + return fileURL.lastPathComponent } // MARK: - Processing of Appcast -func processAppcastRemovingVersion(version: String, appcastFilePath: URL) -> Bool { +func processAppcast(removing versionNumber: String, appcastFilePath: URL) -> Bool { guard let appcastContent = readAppcastContent(from: appcastFilePath) else { print("Failed to read the appcast file.") return false } - guard let modifiedAppcastContent = removeVersionFromAppcast(version, appcastContent: appcastContent) else { - print("Failed to remove version \(version) from the appcast.") + guard let modifiedAppcastContent = removeVersionFromAppcast(versionNumber, appcastContent: appcastContent) else { + print("Failed to remove version #\(versionNumber) from the appcast.") return false } writeAppcastContent(modifiedAppcastContent, to: appcastFilePath) - print("Version \(version) removed from the appcast.") return true } @@ -495,7 +505,7 @@ func readAppcastContent(from filePath: URL) -> String? { } func removeVersionFromAppcast(_ version: String, appcastContent: String) -> String? { - let pattern = "(\\s*\\s*\(version)\\s*.*?)" + let pattern = "(.*?\(version).*?)" guard let regex = try? NSRegularExpression(pattern: pattern, options: .dotMatchesLineSeparators), let startMatch = regex.firstMatch(in: appcastContent, range: NSRange(appcastContent.startIndex..., in: appcastContent)), @@ -521,7 +531,7 @@ func writeAppcastContent(_ content: String, to filePath: URL) { // MARK: - Generating of New Appcast -func runGenerateAppcast(withVersions versions: String, channel: String? = nil, rolloutInterval: String? = nil) { +func runGenerateAppcast(with versionNumber: String, channel: String? = nil, rolloutInterval: String? = nil) { // Check if backup file already exists and remove it if FileManager.default.fileExists(atPath: backupFileURL.path) { do { @@ -545,7 +555,7 @@ func runGenerateAppcast(withVersions versions: String, channel: String? = nil, r var commandComponents: [String] = [] commandComponents.append("generate_appcast") - commandComponents.append("--versions \(versions)") + commandComponents.append("--versions \(versionNumber)") commandComponents.append("--maximum-versions \(maximumVersions)") commandComponents.append("--maximum-deltas \(maximumDeltas)") @@ -584,7 +594,7 @@ func runGenerateAppcast(withVersions versions: String, channel: String? = nil, r } // Get and save the diff - let diffResult = shell("diff", backupAppcastFilePath, appcastFilePath.path) + let diffResult = shell("diff", "-u", backupAppcastFilePath, appcastFilePath.path) let diffFilePath = specificDir.appendingPathComponent("appcast_diff.txt").path do { try diffResult.write(toFile: diffFilePath, atomically: true, encoding: .utf8) diff --git a/scripts/archive.sh b/scripts/archive.sh index fe16f89283..b0e9b89e27 100755 --- a/scripts/archive.sh +++ b/scripts/archive.sh @@ -132,18 +132,20 @@ set_up_environment() { team_id=$(security find-certificate -c "Developer ID Application: Duck" | grep "alis" | awk 'NF { print $NF }' | tr -d \(\)\") export_options_plist="${cwd}/assets/ExportOptions.plist" + source "${cwd}/helpers/version.sh" if [[ -n "${override_version}" ]]; then app_version="${override_version}" else - source "${cwd}/helpers/version.sh" app_version=$(get_app_version "${scheme}") fi + build_number=$(get_build_number "${scheme}") + version_identifier="${app_version}.${build_number}" app_path="${workdir}/${app_name}.app" dsym_path="${archive}/dSYMs" - output_app_zip_path="${workdir}/DuckDuckGo-${app_version}.zip" - output_dsym_zip_path="${workdir}/DuckDuckGo-${app_version}-dSYM.zip" + output_app_zip_path="${workdir}/DuckDuckGo-${version_identifier}.zip" + output_dsym_zip_path="${workdir}/DuckDuckGo-${version_identifier}-dSYM.zip" } get_developer_credentials() { @@ -232,7 +234,7 @@ archive_and_export() { local log_formatter setup_log_formatter - echo "Building and archiving the app (version ${app_version}) ..." + echo "Building and archiving the app version ${app_version} (${build_number}) ..." local derived_data="${workdir}/DerivedData" rm -rf "${derived_data}" @@ -242,8 +244,8 @@ archive_and_export() { -configuration "${configuration}" \ -archivePath "${archive}" \ -derivedDataPath "${derived_data}" \ - CURRENT_PROJECT_VERSION="${app_version}" \ MARKETING_VERSION="${app_version}" \ + CURRENT_PROJECT_VERSION="${build_number}" \ RELEASE_PRODUCT_NAME_OVERRIDE=DuckDuckGo \ 2>&1 \ | ${log_formatter} @@ -300,7 +302,7 @@ create_dmg() { local dmg_dir="${workdir}/dmg" local dmg_background="${cwd}/assets/dmg-background.png" - dmg_output_path="${workdir}/duckduckgo-${app_version}.dmg" + dmg_output_path="${workdir}/duckduckgo-${version_identifier}.dmg" rm -rf "${dmg_dir}" "${dmg_output_path}" mkdir -p "${dmg_dir}" @@ -316,7 +318,7 @@ create_dmg() { export_app_version_to_environment() { if [[ -n "${GITHUB_ENV}" ]]; then - echo "app-version=${app_version}" >> "${GITHUB_ENV}" + echo "app-version=${version_identifier}" >> "${GITHUB_ENV}" echo "app-name=${app_name}" >> "${GITHUB_ENV}" fi } diff --git a/scripts/helpers/version.sh b/scripts/helpers/version.sh index 122abbb94a..06c49a8ccc 100644 --- a/scripts/helpers/version.sh +++ b/scripts/helpers/version.sh @@ -2,13 +2,14 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" -_get_marketing_version() { +_extract_build_setting() { local scheme="$1" + local variable_name="$2" xcrun xcodebuild \ -scheme "${scheme}" \ -showBuildSettings 2>/dev/null \ - | grep MARKETING_VERSION \ + | grep "${variable_name}" \ | awk '{print $3;}' } @@ -16,7 +17,7 @@ get_app_version() { local scheme="$1" local version - if ! version="$(_get_marketing_version "${scheme}")"; then + if ! version="$(_extract_build_setting "${scheme}" MARKETING_VERSION)"; then read -r -d '' reason <<- EOF Failed to retrieve app version from Xcode project settings. Make sure that the following command works: @@ -29,7 +30,19 @@ get_app_version() { echo "${version}" } -bump_version() { - local original_version="$1" - awk -F. '{ $NF++; print; }' OFS=. <<< "${original_version}" +get_build_number() { + local scheme="$1" + local build_number + + if ! build_number="$(_extract_build_setting "${scheme}" CURRENT_PROJECT_VERSION)"; then + read -r -d '' reason <<- EOF + Failed to retrieve build number from Xcode project settings. + Make sure that the following command works: + xcrun xcodebuild -scheme "${scheme}" -showBuildSettings + + EOF + die "${reason}" + fi + + echo "${build_number}" } diff --git a/scripts/sparkle-sandbox.sh b/scripts/sparkle-sandbox.sh deleted file mode 100755 index 9b7f19cc8c..0000000000 --- a/scripts/sparkle-sandbox.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -if ! [[ $common_sh ]]; then - cwd="$(dirname "${BASH_SOURCE[0]}")" - source "${cwd}/helpers/common.sh" - execute_from_tmp "${BASH_SOURCE[0]}" "$@" -fi - -cdn_dir="${PWD}/cdn" - -print_usage_and_exit() { - local reason=$1 - - cat <<- EOF - Usage: - $ $(basename "$0") [-i] - - Options: - -h Print this message - -i Interactive mode: stop the script to allow switching branches and/or making - changes to the code before building the app each time. - - EOF - - die "${reason}" -} - -read_command_line_arguments() { - while getopts 'hi' OPTION; do - case "${OPTION}" in - h) - print_usage_and_exit - ;; - i) - interactive=1 - ;; - *) - print_usage_and_exit "Unknown option '${OPTION}'" - ;; - esac - done - - shift $((OPTIND-1)) -} - -check_ngrok() { - if ! command -v ngrok &> /dev/null; then - cat <<- EOF - ngrok is required to serve local directories online. - Install it with: - $ brew install ngrok - - You will also need to create an account at https://dashboard.ngrok.com/signup - and install authtoken (available from your dashboard: https://dashboard.ngrok.com/get-started/your-authtoken) - - EOF - die "ngrok not installed" - fi -} - -kill_existing_ngrok_processes() { - local ngrok_pids - read -ra ngrok_pids <<< "$(pgrep ngrok)" - - if [[ -n "${ngrok_pids[*]}" ]]; then - printf '%s' 'Killing existing ngrok processes ... ' - - if kill "${ngrok_pids[*]}"; then - echo "Done" - else - echo "Failed to kill ngrok. Please stop any ngrok processes and restart the script." - exit 1 - fi - fi -} - -clean_up() { - kill_existing_ngrok_processes - exit 0 -} - -prepare_fake_cdn_directory() { - rm -rf "${cdn_dir}" - mkdir -p "${cdn_dir}" -} - -start_ngrok() { - local basic_auth - # shellcheck disable=2119 - basic_auth="$(random_string):$(random_string)" - echo "Starting ngrok ..." - ngrok http --log stdout --basic-auth "${basic_auth}" "file://${cdn_dir}" >/dev/null & - ngrok_pid=$! - - # Wait until tunnel is created - while true; do - sleep 1 - server_url=$( - curl -s http://localhost:4040/api/tunnels \ - | jq '.tunnels[0].public_url' \ - | tr -d '"' \ - | sed -E "s~(https://)~\1${basic_auth}@~" - ) - - if [[ ${server_url} != "null" ]]; then - break - fi - done - - echo "Running ngrok on ${server_url} (pid ${ngrok_pid})" - trap clean_up SIGINT -} - -get_feed_url() { - plutil -extract SUFeedURL raw DuckDuckGo/Info.plist -} - -update_feed_url() { - local value="$1" - plutil -replace SUFeedURL -string "${value}" DuckDuckGo/Info.plist -} - -build_app_and_copy_dmg() { - local version="$1" - - echo "Building app version ${version}. This can take a couple of minutes ..." - - feed_url="$(get_feed_url)" - update_feed_url "${server_url}/appcast.xml" - - "${cwd}/archive.sh" review -d -s -v "${version}" - cp -f "${PWD}/release/duckduckgo-${version}.dmg" "${cdn_dir}" - - update_feed_url "${feed_url}" - - echo "Done building app version ${version}." -} - -wait_for_sigint() { - cat <<- EOF - - Temporary file server (pid ${ngrok_pid}) is running on: - ${server_url} - - Directory contents: - $(find "${cdn_dir}" -depth 1 | awk -F/ '{ print $NF; }' | sort) - - When you're done testing, press CTRL+C to stop ngrok tunnel. - - EOF - - read -r -d '' _ -} - -main() { - read_command_line_arguments "$@" - check_ngrok - kill_existing_ngrok_processes - prepare_fake_cdn_directory - start_ngrok - - if [[ ${interactive} ]]; then - cat <<- EOF - - Prepare the code for building source version (pre-update). - Don't bother updating SUFeedURL in the Info.plist file or app version in project settings - as they will be set to correct values automatically. Press Return key when ready. - EOF - - read -r - fi - - source "${cwd}/helpers/version.sh" - base_app_version="$(get_app_version "Product Review Release")" - build_app_and_copy_dmg "${base_app_version}" - - - if [[ ${interactive} ]]; then - cat <<- EOF - - Prepare the code for building target version (that Sparkle would offer to update to). - Don't bother updating SUFeedURL in the Info.plist file or app version in project settings - as they will be set to correct values automatically. Press Return key when ready. - EOF - - read -r - fi - - bumped_version="$(bump_version "${base_app_version}")" - build_app_and_copy_dmg "${bumped_version}" - - echo "Generating appcast.xml ..." - generate_appcast "${cdn_dir}" - - wait_for_sigint -} - -main "$@" diff --git a/set_version.sh b/set_version.sh deleted file mode 100755 index d40be9542f..0000000000 --- a/set_version.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -version_number="$1" - -if [[ -z "${version_number}" ]]; then - echo 'Usage: ./set_version.sh VERSION_NUMBER' - echo 'Example: ./set_version.sh 0.28.6' - echo "Current version: $(cut -d ' ' -f 3