From 49b17bcf9d95c3592063f300780256dd28bf48be Mon Sep 17 00:00:00 2001 From: David Reinecke Date: Sun, 25 Oct 2026 17:29:48 +0000 Subject: [PATCH] DeveloperServices: assign all of an app's groups to its App ID in one request assignApplicationGroupToAppId replaces the App ID's groups with the ones in the request, and DeveloperServicesAssignAppGroupsOperation sent one request per group, concurrently. An app whose entitlements name two groups therefore ended with one group in its profile (whichever request Apple handled last), and the device refused to install it (ApplicationVerificationFailed). The operation now creates any missing groups as before, then sends a single request carrying every group ID, and returns the group IDs in the order the entitlements listed them, so the rewritten entitlement is stable between builds. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UKzxgxaLwK7Ap5LMwj9MxH --- ...veloperServicesAssignAppGroupRequest.swift | 11 ++-- ...operServicesAssignAppGroupsOperation.swift | 60 +++++++++++-------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift index 713afdb0..399ed8c6 100644 --- a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift +++ b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupRequest.swift @@ -8,6 +8,9 @@ import Foundation +/// Assigns a set of app groups to an App ID. The request replaces the App ID's groups with +/// `groupIDs`, so every group the app needs has to be in one request: assigning them one +/// request at a time leaves only the last one standing. public final class DeveloperServicesAssignAppGroupRequest: DeveloperServicesPlatformRequest { public struct Response: Decodable, Sendable {} @@ -16,14 +19,14 @@ public final class DeveloperServicesAssignAppGroupRequest: DeveloperServicesPlat public let platform: DeveloperServicesPlatform public let teamID: DeveloperServicesTeam.ID public let appIDID: String - public let groupID: DeveloperServicesAppGroup.ID + public let groupIDs: [DeveloperServicesAppGroup.ID] var subAction: String { return "assignApplicationGroupToAppId" } var subParameters: [String: Any] { return [ "teamId": teamID.rawValue, "appIdId": appIDID, - "applicationGroups": groupID.rawValue + "applicationGroups": groupIDs.map(\.rawValue) ] } @@ -31,12 +34,12 @@ public final class DeveloperServicesAssignAppGroupRequest: DeveloperServicesPlat platform: DeveloperServicesPlatform, teamID: DeveloperServicesTeam.ID, appIDID: String, - groupID: DeveloperServicesAppGroup.ID + groupIDs: [DeveloperServicesAppGroup.ID] ) { self.platform = platform self.teamID = teamID self.appIDID = appIDID - self.groupID = groupID + self.groupIDs = groupIDs } } diff --git a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift index b13e1a5b..d10fa846 100644 --- a/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift +++ b/Sources/XKit/DeveloperServices/App Groups/DeveloperServicesAssignAppGroupsOperation.swift @@ -33,52 +33,60 @@ public struct DeveloperServicesAssignAppGroupsOperation: DeveloperServicesOperat self.client = DeveloperServicesClient(authData: authData) } + /// Finds the team's group for `groupID`, creating it when there is none. private func upsertAppGroup( _ groupID: DeveloperServicesAppGroup.GroupID, existingGroups: [String: DeveloperServicesAppGroup] - ) async throws -> DeveloperServicesAppGroup.GroupID { + ) async throws -> DeveloperServicesAppGroup { let sanitized = ProvisioningIdentifiers.sanitize(groupID: groupID) - let group: DeveloperServicesAppGroup if let existingGroup = existingGroups[sanitized] { - group = existingGroup - } else { - let groupID = ProvisioningIdentifiers.groupID(fromSanitized: sanitized, context: context) - let name = ProvisioningIdentifiers.groupName(fromSanitized: sanitized) - let request = DeveloperServicesAddAppGroupRequest( - platform: .iOS, - teamID: xcodeAuthData.teamID, - name: name, - groupID: groupID - ) - group = try await client.send(request) + return existingGroup } - - _ = try await client.send( - DeveloperServicesAssignAppGroupRequest( - platform: .iOS, - teamID: xcodeAuthData.teamID, - appIDID: appID.id, - groupID: group.id - ) + let groupID = ProvisioningIdentifiers.groupID(fromSanitized: sanitized, context: context) + let name = ProvisioningIdentifiers.groupName(fromSanitized: sanitized) + let request = DeveloperServicesAddAppGroupRequest( + platform: .iOS, + teamID: xcodeAuthData.teamID, + name: name, + groupID: groupID ) - - return group.groupID + return try await client.send(request) } + /// Creates any missing groups, then assigns all of them to the App ID in one request. + /// `assignApplicationGroupToAppId` replaces the App ID's groups with the ones in the request, + /// so assigning each group separately (as this once did, concurrently) left the App ID with + /// only one of them, and an app with two groups got a profile that iOS refused to install. + /// Returns the groups' IDs in the order they were given. public func perform() async throws -> [DeveloperServicesAppGroup.GroupID] { let existing = try await client.send(DeveloperServicesListAppGroupsRequest( platform: .iOS, teamID: xcodeAuthData.teamID )) let sanitized = existing.map { (ProvisioningIdentifiers.sanitize(groupID: $0.groupID), $0) } let dict = Dictionary(sanitized, uniquingKeysWith: { $1 }) - return try await withThrowingTaskGroup(of: DeveloperServicesAppGroup.GroupID.self) { group in - for groupID in groupIDs { + let groups = try await withThrowingTaskGroup(of: (Int, DeveloperServicesAppGroup).self) { group in + for (index, groupID) in groupIDs.enumerated() { group.addTask { - try await upsertAppGroup(groupID, existingGroups: dict) + (index, try await upsertAppGroup(groupID, existingGroups: dict)) } } return try await group.reduce(into: []) { $0.append($1) } } + .sorted { $0.0 < $1.0 } + .map(\.1) + + if !groups.isEmpty { + _ = try await client.send( + DeveloperServicesAssignAppGroupRequest( + platform: .iOS, + teamID: xcodeAuthData.teamID, + appIDID: appID.id, + groupIDs: groups.map(\.id) + ) + ) + } + + return groups.map(\.groupID) } }