Skip to content

Commit 59303d2

Browse files
committed
merge master to enable-capability
2 parents ff3ae21 + 6254bab commit 59303d2

14 files changed

+296
-171
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ An easy to use command-line tool for interacting with the Apple AppStore Connect
1010
AppStoreConnect CLI lets you interact with the AppStore Connect from the command line.
1111

1212
- Manage Users and access.
13-
- TODO: Manage TestFlight Users, Beta Groups, and Builds.
13+
- Manage TestFlight Users, Beta Groups, and Builds.
1414
- Manage Devices, Profiles and Bundle IDs.
15-
- TODO: Manage Certificates
15+
- Manage Certificates
1616
- TODO: Download reports.
1717

1818
⚠️ **Note:** _AppStoreConnect CLI_ is under heavy development and not all features are complete.

Sources/AppStoreConnectCLI/Commands/Profiles/ListProfilesCommand.swift

Lines changed: 18 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import AppStoreConnect_Swift_SDK
44
import ArgumentParser
5-
import Combine
65
import Foundation
76
import Files
8-
import Model
97

108
struct ListProfilesCommand: CommonParsableCommand {
119
static var configuration = CommandConfiguration(
@@ -67,59 +65,28 @@ struct ListProfilesCommand: CommonParsableCommand {
6765
static let profileExtension = "mobileprovision"
6866

6967
func run() throws {
70-
let api = try makeService()
71-
72-
var filters = [Profiles.Filter]()
73-
74-
if !filterName.isEmpty {
75-
filters.append(.name(filterName))
76-
}
77-
78-
if let filterProfileState = filterProfileState {
79-
filters.append(.profileState([filterProfileState]))
80-
}
81-
82-
if !filterProfileType.isEmpty {
83-
filters.append(.profileType(filterProfileType))
84-
}
85-
86-
var limits = [Profiles.Limit]()
87-
if let limit = limit {
88-
limits.append(.profiles(limit))
89-
}
90-
91-
let request = APIEndpoint.listProfiles(
92-
filter: filters,
93-
include: [.bundleId, .certificates, .devices],
94-
sort: [sort].compactMap { $0 },
95-
limit: limits
68+
let service = try makeService()
69+
70+
let profiles = try service.listProfiles(
71+
filterName: filterName,
72+
filterProfileState: filterProfileState,
73+
filterProfileType: filterProfileType,
74+
sort: sort,
75+
limit: limit
9676
)
9777

98-
let profiles = try api.request(request)
99-
.map { $0.data.map(Model.Profile.init) }
100-
.saveProfile(downloadPath: self.downloadPath) // FIXME: This feels like a hack.
101-
.await()
102-
103-
profiles.render(format: common.outputFormat)
104-
}
105-
}
78+
if let path = downloadPath {
79+
let folder = try Folder(path: path)
10680

107-
private extension Publisher where Output == [Model.Profile], Failure == Error {
108-
func saveProfile(downloadPath: String?) -> AnyPublisher<Output, Failure> {
109-
tryMap { profiles -> Output in
110-
if let path = downloadPath {
111-
112-
let folder = try Folder(path: path)
113-
for profile in profiles {
114-
let file = try folder.createFile(
115-
named: "\(profile.uuid!).\(ListProfilesCommand.profileExtension)",
116-
contents: Data(base64Encoded: profile.profileContent!)!
117-
)
118-
Swift.print("📥 Profile '\(profile.name!)' downloaded to: \(file.path)")
119-
}
81+
try profiles.forEach {
82+
let file = try folder.createFile(
83+
named: "\($0.uuid!).\(ListProfilesCommand.profileExtension)",
84+
contents: Data(base64Encoded: $0.profileContent!)!
85+
)
86+
print("📥 Profile '\($0.name!)' downloaded to: \(file.path)")
12087
}
88+
}
12189

122-
return profiles
123-
}.eraseToAnyPublisher()
90+
profiles.render(format: common.outputFormat)
12491
}
12592
}

Sources/AppStoreConnectCLI/Commands/Users/Invitations/ListUserInvitationsCommand.swift

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import AppStoreConnect_Swift_SDK
44
import ArgumentParser
5-
import Foundation
65

76
struct ListUserInvitationsCommand: CommonParsableCommand {
87
public static var configuration = CommandConfiguration(
@@ -19,38 +18,22 @@ struct ListUserInvitationsCommand: CommonParsableCommand {
1918
@Option(parsing: .upToNextOption, help: "Filter the results by the specified username")
2019
var filterEmail: [String]
2120

22-
@Option(parsing: .upToNextOption, help: "Filter the results by the specified roles")
21+
@Option(parsing: .upToNextOption, help: "Filter the results by the specified roles, (eg. \(UserRole.allCases.compactMap { $0.rawValue.lowercased() })")
2322
var filterRole: [UserRole]
2423

2524
@Flag(help: "Include visible apps in results.")
2625
var includeVisibleApps: Bool
2726

28-
private var filters: [ListInvitedUsers.Filter]? {
29-
var filters = [ListInvitedUsers.Filter]()
30-
31-
if filterEmail.isEmpty == false {
32-
filters += [ListInvitedUsers.Filter.email(filterEmail)]
33-
}
34-
35-
if filterRole.isEmpty == false {
36-
filters += [ListInvitedUsers.Filter.roles(filterRole.map { $0.rawValue })]
37-
}
38-
39-
return filters
40-
}
41-
4227
public func run() throws {
4328
let service = try makeService()
4429

45-
let endpoint = APIEndpoint.invitedUsers(
46-
limit: limitVisibleApps.map { [ListInvitedUsers.Limit.visibleApps($0)] },
47-
filter: filters
30+
let invitations = try service.listUserInvitaions(
31+
filterEmail: filterEmail,
32+
filterRole: filterRole,
33+
limitVisibleApps: limitVisibleApps,
34+
includeVisibleApps: includeVisibleApps
4835
)
4936

50-
let invitations = try service.request(endpoint)
51-
.map(\.data)
52-
.await()
53-
5437
invitations.render(format: common.outputFormat)
5538
}
5639
}

Sources/AppStoreConnectCLI/Helpers/Collection+Helpers.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ extension Collection {
66
func nilIfEmpty() -> Self? {
77
isEmpty ? nil : self
88
}
9+
10+
var isNotEmpty: Bool { isEmpty == false }
911
}

Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,45 @@ class AppStoreConnectService {
683683
.map(Device.init)
684684
}
685685

686+
func listProfiles(
687+
filterName: [String],
688+
filterProfileState: ProfileState?,
689+
filterProfileType: [ProfileType],
690+
sort: Profiles.Sort?,
691+
limit: Int?
692+
) throws -> [Model.Profile] {
693+
try ListProfilesOperation(
694+
options: .init(
695+
filterName: filterName,
696+
filterProfileState: filterProfileState,
697+
filterProfileType: filterProfileType,
698+
sort: sort,
699+
limit: limit
700+
)
701+
)
702+
.execute(with: requestor)
703+
.await()
704+
.map(Model.Profile.init)
705+
}
706+
707+
func listUserInvitaions(
708+
filterEmail: [String],
709+
filterRole: [UserRole],
710+
limitVisibleApps: Int?,
711+
includeVisibleApps: Bool
712+
) throws -> [UserInvitation] {
713+
try ListUserInvitationsOperation(
714+
options: .init(
715+
filterEmail: filterEmail,
716+
filterRole: filterRole,
717+
includeVisibleApps: includeVisibleApps,
718+
limitVisibleApps: limitVisibleApps
719+
)
720+
)
721+
.execute(with: requestor)
722+
.await()
723+
}
724+
686725
func enableBundleIdCapability(
687726
bundleId: String,
688727
capabilityType: CapabilityType

Sources/AppStoreConnectCLI/Services/Operations/ListAppsOperation.swift

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,6 @@ struct ListAppsOperation: APIOperation {
1010
let names: [String]
1111
let skus: [String]
1212
let limit: Int?
13-
14-
fileprivate var endpoint: APIEndpoint<AppsResponse> {
15-
var filters: [ListApps.Filter] = []
16-
bundleIds.isEmpty ? () : filters.append(.bundleId(bundleIds))
17-
names.isEmpty ? () : filters.append(.name(names))
18-
skus.isEmpty ? () : filters.append(.sku(skus))
19-
20-
let limits = limit.map { [ListApps.Limit.apps($0)] }
21-
22-
return .apps(filters: filters, limits: limits)
23-
}
2413
}
2514

2615
private let options: Options
@@ -32,7 +21,27 @@ struct ListAppsOperation: APIOperation {
3221
typealias App = AppStoreConnect_Swift_SDK.App
3322

3423
func execute(with requestor: EndpointRequestor) -> AnyPublisher<[App], Error> {
35-
requestor.request(options.endpoint).map(\.data).eraseToAnyPublisher()
24+
var filters: [ListApps.Filter] = []
25+
26+
if options.bundleIds.isNotEmpty { filters.append(.bundleId(options.bundleIds)) }
27+
if options.names.isNotEmpty { filters.append(.name(options.names)) }
28+
if options.skus.isNotEmpty { filters.append(.sku(options.skus)) }
29+
30+
let limits = options.limit.map { [ListApps.Limit.apps($0)] }
31+
32+
guard limits != nil else {
33+
return requestor.requestAllPages {
34+
.apps(filters: filters, next: $0)
35+
}
36+
.map { $0.flatMap(\.data) }
37+
.eraseToAnyPublisher()
38+
}
39+
40+
return requestor.request(.apps(filters: filters, limits: limits))
41+
.map(\.data)
42+
.eraseToAnyPublisher()
3643
}
3744

3845
}
46+
47+
extension AppsResponse: PaginatedResponse { }

Sources/AppStoreConnectCLI/Services/Operations/ListBundleIdsOperation.swift

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,6 @@ struct ListBundleIdsOperation: APIOperation {
1010
let platforms: [String]
1111
let seedIds: [String]
1212
let limit: Int?
13-
14-
fileprivate var endpoint: APIEndpoint<BundleIdsResponse> {
15-
let platforms = self.platforms.compactMap(Platform.init(rawValue:))
16-
17-
var filters: [BundleIds.Filter] = []
18-
filters += identifiers.isEmpty ? [] : [.identifier(identifiers)]
19-
filters += names.isEmpty ? [] : [.name(names)]
20-
filters += platforms.isEmpty ? [] : [.platform(platforms)]
21-
filters += seedIds.isEmpty ? [] : [.seedId(seedIds)]
22-
23-
return .listBundleIds(filter: filters, limit: limit)
24-
}
2513
}
2614

2715
private let options: Options
@@ -33,6 +21,25 @@ struct ListBundleIdsOperation: APIOperation {
3321
typealias BundleId = AppStoreConnect_Swift_SDK.BundleId
3422

3523
func execute(with requestor: EndpointRequestor) throws -> AnyPublisher<[BundleId], Error> {
36-
requestor.request(options.endpoint).map(\.data).eraseToAnyPublisher()
24+
let platforms = options.platforms.compactMap(Platform.init(rawValue:))
25+
26+
var filters: [BundleIds.Filter] = []
27+
28+
if options.identifiers.isNotEmpty { filters.append(.identifier(options.identifiers)) }
29+
if options.names.isNotEmpty { filters.append(.name(options.names)) }
30+
if options.platforms.isNotEmpty { filters.append(.platform(platforms)) }
31+
if options.seedIds.isNotEmpty { filters.append(.seedId(options.seedIds)) }
32+
33+
let limit = options.limit
34+
35+
return requestor.requestAllPages {
36+
.listBundleIds(filter: filters, limit: limit, next: $0)
37+
}
38+
.map {
39+
$0.flatMap(\.data)
40+
}
41+
.eraseToAnyPublisher()
3742
}
3843
}
44+
45+
extension BundleIdsResponse: PaginatedResponse { }

Sources/AppStoreConnectCLI/Services/Operations/ListCertificatesOpertaion.swift

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import struct Model.Certificate
77

88
struct ListCertificatesOperation: APIOperation {
99

10-
enum ListCertificatesError: LocalizedError {
10+
typealias Filter = Certificates.Filter
11+
12+
enum Error: LocalizedError {
1113
case couldNotFindCertificate
1214

1315
var errorDescription: String? {
@@ -17,7 +19,7 @@ struct ListCertificatesOperation: APIOperation {
1719
}
1820
}
1921
}
20-
22+
2123
struct Options {
2224
let filterSerial: String?
2325
let sort: Certificates.Sort?
@@ -26,12 +28,8 @@ struct ListCertificatesOperation: APIOperation {
2628
let limit: Int?
2729
}
2830

29-
private let endpoint: APIEndpoint<CertificatesResponse>
30-
31-
init(options: Options) {
32-
typealias Filter = Certificates.Filter
33-
34-
var filters = [Certificates.Filter]()
31+
var filters: [Filter] {
32+
var filters = [Filter]()
3533

3634
if let filterSerial = options.filterSerial {
3735
filters.append(.serialNumber([filterSerial]))
@@ -45,23 +43,40 @@ struct ListCertificatesOperation: APIOperation {
4543
filters.append(.displayName([filterDisplayName]))
4644
}
4745

48-
endpoint = APIEndpoint.listDownloadCertificates(
49-
filter: filters,
50-
sort: [options.sort].compactMap { $0 },
51-
limit: options.limit
52-
)
46+
return filters
5347
}
5448

55-
func execute(with requestor: EndpointRequestor) -> AnyPublisher<[Certificate], Error> {
56-
requestor.request(endpoint)
57-
.tryMap { (response: CertificatesResponse) -> [Certificate] in
49+
let options: Options
50+
51+
init(options: Options) {
52+
self.options = options
53+
}
54+
55+
func execute(with requestor: EndpointRequestor) -> AnyPublisher<[Certificate], Swift.Error> {
56+
let filters = self.filters
57+
let sort = [options.sort].compactMap { $0 }
58+
let limit = options.limit
59+
60+
return requestor.requestAllPages {
61+
.listDownloadCertificates(
62+
filter: filters,
63+
sort: sort,
64+
limit: limit,
65+
next: $0
66+
)
67+
}
68+
.tryMap {
69+
try $0.flatMap { (response: CertificatesResponse) -> [Certificate] in
5870
guard !response.data.isEmpty else {
59-
throw ListCertificatesError.couldNotFindCertificate
71+
throw Error.couldNotFindCertificate
6072
}
6173

6274
return response.data.map(Certificate.init)
6375
}
64-
.eraseToAnyPublisher()
76+
}
77+
.eraseToAnyPublisher()
6578
}
6679

6780
}
81+
82+
extension CertificatesResponse: PaginatedResponse { }

0 commit comments

Comments
 (0)