Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
759c5ff
Point SDK source back to AvdLee Master
DechengMa Jun 11, 2020
7dc4c56
update fileContent() -> Data to return FileContent
DechengMa Jun 11, 2020
de1c76c
Add id to beta group, make group name non-nil
DechengMa Jun 11, 2020
3b33c41
Add delete to processor with some tweaks, create betagroup processor
DechengMa Jun 11, 2020
4b9ad36
Remove not found error in ListBetaTesterOperation. make options optional
DechengMa Jun 11, 2020
aefdb40
Create service function pullBetaGroups
DechengMa Jun 11, 2020
ca6d9f5
Implement basic pull beta groups command
DechengMa Jun 11, 2020
ba2d7ff
Create BetaTesterProcessor for rendering CSV
DechengMa Jun 12, 2020
58be826
Wire BetaGroupProcessor up with TesterProcessor
DechengMa Jun 12, 2020
18f8fe5
Model.BetaGroup + Hashable, Equatable
DechengMa Jun 12, 2020
d7b7148
Add read() to BetaGroupProcessor
DechengMa Jun 12, 2020
f235a53
Implement UpdateBetaGroupOperation, add update and delete service func
DechengMa Jun 12, 2020
2082ac6
Create SyncStrategy, SyncResultRenderable, SyncResultRenderer
DechengMa Jun 12, 2020
fe92a6c
Wire up PushBetaGroupCommand with sync funcs and strategies
DechengMa Jun 12, 2020
508b7c7
Reformat files for linting get passed
DechengMa Jun 12, 2020
9ef4ece
Introduce SyncResourceComparator,
DechengMa Jun 14, 2020
2c2b02f
Apply new SyncResourceComparator to sync betagroup command
DechengMa Jun 14, 2020
8f2a4ae
Add tests to SyncResourceComparator
DechengMa Jun 14, 2020
7616eae
PushBetaGroupsCommand syncBetaGroup function code separation
DechengMa Jun 15, 2020
962ea21
mark compareIdentity non-optional in ResourceComparable, tweak sync test
DechengMa Jun 15, 2020
6798d52
Model.BetaTester + Hashable, Equatable, SyncResourceProcessable
DechengMa Jun 15, 2020
cdd0238
Add func readGroupAndTesters to BetaGroupProcessor
DechengMa Jun 15, 2020
b394db2
Add service function inviteBetaTesterToGroups && removeTesterFromGroups
DechengMa Jun 15, 2020
532459e
Add sync beta testers function to PushBetaGroupsCommand
DechengMa Jun 15, 2020
68de2ce
Add coding key to Beta Tester for reading csv
DechengMa Jun 15, 2020
86c9459
Use relative path when storing beta tester file path
DechengMa Jun 15, 2020
265f639
Print group name before testers when syncing beta testers
DechengMa Jun 16, 2020
7b05cae
Tweak sync betagroup logic, support creating group and testers together
DechengMa Jun 16, 2020
4be7251
Remove unused beta tester service function
DechengMa Jun 16, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct TestFlightBetaGroupCommand: ParsableCommand {
ReadBetaGroupCommand.self,
RemoveTestersFromGroupCommand.self,
AddTestersToGroupCommand.self,
SyncBetaGroupsCommand.self,
],
defaultSubcommand: ListBetaGroupsCommand.self
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import ArgumentParser
import FileSystem

struct PullBetaGroupsCommand: CommonParsableCommand {

static var configuration = CommandConfiguration(
commandName: "pull",
abstract: "Pull down server beta groups, refresh local beta group config files"
)

@OptionGroup()
var common: CommonOptions

@Option(
default: "./config/betagroups",
help: "Path to the Folder containing the information about beta groups. (default: './config/betagroups')"
) var outputPath: String

func run() throws {
let service = try makeService()

let betaGroupWithTesters = try service.pullBetaGroups()

try BetaGroupProcessor(path: .folder(path: outputPath))
.write(groupsWithTesters: betaGroupWithTesters)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import ArgumentParser
import FileSystem
import Foundation
import struct Model.BetaGroup
import struct Model.BetaTester

struct PushBetaGroupsCommand: CommonParsableCommand {

static var configuration = CommandConfiguration(
commandName: "push",
abstract: "Push local beta group config files to server, update server beta groups"
)

@OptionGroup()
var common: CommonOptions

@Option(
default: "./config/betagroups",
help: "Path to the Folder containing the information about beta groups. (default: './config/betagroups')"
) var inputPath: String

@Flag(help: "Perform a dry run.")
var dryRun: Bool

func run() throws {
let service = try makeService()

let resourceProcessor = BetaGroupProcessor(path: .folder(path: inputPath))

let serverGroupsWithTesters = try service.pullBetaGroups()
let localGroups = try resourceProcessor.read()

// Sync Beta Groups
let strategies = SyncResourceComparator(
localResources: localGroups,
serverResources: serverGroupsWithTesters.map { $0.betaGroup }
)
.compare()

let renderer = Renderers.SyncResultRenderer<BetaGroup>()

if dryRun {
renderer.render(strategies, isDryRun: true)
} else {
try strategies.forEach { (strategy: SyncStrategy) in
try syncBetaGroup(strategy: strategy, with: service)
renderer.render(strategy, isDryRun: false)
}
}

// Sync Beta Testers
let localGroupWithTesters = try resourceProcessor.readGroupAndTesters()

try localGroupWithTesters.forEach {
let localGroup = $0.betaGroup
let localTesters = $0.testers

let serverTesters = serverGroupsWithTesters.first {
$0.betaGroup.id == localGroup.id
}?.testers ?? []

let testerStrategies = SyncResourceComparator(
localResources: localTesters,
serverResources: serverTesters
)
.compare()

let renderer = Renderers.SyncResultRenderer<BetaTester>()

if testerStrategies.count > 0 {
print("\(localGroup.groupName): ")
}

if dryRun {
renderer.render(testerStrategies, isDryRun: true)
} else {
try testerStrategies.forEach {
try syncTester(with: service,
bundleId: localGroup.app.bundleId!,
groupName: localGroup.groupName,
strategies: $0)

renderer.render($0, isDryRun: false)
}
}
}

// After all operations, sync group and testers
if !dryRun {
try resourceProcessor.write(groupsWithTesters: try service.pullBetaGroups())
}
}

func syncBetaGroup(
strategy: SyncStrategy<BetaGroup>,
with service: AppStoreConnectService
) throws {
switch strategy {
case .create(let group):
_ = try service.createBetaGroup(
appBundleId: group.app.bundleId!,
groupName: group.groupName,
publicLinkEnabled: group.publicLinkEnabled ?? false,
publicLinkLimit: group.publicLinkLimit
)
case .delete(let group):
try service.deleteBetaGroup(with: group.id!)
case .update(let group):
try service.updateBetaGroup(betaGroup: group)
}
}

func syncTester(
with service: AppStoreConnectService,
bundleId: String,
groupName: String,
strategies: SyncStrategy<BetaTester>
) throws {
switch strategies {
case .create(let tester):
_ = try service.inviteBetaTesterToGroups(
firstName: tester.firstName,
lastName: tester.lastName,
email: tester.email!,
bundleId: bundleId,
groupNames: [groupName]
)
case .update:
print("Update single beta tester is not supported.")
case .delete(let tester):
try service.removeTesterFromGroups(email: tester.email!, groupNames: [groupName])
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import ArgumentParser

struct SyncBetaGroupsCommand: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "sync",
abstract: """
Sync information about beta groups with provided configuration file.
""",
subcommands: [
PullBetaGroupsCommand.self,
PushBetaGroupsCommand.self,
],
defaultSubcommand: PullBetaGroupsCommand.self
)
}
2 changes: 1 addition & 1 deletion Sources/AppStoreConnectCLI/Model/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extension App: TableInfoProvider {

var tableRow: [CustomStringConvertible] {
return [
id,
id ?? "",
bundleId ?? "",
name ?? "",
primaryLocale ?? "",
Expand Down
36 changes: 32 additions & 4 deletions Sources/AppStoreConnectCLI/Model/BetaGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ extension BetaGroup: TableInfoProvider, ResultRenderable {

var tableRow: [CustomStringConvertible] {
[
app.id,
app.id ?? "",
app.bundleId ?? "",
app.name ?? "",
groupName ?? "",
groupName,
isInternal ?? "",
publicLink ?? "",
publicLinkEnabled ?? "",
Expand All @@ -41,13 +41,29 @@ extension BetaGroup: TableInfoProvider, ResultRenderable {
}

extension BetaGroup {
enum Error: LocalizedError {
case invalidName

var errorDescription: String? {
switch self {
case .invalidName:
return "Beta group doesn't have a valid group name."
}
}
}

init(
_ apiApp: AppStoreConnect_Swift_SDK.App,
_ apiBetaGroup: AppStoreConnect_Swift_SDK.BetaGroup
) {
) throws {
guard let groupName = apiBetaGroup.attributes?.name else {
throw Error.invalidName
}

self.init(
app: App(apiApp),
groupName: apiBetaGroup.attributes?.name,
id: apiBetaGroup.id,
groupName: groupName,
isInternal: apiBetaGroup.attributes?.isInternalGroup,
publicLink: apiBetaGroup.attributes?.publicLink,
publicLinkEnabled: apiBetaGroup.attributes?.publicLinkEnabled,
Expand All @@ -57,3 +73,15 @@ extension BetaGroup {
)
}
}

extension BetaGroup: SyncResultRenderable {
var syncResultText: String {
"\(app.bundleId ?? "" )_\(groupName)"
}
}

extension BetaGroup: SyncResourceProcessable {
var compareIdentity: String {
id ?? ""
}
}
10 changes: 10 additions & 0 deletions Sources/AppStoreConnectCLI/Model/BetaTester.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ extension BetaTester: ResultRenderable, TableInfoProvider {
]
}
}

extension BetaTester: SyncResourceProcessable {
var syncResultText: String {
email!
}

var compareIdentity: String {
email!
}
}
39 changes: 39 additions & 0 deletions Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,42 @@ extension ResultRenderable where Self: TableInfoProvider {
return table.render()
}
}

protocol SyncResultRenderable: Equatable {
var syncResultText: String { get }
}

enum SyncStrategy<T: SyncResultRenderable> {
case delete(T)
case create(T)
case update(T)
}

extension Renderers {

struct SyncResultRenderer<T: SyncResultRenderable> {

func render(_ strategy: [SyncStrategy<T>], isDryRun: Bool) {
strategy.forEach { renderResultText($0, isDryRun) }
}

func render(_ strategy: SyncStrategy<T>, isDryRun: Bool) {
renderResultText(strategy, isDryRun)
}

private func renderResultText(_ strategy: SyncStrategy<T>, _ isDryRun: Bool) {
let resultText: String
switch strategy {
case .create(let input):
resultText = "➕ \(input.syncResultText)"
case .delete(let input):
resultText = "➖ \(input.syncResultText)"
case .update(let input):
resultText = "⬆️ \(input.syncResultText)"
}

print("\(isDryRun ? "" : "✅") \(resultText)")
}
}

}
33 changes: 30 additions & 3 deletions Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ class AppStoreConnectService {
.execute(with: requestor)
.await()

return Model.BetaGroup(app, betaGroup)
return try Model.BetaGroup(app, betaGroup)
}

func createBetaGroup(
Expand All @@ -432,7 +432,7 @@ class AppStoreConnectService {
)

let betaGroupResponse = createBetaGroupOperation.execute(with: requestor)
return try betaGroupResponse.map(Model.BetaGroup.init).await()
return try betaGroupResponse.tryMap(Model.BetaGroup.init).await()
}

func deleteBetaGroup(appBundleId: String, betaGroupName: String) throws {
Expand Down Expand Up @@ -507,7 +507,34 @@ class AppStoreConnectService {
let modifyBetaGroupOperation = ModifyBetaGroupOperation(options: modifyBetaGroupOptions)
let modifiedBetaGroup = try modifyBetaGroupOperation.execute(with: requestor).await()

return Model.BetaGroup(app, modifiedBetaGroup)
return try Model.BetaGroup(app, modifiedBetaGroup)
}

func pullBetaGroups() throws -> [(betaGroup: Model.BetaGroup, testers: [Model.BetaTester])] {
let groupOutputs = try ListBetaGroupsOperation(options: .init(appIds: [], names: [], sort: nil)).execute(with: requestor).await()

return try groupOutputs.map {
let testers = try ListBetaTestersOperation(
options: .init(groupIds: [$0.betaGroup.id])
)
.execute(with: requestor)
.await()
.map(BetaTester.init)

return (try BetaGroup($0.app, $0.betaGroup), testers)
}
}

func updateBetaGroup(betaGroup: Model.BetaGroup) throws {
_ = try UpdateBetaGroupOperation(options: .init(betaGroup: betaGroup))
.execute(with: requestor)
.await()
}

func deleteBetaGroup(with id: String) throws {
try DeleteBetaGroupOperation(options: .init(betaGroupId: id))
.execute(with: requestor)
.await()
}

func readBuild(bundleId: String, buildNumber: String, preReleaseVersion: String) throws -> Model.Build {
Expand Down
Loading