6
6
from typing import List
7
7
from typing import Optional
8
8
from typing import Sequence
9
+ from typing import Set
9
10
from typing import Tuple
10
11
from typing import Union
11
12
18
19
from codemagic .apple .resources import Profile
19
20
from codemagic .apple .resources import ProfileState
20
21
from codemagic .apple .resources import ProfileType
22
+ from codemagic .apple .resources import ResourceId
21
23
from codemagic .apple .resources import SigningCertificate
22
24
from codemagic .cli import Colors
23
25
from codemagic .models import PrivateKey
31
33
from ..errors import AppStoreConnectError
32
34
33
35
36
+ class _StaleProfileError (Exception ):
37
+ """
38
+ App Store Connect API response for listing bundle identifier profiles contains resources that are
39
+ already "deleted". These profiles aren't available in the Developer Portal UI nor via read requests,
40
+ but can be deleted via API. They significantly slow down actions as the deleted profiles accumulate
41
+ and eventually many unnecessary 404 requests need to be performed.
42
+ """
43
+
44
+
34
45
class FetchSigningFilesAction (AbstractBaseAction , metaclass = ABCMeta ):
35
46
@cli .action (
36
47
"fetch-signing-files" ,
@@ -42,6 +53,7 @@ class FetchSigningFilesAction(AbstractBaseAction, metaclass=ABCMeta):
42
53
ProfileArgument .PROFILE_TYPE ,
43
54
BundleIdArgument .IDENTIFIER_STRICT_MATCH ,
44
55
CommonArgument .CREATE_RESOURCE ,
56
+ ProfileArgument .DELETE_STALE_PROFILES ,
45
57
)
46
58
def fetch_signing_files (
47
59
self ,
@@ -53,6 +65,7 @@ def fetch_signing_files(
53
65
profile_type : ProfileType = ProfileType .IOS_APP_DEVELOPMENT ,
54
66
bundle_id_identifier_strict_match : bool = False ,
55
67
create_resource : bool = False ,
68
+ delete_stale_profiles : bool = False ,
56
69
) -> Tuple [List [Profile ], List [SigningCertificate ]]:
57
70
"""
58
71
Fetch provisioning profiles and code signing certificates
@@ -90,6 +103,7 @@ def fetch_signing_files(
90
103
profile_type ,
91
104
create_resource ,
92
105
platform ,
106
+ delete_stale_profiles ,
93
107
)
94
108
self .echo ("" )
95
109
@@ -148,40 +162,106 @@ def _get_or_create_certificates(
148
162
certificates .append (certificate )
149
163
return certificates
150
164
151
- def _get_or_create_profiles (
152
- self ,
153
- bundle_ids : Sequence [BundleId ],
154
- certificates : Sequence [SigningCertificate ],
155
- profile_type : ProfileType ,
156
- create_resource : bool ,
157
- platform : Optional [BundleIdPlatform ] = None ,
158
- ):
159
- def has_certificate (profile ) -> bool :
160
- try :
161
- profile_certificates = self .api_client .profiles .list_certificate_ids (profile )
162
- return bool (certificate_ids .issubset ({c .id for c in profile_certificates }))
163
- except AppStoreConnectApiError as err :
164
- error = f"Listing { SigningCertificate .s } for { Profile } { profile .id } failed unexpectedly"
165
- self .logger .warning (Colors .YELLOW (f"{ error } : { err .error_response } " ))
166
- return False
165
+ def _has_certificate (self , profile : Profile , available_certificate_ids : Set [ResourceId ]) -> bool :
166
+ try :
167
+ profile_certificates = self .api_client .profiles .list_certificate_ids (profile )
168
+ except AppStoreConnectApiError as err :
169
+ if f"There is no resource of type 'profiles' with id '{ profile .id } '" in str (err .error_response ):
170
+ raise _StaleProfileError () from err
171
+ error = f"Listing { SigningCertificate .s } for { Profile } { profile .id } failed unexpectedly"
172
+ self .logger .warning (Colors .YELLOW (f"{ error } : { err .error_response } " ))
173
+ return False
174
+
175
+ # Do not use set.issubset as empty set is subset of another empty set.
176
+ return bool ({c .id for c in profile_certificates } & available_certificate_ids )
177
+
178
+ def _has_profile (self , bundle_id : BundleId , available_profile_ids : Set [ResourceId ]) -> bool :
179
+ try :
180
+ bundle_id_profiles = self .api_client .bundle_ids .list_profile_ids (bundle_id )
181
+ except AppStoreConnectApiError as err :
182
+ error = f"Listing { Profile .s } for { BundleId } { bundle_id .id } failed unexpectedly"
183
+ self .logger .warning (Colors .YELLOW (f"{ error } : { err .error_response } " ))
184
+ return False
185
+
186
+ # Do not use set.issubset as empty set is subset of another empty set.
187
+ return bool ({p .id for p in bundle_id_profiles } & available_profile_ids )
188
+
189
+ def _handle_stale_profiles (self , stale_profiles : Sequence [Profile ], delete_stale_profiles : bool ) -> None :
190
+ """
191
+ Listing profiles for bundle identifiers can return "zombie" profiles, which cannot be used.
192
+ Notify about such findings and if user has granted a permission, then attempt to delete
193
+ such profiles so that they wouldn't be picked up on the following action invocations.
194
+ """
195
+ if not stale_profiles :
196
+ return
197
+
198
+ stale_profile_ids = ", " .join (p .id for p in stale_profiles )
199
+
200
+ if not delete_stale_profiles :
201
+ self .logger .warning (Colors .RED (f"\n Found { len (stale_profiles )} stale profiles: { stale_profile_ids } ." ))
202
+ message = (
203
+ "These profiles are expired with invalid status and cannot be used or seen in "
204
+ "Apple Developer Portal. Requesting information about such provisioning profiles "
205
+ "causes unnecessarily slow-downs of the action. Get rid of them and speed up future "
206
+ "action invocations by using optional flag "
207
+ f"{ Colors .BRIGHT_WHITE (ProfileArgument .DELETE_STALE_PROFILES .flag )} ."
208
+ )
209
+ self .logger .warning (message )
210
+ return
167
211
168
- def missing_profile (bundle_id ) -> bool :
212
+ self .logger .info (f"\n Found { len (stale_profiles )} stale profiles, deleting them." )
213
+ for stale_profile in stale_profiles :
169
214
try :
170
- bundle_ids_profiles = self .api_client .bundle_ids .list_profile_ids (bundle_id )
171
- return not (profile_ids & {p .id for p in bundle_ids_profiles })
215
+ self .api_client .profiles .delete (stale_profile )
172
216
except AppStoreConnectApiError as err :
173
- error = f"Listing { Profile .s } for { BundleId } { bundle_id .id } failed unexpectedly"
174
- self .logger .warning (Colors .YELLOW (f"{ error } : { err .error_response } " ))
175
- return True
217
+ error_message = f"- Failed to delete stale { Profile } { stale_profile .id } : { err .error_response } "
218
+ self .logger .warning (Colors .RED (error_message ))
219
+ else :
220
+ self .logger .info (Colors .GREEN (f"- Deleted stale { Profile } { stale_profile .id } " ))
176
221
177
- certificate_ids = {c .id for c in certificates }
178
- profiles = self .list_bundle_id_profiles (
222
+ def _find_usable_profiles (
223
+ self ,
224
+ bundle_ids : Sequence [BundleId ],
225
+ certificates : Sequence [SigningCertificate ],
226
+ profile_type : ProfileType ,
227
+ delete_stale_profiles : bool ,
228
+ ) -> List [Profile ]:
229
+ all_profiles = self .list_bundle_id_profiles (
179
230
[bundle_id .id for bundle_id in bundle_ids ],
180
231
profile_type = profile_type ,
181
232
profile_state = ProfileState .ACTIVE ,
182
233
should_print = False ,
183
234
)
184
- profiles = list (filter (has_certificate , profiles ))
235
+
236
+ usable_profiles , stale_profiles = [], []
237
+ certificate_ids = {c .id for c in certificates }
238
+ for profile in all_profiles :
239
+ try :
240
+ if self ._has_certificate (profile , certificate_ids ):
241
+ usable_profiles .append (profile )
242
+ except _StaleProfileError :
243
+ stale_profiles .append (profile )
244
+
245
+ self ._handle_stale_profiles (stale_profiles , delete_stale_profiles )
246
+ self .logger .info ("" )
247
+
248
+ return usable_profiles
249
+
250
+ def _get_or_create_profiles (
251
+ self ,
252
+ bundle_ids : Sequence [BundleId ],
253
+ certificates : Sequence [SigningCertificate ],
254
+ profile_type : ProfileType ,
255
+ create_resource : bool ,
256
+ platform : Optional [BundleIdPlatform ] = None ,
257
+ delete_stale_profiles : bool = False ,
258
+ ):
259
+ profiles = self ._find_usable_profiles (
260
+ bundle_ids ,
261
+ certificates ,
262
+ profile_type ,
263
+ delete_stale_profiles ,
264
+ )
185
265
186
266
certificate_names = ", " .join (c .get_display_info () for c in certificates )
187
267
message = f"that contain { SigningCertificate .plural (len (certificates ))} { certificate_names } "
@@ -190,8 +270,9 @@ def missing_profile(bundle_id) -> bool:
190
270
self .logger .info (f"- { profile .get_display_info ()} " )
191
271
192
272
profile_ids = {p .id for p in profiles }
193
- bundle_ids_without_profiles = list ( filter ( missing_profile , bundle_ids ))
273
+ bundle_ids_without_profiles = [ bid for bid in bundle_ids if not self . _has_profile ( bid , profile_ids )]
194
274
if bundle_ids_without_profiles and not create_resource :
275
+ self .logger .info ("" )
195
276
missing = ", " .join (f'"{ bid .attributes .identifier } " [{ bid .id } ]' for bid in bundle_ids_without_profiles )
196
277
raise AppStoreConnectError (f"Did not find { profile_type } { Profile .s } for { BundleId .s } : { missing } " )
197
278
0 commit comments