Skip to content

Commit de3f919

Browse files
committed
feat(review-submissions): add command to list review submissions
- Implement `asc review-submissions list` with filters for app ID, states, and limit - Add `listSubmissions(appId:states:limit:)` to `SubmissionRepository` and backend - Include comprehensive tests for state parsing and output formatting feat(certificates): enhance list command with filtering options - Add `--limit`, `--expired-only`, and `--before` flags to `asc certificates list` - Update `CertificateRepository.listCertificates` to support limit parameter - Apply client-side filtering for expired and expiration cutoff date - Add tests covering new filtering behaviors and limit forwarding
1 parent b4ee0c3 commit de3f919

15 files changed

Lines changed: 335 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- **`asc review-submissions list`** — list App Store review submissions for an app. Required `--app-id`; optional `--state <CSV>` (e.g. `WAITING_FOR_REVIEW,IN_REVIEW,READY_FOR_REVIEW` or `UNRESOLVED_ISSUES`) and `--limit`. Backed by `SubmissionRepository.listSubmissions(appId:states:limit:)`
12+
- **`asc certificates list` filtering flags**`--limit` (server-side), `--expired-only` (client-side, drops unexpired certs), `--before <ISO8601>` (client-side, keeps certs with expirationDate strictly before the cutoff)
13+
14+
### Changed
15+
- **`CertificateRepository.listCertificates`** signature now takes `(certificateType:limit:)` — forwards `limit` to the SDK
16+
1017
---
1118

1219
## [0.1.68] - 2026-04-14

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ asc beta-review submissions create --build-id <id>
164164
asc beta-review submissions get --submission-id <id>
165165
asc beta-review detail get --app-id <id>
166166
asc beta-review detail update --detail-id <id> [--contact-first-name <name>] [--notes <text>]
167+
168+
asc review-submissions list --app-id <id> [--state WAITING_FOR_REVIEW,IN_REVIEW,READY_FOR_REVIEW] [--limit 200]
167169
```
168170

169171
### Xcode Cloud
@@ -364,7 +366,7 @@ asc bundle-ids list [--platform ios|macos|universal] [--identifier com.example.a
364366
asc bundle-ids create --name "My App" --identifier com.example.app --platform ios
365367
asc bundle-ids delete --bundle-id-id <id>
366368

367-
asc certificates list [--type IOS_DISTRIBUTION]
369+
asc certificates list [--type IOS_DISTRIBUTION] [--limit 200] [--expired-only] [--before 2026-11-01T00:00:00Z]
368370
asc certificates create --type IOS_DISTRIBUTION --csr-content "$(cat MyApp.certSigningRequest)"
369371
asc certificates revoke --certificate-id <id>
370372

@@ -524,6 +526,7 @@ Detailed documentation for each feature:
524526
- [App Info](docs/features/app-infos.md) — name, subtitle, privacy policy, categories, age rating
525527
- [TestFlight](docs/features/testflight.md) — beta groups, tester management, CSV import/export
526528
- [Beta Review](docs/features/beta-review.md) — submit builds for beta app review, manage review contact details
529+
- [Review Submissions](docs/features/review-submissions.md) — list App Store review submissions filtered by state
527530
- [Xcode Cloud](docs/features/xcode-cloud.md) — products, workflows, build runs, start builds
528531
- [Builds Archive](docs/features/builds-archive.md) — archive Xcode projects, export IPA/PKG, optional upload chaining
529532
- [Builds Upload](docs/features/builds-upload.md) — upload IPA/PKG, TestFlight distribution, beta notes

Sources/ASCCommand/ASC.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ struct ASC: AsyncParsableCommand {
6464
DiagnosticsCommand.self,
6565
DiagnosticLogsCommand.self,
6666
BetaReviewCommand.self,
67+
ReviewSubmissionsCommand.self,
6768
AppAvailabilityCommand.self,
6869
IAPAvailabilityCommand.self,
6970
SubscriptionAvailabilityCommand.self,

Sources/ASCCommand/Commands/Certificates/CertificatesList.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ArgumentParser
22
import Domain
3+
import Foundation
34

45
struct CertificatesList: AsyncParsableCommand {
56
static let configuration = CommandConfiguration(
@@ -12,14 +13,37 @@ struct CertificatesList: AsyncParsableCommand {
1213
@Option(name: .long, help: "Filter by certificate type (e.g. IOS_DISTRIBUTION, MAC_APP_STORE)")
1314
var type: String?
1415

16+
@Option(name: .long, help: "Maximum number of certificates to return (server-side)")
17+
var limit: Int?
18+
19+
@Flag(name: .long, help: "Only return certificates whose expirationDate has passed")
20+
var expiredOnly: Bool = false
21+
22+
@Option(name: .long, help: "Only return certificates with expirationDate strictly before this ISO8601 date")
23+
var before: String?
24+
1525
func run() async throws {
1626
let repo = try ClientProvider.makeCertificateRepository()
1727
print(try await execute(repo: repo))
1828
}
1929

2030
func execute(repo: any CertificateRepository, affordanceMode: AffordanceMode = .cli) async throws -> String {
2131
let certType = type.flatMap { CertificateType(rawValue: $0.uppercased()) }
22-
let items = try await repo.listCertificates(certificateType: certType)
32+
var items = try await repo.listCertificates(certificateType: certType, limit: limit)
33+
34+
if expiredOnly {
35+
items = items.filter(\.isExpired)
36+
}
37+
if let before {
38+
guard let cutoff = ISO8601DateFormatter().date(from: before) else {
39+
throw ValidationError("--before must be an ISO8601 date (e.g. 2025-11-14T22:13:20Z)")
40+
}
41+
items = items.filter { cert in
42+
guard let exp = cert.expirationDate else { return false }
43+
return exp < cutoff
44+
}
45+
}
46+
2347
let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
2448
return try formatter.formatAgentItems(items, affordanceMode: affordanceMode)
2549
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ArgumentParser
2+
3+
struct ReviewSubmissionsCommand: AsyncParsableCommand {
4+
static let configuration = CommandConfiguration(
5+
commandName: "review-submissions",
6+
abstract: "List App Store review submissions",
7+
subcommands: [ReviewSubmissionsList.self]
8+
)
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ArgumentParser
2+
import Domain
3+
import Foundation
4+
5+
struct ReviewSubmissionsList: AsyncParsableCommand {
6+
static let configuration = CommandConfiguration(
7+
commandName: "list",
8+
abstract: "List App Store review submissions for an app"
9+
)
10+
11+
@OptionGroup var globals: GlobalOptions
12+
13+
@Option(name: .long, help: "App ID to filter submissions")
14+
var appId: String
15+
16+
@Option(name: .long, help: "Comma-separated states (e.g. WAITING_FOR_REVIEW,IN_REVIEW,READY_FOR_REVIEW)")
17+
var state: String?
18+
19+
@Option(name: .long, help: "Maximum number of submissions to return")
20+
var limit: Int?
21+
22+
func run() async throws {
23+
let repo = try ClientProvider.makeSubmissionRepository()
24+
print(try await execute(repo: repo))
25+
}
26+
27+
func execute(repo: any SubmissionRepository) async throws -> String {
28+
let parsedStates = state.map { csv in
29+
csv.split(separator: ",").compactMap {
30+
ReviewSubmissionState(rawValue: String($0).trimmingCharacters(in: .whitespaces).uppercased())
31+
}
32+
}
33+
let items = try await repo.listSubmissions(appId: appId, states: parsedStates, limit: limit)
34+
let formatter = OutputFormatter(format: globals.outputFormat, pretty: globals.pretty)
35+
return try formatter.formatAgentItems(
36+
items,
37+
headers: ["ID", "App ID", "Platform", "State"],
38+
rowMapper: { [$0.id, $0.appId, $0.platform.rawValue, $0.state.rawValue] }
39+
)
40+
}
41+
}

Sources/ASCCommand/Commands/Web/Controllers/CodeSigningController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct CodeSigningController: Sendable {
1212

1313
func addRoutes(to group: RouterGroup<BasicWebSocketRequestContext>) {
1414
group.get("/certificates") { _, _ -> Response in
15-
let certs = try await self.certRepo.listCertificates(certificateType: nil)
15+
let certs = try await self.certRepo.listCertificates(certificateType: nil, limit: nil)
1616
return try restFormat(certs)
1717
}
1818

Sources/Domain/CodeSigning/Certificates/CertificateRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Mockable
22

33
@Mockable
44
public protocol CertificateRepository: Sendable {
5-
func listCertificates(certificateType: CertificateType?) async throws -> [Certificate]
5+
func listCertificates(certificateType: CertificateType?, limit: Int?) async throws -> [Certificate]
66
func createCertificate(certificateType: CertificateType, csrContent: String) async throws -> Certificate
77
func revokeCertificate(id: String) async throws
88
}

Sources/Domain/Submissions/SubmissionRepository.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import Mockable
33
@Mockable
44
public protocol SubmissionRepository: Sendable {
55
func submitVersion(versionId: String) async throws -> ReviewSubmission
6+
func listSubmissions(appId: String, states: [ReviewSubmissionState]?, limit: Int?) async throws -> [ReviewSubmission]
67
}

Sources/Infrastructure/CodeSigning/SDKCertificateRepository.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ public struct SDKCertificateRepository: CertificateRepository, @unchecked Sendab
88
self.client = client
99
}
1010

11-
public func listCertificates(certificateType: Domain.CertificateType?) async throws -> [Domain.Certificate] {
11+
public func listCertificates(certificateType: Domain.CertificateType?, limit: Int?) async throws -> [Domain.Certificate] {
1212
let filterType = certificateType.flatMap {
1313
APIEndpoint.V1.Certificates.GetParameters.FilterCertificateType(rawValue: $0.rawValue)
1414
}
1515
let request = APIEndpoint.v1.certificates.get(parameters: .init(
16-
filterCertificateType: filterType.map { [$0] }
16+
filterCertificateType: filterType.map { [$0] },
17+
limit: limit
1718
))
1819
let response = try await client.request(request)
1920
return response.data.map(mapCertificate)

0 commit comments

Comments
 (0)