diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift index bde81ed2..7f36ed90 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift @@ -3,6 +3,7 @@ import ArgumentParser import FileSystem import Foundation +import Model struct TestFlightPushCommand: CommonParsableCommand { @@ -26,53 +27,22 @@ struct TestFlightPushCommand: CommonParsableCommand { func run() throws { let service = try makeService() - if dryRun { - print("'Dry Run' mode activated, changes will not be applied. \n") - } - print("Loading local TestFlight configs... \n") let localConfigurations = try [TestFlightConfiguration](from: inputPath) print("Loading server TestFlight configs... \n") - let serverConfigs = try service.pullTestFlightConfigurations() - - try serverConfigs.forEach { serverConfig in - guard - let localConfig = localConfigurations - .first(where: { $0.app.id == serverConfig.app.id }) else { - return - } - - let appId = localConfig.app.id - - print("Syncing App '\(localConfig.app.bundleId ?? appId)':") - - try processAppSharedTesters( - localTesters: localConfig.testers, - serverTesters: serverConfig.testers, - appId: appId, - service: service - ) - - let localBetagroups = localConfig.betagroups - let serverBetagroups = serverConfig.betagroups - - try processBetaGroups( - localGroups: localBetagroups, - serverGroups: serverBetagroups, - appId: appId, - service: service - ) + let serverConfigurations = try service.pullTestFlightConfigurations() - try processTestersInGroups( - localGroups: localBetagroups, - serverGroups: serverBetagroups, - sharedTesters: localConfig.testers, - service: service - ) + let actions = compare( + serverConfigurations: serverConfigurations, + with: localConfigurations + ) - print("Syncing completed. \n") + if dryRun { + render(actions: actions) + } else { + try process(actions: actions, with: service) } print("Refreshing local configurations...") @@ -82,160 +52,237 @@ struct TestFlightPushCommand: CommonParsableCommand { print("Refreshing completed.") } - private func processAppSharedTesters( - localTesters: [BetaTester], - serverTesters: [BetaTester], - appId: String, - service: AppStoreConnectService - ) throws { - // 1. compare shared testers in app - let sharedTestersHandleStrategies = SyncResourceComparator( - localResources: localTesters, - serverResources: serverTesters - ) - .compare() + func render(actions: [AppSyncActions]) { + print("'Dry Run' mode activated, changes will not be applied. \n") - // 1.1 handle shared testers delete only - if sharedTestersHandleStrategies.isNotEmpty { - print("- App Testers Changes: ") - try processAppTesterStrategies(sharedTestersHandleStrategies, appId: appId, service: service) + actions.forEach { + print("\($0.app.name ?? ""):") + // 1. app testers + print("- Testers in App: ") + $0.appTestersSyncActions.forEach { $0.render(dryRun: dryRun) } + + // 2. BetaGroups in App + print("- BetaGroups in App: ") + $0.betaGroupSyncActions.forEach { $0.render(dryRun: dryRun) } + + // 3. Testers in BetaGroup + print("- Testers In Beta Group: ") + $0.testerInGroupsAction.forEach { + print("\($0.betaGroup.groupName):") + $0.testerActions.forEach { $0.render(dryRun: dryRun) } + } } } - private func processBetaGroups( - localGroups: [BetaGroup], - serverGroups: [BetaGroup], - appId: String, - service: AppStoreConnectService - ) throws { - // 2. compare beta groups - let betaGroupHandlingStrategies = SyncResourceComparator( - localResources: localGroups, - serverResources: serverGroups - ).compare() - - // 2.1 handle groups create, update, delete - if betaGroupHandlingStrategies.isNotEmpty { - print("- Beta Group Changes: ") - try processBetagroupsStrategies(betaGroupHandlingStrategies, appId: appId, service: service) + private func process(actions: [AppSyncActions], with service: AppStoreConnectService) throws { + try actions.forEach { appAction in + print("\(appAction.app.name ?? ""): ") + // 1. app testers + print("- Testers in App: ") + try processAppTesterActions( + appAction.appTestersSyncActions, + appId: appAction.app.id, + service: service + ) + + // 2. beta groups in app + print("- BetaGroups in App: ") + try processBetagroupsActions( + appAction.betaGroupSyncActions, + appId: appAction.app.id, + service: service + ) + + // 3. testers in beta group + print("- Testers In Beta Group: ") + try appAction.testerInGroupsAction.forEach { + try processTestersInBetaGroupActions( + $0.testerActions, + betagroupId: $0.betaGroup.id!, + appTesters: appAction.appTesters, + service: service + ) + } } } - private func processTestersInGroups( - localGroups: [BetaGroup], - serverGroups: [BetaGroup], - sharedTesters: [BetaTester], - service: AppStoreConnectService - ) throws { - // 3. compare testers in group, perform adding/deleting - try localGroups.forEach { localBetagroup in + private func compare( + serverConfigurations: [TestFlightConfiguration], + with localConfigurations: [TestFlightConfiguration] + ) -> [AppSyncActions] { + return serverConfigurations.compactMap { serverConfiguration in guard - let serverBetagroup = serverGroups.first(where: { $0.id == localBetagroup.id }) else { - return + let localConfiguration = localConfigurations + .first(where: { $0.app.id == serverConfiguration.app.id }) else { + return nil } - let localGroupTesters = localBetagroup.testers + let appTesterSyncActions = SyncResourceComparator( + localResources: localConfiguration.testers, + serverResources: serverConfiguration.testers + ) + .compare() - let serverGroupTesters = serverBetagroup.testers + let betaGroupSyncActions = SyncResourceComparator( + localResources: localConfiguration.betagroups, + serverResources: serverConfiguration.betagroups + ) + .compare() - let testersInGroupHandlingStrategies = SyncResourceComparator( - localResources: localGroupTesters, - serverResources: serverGroupTesters - ).compare() + let testerInGroupsAction = localConfiguration.betagroups.compactMap { localBetagroup -> BetaTestersInGroupActions? in + guard + let serverBetaGroup = serverConfiguration + .betagroups + .first(where: { $0.id == localBetagroup.id }) else { + return nil + } - // 3.1 handling adding/deleting testers per group - if testersInGroupHandlingStrategies.isNotEmpty { - print("- Beta Group '\(serverBetagroup.groupName)' Testers Changes: ") - try processTestersInBetaGroupStrategies( - testersInGroupHandlingStrategies, - betagroupId: serverBetagroup.id!, - appTesters: sharedTesters, - service: service + return BetaTestersInGroupActions( + betaGroup: localBetagroup, + testerActions: SyncResourceComparator( + localResources: localBetagroup.testers, + serverResources: serverBetaGroup.testers + ) + .compare() ) } + + return AppSyncActions( + app: localConfiguration.app, + appTesters: localConfiguration.testers, + appTestersSyncActions: appTesterSyncActions, + betaGroupSyncActions: betaGroupSyncActions, + testerInGroupsAction: testerInGroupsAction + ) } } - func processAppTesterStrategies(_ strategies: [SyncStrategy], appId: String, service: AppStoreConnectService) throws { - if dryRun { - SyncResultRenderer().render(strategies, isDryRun: true) - } else { - try strategies.forEach { strategy in - switch strategy { - case .delete(let betatester): - try service.removeTesterFromApp(testerEmail: betatester.email, appId: appId) - SyncResultRenderer().render(strategies, isDryRun: false) - default: - return - } +// private func processAppSharedTesters( +// localTesters: [BetaTester], +// serverTesters: [BetaTester], +// appId: String, +// service: AppStoreConnectService +// ) -> [SyncAction] { + // 1. compare shared testers in app +// let sharedTestersHandleStrategies = SyncResourceComparator( +// localResources: localTesters, +// serverResources: serverTesters +// ) +// .compare() +// +// return sharedTestersHandleStrategies + + // 1.1 handle shared testers delete only +// if sharedTestersHandleStrategies.isNotEmpty { +// print("- App Testers Changes: ") +// try processAppTesterStrategies(sharedTestersHandleStrategies, appId: appId, service: service) +// } +// } + +// private func processBetaGroups( +// localGroups: [BetaGroup], +// serverGroups: [BetaGroup], +// appId: String, +// service: AppStoreConnectService +// ) -> [SyncAction] { +// // 2. compare beta groups +// return SyncResourceComparator( +// localResources: localGroups, +// serverResources: serverGroups +// ).compare() + +// // 2.1 handle groups create, update, delete +// if betaGroupHandlingStrategies.isNotEmpty { +// print("- Beta Group Changes: ") +// try processBetagroupsStrategies(betaGroupHandlingStrategies, appId: appId, service: service) +// } +// } + +// private func processTestersInGroups( +// localGroups: [FileSystem.BetaGroup], +// serverGroups: [FileSystem.BetaGroup] +// ) -> [SyncAction] { +// // 3. compare testers in group, perform adding/deleting +// return localGroups.flatMap { localBetagroup -> [SyncAction] in +// guard +// let serverBetaGroup = serverGroups.first(where: { $0.id == localBetagroup.id }) else { +// return [] +// } +// +// return SyncResourceComparator( +// localResources: localBetagroup.testers, +// serverResources: serverBetaGroup.testers +// ) +// .compare() +// } +// } + + func processAppTesterActions(_ actions: [SyncAction], appId: String, service: AppStoreConnectService) throws { + try actions.forEach { action in + switch action { + case .delete(let betatester): + try service.removeTesterFromApp(testerEmail: betatester.email, appId: appId) + action.render(dryRun: dryRun) + default: + return } } } - func processBetagroupsStrategies(_ strategies: [SyncStrategy], appId: String, service: AppStoreConnectService) throws { - let renderer = SyncResultRenderer() - - if dryRun { - renderer.render(strategies, isDryRun: true) - } else { - try strategies.forEach { strategy in - switch strategy { - case .create(let betagroup): - _ = try service.createBetaGroup( - appId: appId, - groupName: betagroup.groupName, - publicLinkEnabled: betagroup.publicLinkEnabled ?? false, - publicLinkLimit: betagroup.publicLinkLimit - ) - renderer.render(strategy, isDryRun: false) - case .delete(let betagroup): - try service.deleteBetaGroup(with: betagroup.id!) - renderer.render(strategy, isDryRun: false) - case .update(let betagroup): - try service.updateBetaGroup(betaGroup: betagroup) - renderer.render(strategy, isDryRun: false) - } + func processBetagroupsActions(_ actions: [SyncAction], appId: String, service: AppStoreConnectService) throws { + try actions.forEach { action in + switch action { + case .create(let betagroup): + _ = try service.createBetaGroup( + appId: appId, + groupName: betagroup.groupName, + publicLinkEnabled: betagroup.publicLinkEnabled ?? false, + publicLinkLimit: betagroup.publicLinkLimit + ) + action.render(dryRun: dryRun) + case .delete(let betagroup): + try service.deleteBetaGroup(with: betagroup.id!) + action.render(dryRun: dryRun) + case .update(let betagroup): + try service.updateBetaGroup(betaGroup: betagroup) + action.render(dryRun: dryRun) } } } - func processTestersInBetaGroupStrategies( - _ strategies: [SyncStrategy], + func processTestersInBetaGroupActions( + _ actions: [SyncAction], betagroupId: String, - appTesters: [BetaTester], + appTesters: [FileSystem.BetaTester], service: AppStoreConnectService ) throws { - let renderer = SyncResultRenderer() - - if dryRun { - renderer.render(strategies, isDryRun: true) - } else { - let deletingEmailsWithStrategy = strategies - .compactMap { (strategy: SyncStrategy) -> (email: String, strategy: SyncStrategy)? in - if case .delete(let email) = strategy { - return (email, strategy) + let deletingEmailsWithStrategy = actions + .compactMap { (action: SyncAction) -> + (email: String, strategy: SyncAction)? in + if case .delete(let email) = action { + return (email, action) } return nil } - try service.removeTestersFromGroup( - emails: deletingEmailsWithStrategy.map { $0.email }, - groupId: betagroupId - ) - renderer.render(deletingEmailsWithStrategy.map { $0.strategy }, isDryRun: false) - - let creatingTestersWithStrategy = strategies - .compactMap { (strategy: SyncStrategy) -> - (tester: BetaTester, strategy: SyncStrategy)? in - if case .create(let email) = strategy, - let betatester = appTesters.first(where: { $0.email == email }) { - return (betatester, strategy) - } - return nil + try service.removeTestersFromGroup( + emails: deletingEmailsWithStrategy.map { $0.email }, + groupId: betagroupId + ) + + deletingEmailsWithStrategy.forEach { $0.strategy.render(dryRun: dryRun) } + + let creatingTestersWithStrategy = actions + .compactMap { (strategy: SyncAction) -> + (tester: FileSystem.BetaTester, strategy: SyncAction)? in + if case .create(let email) = strategy, + let betatester = appTesters.first(where: { $0.email == email }) { + return (betatester, strategy) } + return nil + } try creatingTestersWithStrategy.forEach { - try service.inviteBetaTesterToGroups( email: $0.tester.email, groupId: betagroupId, @@ -243,9 +290,9 @@ struct TestFlightPushCommand: CommonParsableCommand { lastName: $0.tester.lastName ) - renderer.render($0.strategy, isDryRun: false) - } + $0.strategy.render(dryRun: dryRun) } + } } diff --git a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift index 81a5efd5..578e79bd 100644 --- a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift +++ b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift @@ -119,15 +119,15 @@ protocol SyncResultRenderable: Equatable { } struct SyncResultRenderer { - func render(_ strategy: [SyncStrategy], isDryRun: Bool) { + func render(_ strategy: [SyncAction], isDryRun: Bool) { strategy.forEach { renderResultText($0, isDryRun) } } - func render(_ strategy: SyncStrategy, isDryRun: Bool) { + func render(_ strategy: SyncAction, isDryRun: Bool) { renderResultText(strategy, isDryRun) } - private func renderResultText(_ strategy: SyncStrategy, _ isDryRun: Bool) { + private func renderResultText(_ strategy: SyncAction, _ isDryRun: Bool) { let resultText: String switch strategy { case .create(let input): diff --git a/Sources/AppStoreConnectCLI/Services/SyncActions.swift b/Sources/AppStoreConnectCLI/Services/SyncActions.swift new file mode 100644 index 00000000..263af1e6 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Services/SyncActions.swift @@ -0,0 +1,51 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import Foundation +import FileSystem +import Model + +struct AppSyncActions { + let app: Model.App + let appTesters: [FileSystem.BetaTester] + + let appTestersSyncActions: [SyncAction] + let betaGroupSyncActions: [SyncAction] + + let testerInGroupsAction: [BetaTestersInGroupActions] +} + +struct BetaTestersInGroupActions { + let betaGroup: FileSystem.BetaGroup + let testerActions: [SyncAction] +} + +extension SyncAction where T == FileSystem.BetaGroup { + func render(dryRun: Bool) { + switch self { + case .create, .delete, .update: + SyncResultRenderer().render(self, isDryRun: dryRun) + } + } +} + +extension SyncAction where T == FileSystem.BetaTester { + func render(dryRun: Bool) { + switch self { + case .delete: + SyncResultRenderer().render(self, isDryRun: dryRun) + default: + return + } + } +} + +extension SyncAction where T == FileSystem.BetaGroup.EmailAddress { + func render(dryRun: Bool) { + switch self { + case .create, .delete: + SyncResultRenderer().render(self, isDryRun: dryRun) + default: + return + } + } +} diff --git a/Sources/AppStoreConnectCLI/Services/SyncResourceComparator.swift b/Sources/AppStoreConnectCLI/Services/SyncResourceComparator.swift index 4689926a..e6275690 100644 --- a/Sources/AppStoreConnectCLI/Services/SyncResourceComparator.swift +++ b/Sources/AppStoreConnectCLI/Services/SyncResourceComparator.swift @@ -2,7 +2,7 @@ import Foundation -enum SyncStrategy: Equatable { +enum SyncAction: Equatable { case delete(T) case create(T) case update(T) @@ -24,10 +24,10 @@ struct SyncResourceComparator { private var localResourcesSet: Set { Set(localResources) } private var serverResourcesSet: Set { Set(serverResources) } - func compare() -> [SyncStrategy] { + func compare() -> [SyncAction] { serverResourcesSet .subtracting(localResourcesSet) - .compactMap { resource -> SyncStrategy? in + .compactMap { resource -> SyncAction? in localResources .contains(where: { resource.compareIdentity == $0.compareIdentity }) ? nil @@ -36,7 +36,7 @@ struct SyncResourceComparator { + localResourcesSet .subtracting(serverResourcesSet) - .compactMap { resource -> SyncStrategy? in + .compactMap { resource -> SyncAction? in serverResourcesSet .contains( where: { resource.compareIdentity == $0.compareIdentity }