Skip to content

Commit 3e4f8fe

Browse files
committed
Introduce AppSyncActions and update TestFlightPushCommand
1 parent 5be708c commit 3e4f8fe

4 files changed

Lines changed: 205 additions & 167 deletions

File tree

Sources/AppStoreConnectCLI/Commands/TestFlight/Sync/TestFlightPushCommand.swift

Lines changed: 147 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ArgumentParser
44
import FileSystem
55
import Foundation
6+
import Model
67

78
struct TestFlightPushCommand: CommonParsableCommand {
89

@@ -26,53 +27,22 @@ struct TestFlightPushCommand: CommonParsableCommand {
2627
func run() throws {
2728
let service = try makeService()
2829

29-
if dryRun {
30-
print("'Dry Run' mode activated, changes will not be applied. \n")
31-
}
32-
3330
print("Loading local TestFlight configs... \n")
3431

3532
let localConfigurations = try [TestFlightConfiguration](from: inputPath)
3633

3734
print("Loading server TestFlight configs... \n")
38-
let serverConfigs = try service.pullTestFlightConfigurations()
39-
40-
try serverConfigs.forEach { serverConfig in
41-
guard
42-
let localConfig = localConfigurations
43-
.first(where: { $0.app.id == serverConfig.app.id }) else {
44-
return
45-
}
46-
47-
let appId = localConfig.app.id
35+
let serverConfigurations = try service.pullTestFlightConfigurations()
4836

49-
print("Syncing App '\(localConfig.app.bundleId ?? appId)':")
50-
51-
try processAppSharedTesters(
52-
localTesters: localConfig.testers,
53-
serverTesters: serverConfig.testers,
54-
appId: appId,
55-
service: service
56-
)
57-
58-
let localBetagroups = localConfig.betagroups
59-
let serverBetagroups = serverConfig.betagroups
60-
61-
try processBetaGroups(
62-
localGroups: localBetagroups,
63-
serverGroups: serverBetagroups,
64-
appId: appId,
65-
service: service
66-
)
67-
68-
try processTestersInGroups(
69-
localGroups: localBetagroups,
70-
serverGroups: serverBetagroups,
71-
sharedTesters: localConfig.testers,
72-
service: service
73-
)
37+
let actions = compare(
38+
serverConfigurations: serverConfigurations,
39+
with: localConfigurations
40+
)
7441

75-
print("Syncing completed. \n")
42+
if dryRun {
43+
render(actions: actions)
44+
} else {
45+
try process(actions: actions, with: service)
7646
}
7747

7848
print("Refreshing local configurations...")
@@ -82,170 +52,187 @@ struct TestFlightPushCommand: CommonParsableCommand {
8252
print("Refreshing completed.")
8353
}
8454

85-
private func processAppSharedTesters(
86-
localTesters: [BetaTester],
87-
serverTesters: [BetaTester],
88-
appId: String,
89-
service: AppStoreConnectService
90-
) throws {
91-
// 1. compare shared testers in app
92-
let sharedTestersHandleStrategies = SyncResourceComparator(
93-
localResources: localTesters,
94-
serverResources: serverTesters
95-
)
96-
.compare()
55+
func render(actions: [AppSyncActions]) {
56+
print("'Dry Run' mode activated, changes will not be applied. \n")
9757

98-
// 1.1 handle shared testers delete only
99-
if sharedTestersHandleStrategies.isNotEmpty {
100-
print("- App Testers Changes: ")
101-
try processAppTesterStrategies(sharedTestersHandleStrategies, appId: appId, service: service)
58+
actions.forEach {
59+
print("\($0.app.name ?? ""):")
60+
// 1. app testers
61+
print("- Testers in App: ")
62+
$0.appTestersSyncActions.forEach { $0.render(dryRun: dryRun) }
63+
64+
// 2. BetaGroups in App
65+
print("- BetaGroups in App: ")
66+
$0.betaGroupSyncActions.forEach { $0.render(dryRun: dryRun) }
67+
68+
// 3. Testers in BetaGroup
69+
print("- Testers In Beta Group: ")
70+
$0.testerInGroupsAction.forEach {
71+
print("\($0.betaGroup.groupName):")
72+
$0.testerActions.forEach { $0.render(dryRun: dryRun) }
73+
}
10274
}
10375
}
10476

105-
private func processBetaGroups(
106-
localGroups: [BetaGroup],
107-
serverGroups: [BetaGroup],
108-
appId: String,
109-
service: AppStoreConnectService
110-
) throws {
111-
// 2. compare beta groups
112-
let betaGroupHandlingStrategies = SyncResourceComparator(
113-
localResources: localGroups,
114-
serverResources: serverGroups
115-
).compare()
116-
117-
// 2.1 handle groups create, update, delete
118-
if betaGroupHandlingStrategies.isNotEmpty {
119-
print("- Beta Group Changes: ")
120-
try processBetagroupsStrategies(betaGroupHandlingStrategies, appId: appId, service: service)
77+
private func process(actions: [AppSyncActions], with service: AppStoreConnectService) throws {
78+
try actions.forEach { appAction in
79+
print("\(appAction.app.name ?? ""): ")
80+
// 1. app testers
81+
print("- Testers in App: ")
82+
try processAppTesterActions(
83+
appAction.appTestersSyncActions,
84+
appId: appAction.app.id,
85+
service: service
86+
)
87+
88+
// 2. beta groups in app
89+
print("- BetaGroups in App: ")
90+
try processBetagroupsActions(
91+
appAction.betaGroupSyncActions,
92+
appId: appAction.app.id,
93+
service: service
94+
)
95+
96+
// 3. testers in beta group
97+
print("- Testers In Beta Group: ")
98+
try appAction.testerInGroupsAction.forEach {
99+
try processTestersInBetaGroupActions(
100+
$0.testerActions,
101+
betagroupId: $0.betaGroup.id!,
102+
appTesters: appAction.appTesters,
103+
service: service
104+
)
105+
}
121106
}
122107
}
123108

124-
private func processTestersInGroups(
125-
localGroups: [BetaGroup],
126-
serverGroups: [BetaGroup],
127-
sharedTesters: [BetaTester],
128-
service: AppStoreConnectService
129-
) throws {
130-
// 3. compare testers in group, perform adding/deleting
131-
try localGroups.forEach { localBetagroup in
109+
private func compare(
110+
serverConfigurations: [TestFlightConfiguration],
111+
with localConfigurations: [TestFlightConfiguration]
112+
) -> [AppSyncActions] {
113+
return serverConfigurations.compactMap { serverConfiguration in
132114
guard
133-
let serverBetagroup = serverGroups.first(where: { $0.id == localBetagroup.id }) else {
134-
return
115+
let localConfiguration = localConfigurations
116+
.first(where: { $0.app.id == serverConfiguration.app.id }) else {
117+
return nil
135118
}
136119

137-
let localGroupTesters = localBetagroup.testers
120+
let appTesterSyncActions = SyncResourceComparator(
121+
localResources: localConfiguration.testers,
122+
serverResources: serverConfiguration.testers
123+
)
124+
.compare()
138125

139-
let serverGroupTesters = serverBetagroup.testers
126+
let betaGroupSyncActions = SyncResourceComparator(
127+
localResources: localConfiguration.betagroups,
128+
serverResources: serverConfiguration.betagroups
129+
)
130+
.compare()
140131

141-
let testersInGroupHandlingStrategies = SyncResourceComparator(
142-
localResources: localGroupTesters,
143-
serverResources: serverGroupTesters
144-
).compare()
132+
let testerInGroupsAction = localConfiguration.betagroups.compactMap { localBetagroup -> BetaTestersInGroupActions? in
133+
guard
134+
let serverBetaGroup = serverConfiguration
135+
.betagroups
136+
.first(where: { $0.id == localBetagroup.id }) else {
137+
return nil
138+
}
145139

146-
// 3.1 handling adding/deleting testers per group
147-
if testersInGroupHandlingStrategies.isNotEmpty {
148-
print("- Beta Group '\(serverBetagroup.groupName)' Testers Changes: ")
149-
try processTestersInBetaGroupStrategies(
150-
testersInGroupHandlingStrategies,
151-
betagroupId: serverBetagroup.id!,
152-
appTesters: sharedTesters,
153-
service: service
140+
return BetaTestersInGroupActions(
141+
betaGroup: localBetagroup,
142+
testerActions: SyncResourceComparator(
143+
localResources: localBetagroup.testers,
144+
serverResources: serverBetaGroup.testers
145+
)
146+
.compare()
154147
)
155148
}
149+
150+
return AppSyncActions(
151+
app: localConfiguration.app,
152+
appTesters: localConfiguration.testers,
153+
appTestersSyncActions: appTesterSyncActions,
154+
betaGroupSyncActions: betaGroupSyncActions,
155+
testerInGroupsAction: testerInGroupsAction
156+
)
156157
}
157158
}
158159

159-
func processAppTesterStrategies(_ strategies: [SyncStrategy<FileSystem.BetaTester>], appId: String, service: AppStoreConnectService) throws {
160-
if dryRun {
161-
SyncResultRenderer<FileSystem.BetaTester>().render(strategies, isDryRun: true)
162-
} else {
163-
try strategies.forEach { strategy in
164-
switch strategy {
165-
case .delete(let betatester):
166-
try service.removeTesterFromApp(testerEmail: betatester.email, appId: appId)
167-
SyncResultRenderer<FileSystem.BetaTester>().render(strategies, isDryRun: false)
168-
default:
169-
return
170-
}
160+
func processAppTesterActions(_ actions: [SyncAction<FileSystem.BetaTester>], appId: String, service: AppStoreConnectService) throws {
161+
try actions.forEach { action in
162+
switch action {
163+
case .delete(let betatester):
164+
try service.removeTesterFromApp(testerEmail: betatester.email, appId: appId)
165+
action.render(dryRun: dryRun)
166+
default:
167+
return
171168
}
172169
}
173170
}
174171

175-
func processBetagroupsStrategies(_ strategies: [SyncStrategy<FileSystem.BetaGroup>], appId: String, service: AppStoreConnectService) throws {
176-
let renderer = SyncResultRenderer<FileSystem.BetaGroup>()
177-
178-
if dryRun {
179-
renderer.render(strategies, isDryRun: true)
180-
} else {
181-
try strategies.forEach { strategy in
182-
switch strategy {
183-
case .create(let betagroup):
184-
_ = try service.createBetaGroup(
185-
appId: appId,
186-
groupName: betagroup.groupName,
187-
publicLinkEnabled: betagroup.publicLinkEnabled ?? false,
188-
publicLinkLimit: betagroup.publicLinkLimit
189-
)
190-
renderer.render(strategy, isDryRun: false)
191-
case .delete(let betagroup):
192-
try service.deleteBetaGroup(with: betagroup.id!)
193-
renderer.render(strategy, isDryRun: false)
194-
case .update(let betagroup):
195-
try service.updateBetaGroup(betaGroup: betagroup)
196-
renderer.render(strategy, isDryRun: false)
197-
}
172+
func processBetagroupsActions(_ actions: [SyncAction<FileSystem.BetaGroup>], appId: String, service: AppStoreConnectService) throws {
173+
try actions.forEach { action in
174+
switch action {
175+
case .create(let betagroup):
176+
_ = try service.createBetaGroup(
177+
appId: appId,
178+
groupName: betagroup.groupName,
179+
publicLinkEnabled: betagroup.publicLinkEnabled ?? false,
180+
publicLinkLimit: betagroup.publicLinkLimit
181+
)
182+
action.render(dryRun: dryRun)
183+
case .delete(let betagroup):
184+
try service.deleteBetaGroup(with: betagroup.id!)
185+
action.render(dryRun: dryRun)
186+
case .update(let betagroup):
187+
try service.updateBetaGroup(betaGroup: betagroup)
188+
action.render(dryRun: dryRun)
198189
}
199190
}
200191
}
201192

202-
func processTestersInBetaGroupStrategies(
203-
_ strategies: [SyncStrategy<BetaGroup.EmailAddress>],
193+
func processTestersInBetaGroupActions(
194+
_ actions: [SyncAction<FileSystem.BetaGroup.EmailAddress>],
204195
betagroupId: String,
205-
appTesters: [BetaTester],
196+
appTesters: [FileSystem.BetaTester],
206197
service: AppStoreConnectService
207198
) throws {
208-
let renderer = SyncResultRenderer<FileSystem.BetaGroup.EmailAddress>()
209-
210-
if dryRun {
211-
renderer.render(strategies, isDryRun: true)
212-
} else {
213-
let deletingEmailsWithStrategy = strategies
214-
.compactMap { (strategy: SyncStrategy<BetaGroup.EmailAddress>) -> (email: String, strategy: SyncStrategy<BetaGroup.EmailAddress>)? in
215-
if case .delete(let email) = strategy {
216-
return (email, strategy)
199+
let deletingEmailsWithStrategy = actions
200+
.compactMap { (action: SyncAction<FileSystem.BetaGroup.EmailAddress>) ->
201+
(email: String, strategy: SyncAction<FileSystem.BetaGroup.EmailAddress>)? in
202+
if case .delete(let email) = action {
203+
return (email, action)
217204
}
218205
return nil
219206
}
220207

221-
try service.removeTestersFromGroup(
222-
emails: deletingEmailsWithStrategy.map { $0.email },
223-
groupId: betagroupId
224-
)
225-
renderer.render(deletingEmailsWithStrategy.map { $0.strategy }, isDryRun: false)
226-
227-
let creatingTestersWithStrategy = strategies
228-
.compactMap { (strategy: SyncStrategy<BetaGroup.EmailAddress>) ->
229-
(tester: BetaTester, strategy: SyncStrategy<BetaGroup.EmailAddress>)? in
230-
if case .create(let email) = strategy,
231-
let betatester = appTesters.first(where: { $0.email == email }) {
232-
return (betatester, strategy)
233-
}
234-
return nil
208+
try service.removeTestersFromGroup(
209+
emails: deletingEmailsWithStrategy.map { $0.email },
210+
groupId: betagroupId
211+
)
212+
213+
deletingEmailsWithStrategy.forEach { $0.strategy.render(dryRun: dryRun) }
214+
215+
let creatingTestersWithStrategy = actions
216+
.compactMap { (strategy: SyncAction<FileSystem.BetaGroup.EmailAddress>) ->
217+
(tester: FileSystem.BetaTester, strategy: SyncAction<FileSystem.BetaGroup.EmailAddress>)? in
218+
if case .create(let email) = strategy,
219+
let betatester = appTesters.first(where: { $0.email == email }) {
220+
return (betatester, strategy)
235221
}
222+
return nil
223+
}
236224

237225
try creatingTestersWithStrategy.forEach {
238-
239226
try service.inviteBetaTesterToGroups(
240227
email: $0.tester.email,
241228
groupId: betagroupId,
242229
firstName: $0.tester.firstName,
243230
lastName: $0.tester.lastName
244231
)
245232

246-
renderer.render($0.strategy, isDryRun: false)
247-
}
233+
$0.strategy.render(dryRun: dryRun)
248234
}
235+
249236
}
250237

251238
}

Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,15 @@ protocol SyncResultRenderable: Equatable {
119119
}
120120

121121
struct SyncResultRenderer<T: SyncResultRenderable> {
122-
func render(_ strategy: [SyncStrategy<T>], isDryRun: Bool) {
122+
func render(_ strategy: [SyncAction<T>], isDryRun: Bool) {
123123
strategy.forEach { renderResultText($0, isDryRun) }
124124
}
125125

126-
func render(_ strategy: SyncStrategy<T>, isDryRun: Bool) {
126+
func render(_ strategy: SyncAction<T>, isDryRun: Bool) {
127127
renderResultText(strategy, isDryRun)
128128
}
129129

130-
private func renderResultText(_ strategy: SyncStrategy<T>, _ isDryRun: Bool) {
130+
private func renderResultText(_ strategy: SyncAction<T>, _ isDryRun: Bool) {
131131
let resultText: String
132132
switch strategy {
133133
case .create(let input):

0 commit comments

Comments
 (0)