Skip to content

Commit 6d48649

Browse files
authored
Add option to remove stale profiles when running app-store-connect fetch-signing-files (#469)
1 parent 667ac8a commit 6d48649

File tree

6 files changed

+140
-28
lines changed

6 files changed

+140
-28
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
Version 0.60.0
2+
-------------
3+
4+
This release contains changes from [PR #469](https://github.com/codemagic-ci-cd/cli-tools/pull/469).
5+
6+
**Features**
7+
- Add option `--delete-stale-profiles` to action `app-store-connect fetch-signing-files` to delete encountered stale provisioning profiles. Those profiles are not shown in Apple Developer Portal and cannot be queried using normal App Store Connect API profiles read and listing endpoints.
8+
9+
**Docs**
10+
- Update docs for `app-store-connect fetch-signing-files`.
11+
112
Version 0.59.1
213
-------------
314

docs/app-store-connect/fetch-signing-files.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ app-store-connect fetch-signing-files [-h] [--log-stream STREAM] [--no-color] [-
2424
[--type PROFILE_TYPE]
2525
[--strict-match-identifier]
2626
[--create]
27+
[--delete-stale-profiles]
2728
BUNDLE_ID_IDENTIFIER
2829
```
2930
### Required arguments for action `fetch-signing-files`
@@ -62,6 +63,10 @@ Only match Bundle IDs that have exactly the same identifier specified by `BUNDLE
6263

6364

6465
Whether to create the resource if it does not exist yet
66+
##### `--delete-stale-profiles`
67+
68+
69+
Whether to delete invalid provisioning profiles that are discovered for specified bundle identifiers. Those profiles have active status with expiration date in the past, rendering them invalid. Such profiles are not shown in the Apple Developer Portal and they cannot be discovered using conventional read or list API queries.
6570
### Optional arguments for command `app-store-connect`
6671

6772
##### `--log-api-calls`

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "codemagic-cli-tools"
3-
version = "0.59.1"
3+
version = "0.60.0"
44
description = "CLI tools used in Codemagic builds"
55
readme = "README.md"
66
authors = [

src/codemagic/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__title__ = "codemagic-cli-tools"
22
__description__ = "CLI tools used in Codemagic builds"
3-
__version__ = "0.59.1.dev"
3+
__version__ = "0.60.0.dev"
44
__url__ = "https://github.com/codemagic-ci-cd/cli-tools"
55
__licence__ = "GNU General Public License v3.0"

src/codemagic/tools/app_store_connect/actions/fetch_signing_files_action.py

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import List
77
from typing import Optional
88
from typing import Sequence
9+
from typing import Set
910
from typing import Tuple
1011
from typing import Union
1112

@@ -18,6 +19,7 @@
1819
from codemagic.apple.resources import Profile
1920
from codemagic.apple.resources import ProfileState
2021
from codemagic.apple.resources import ProfileType
22+
from codemagic.apple.resources import ResourceId
2123
from codemagic.apple.resources import SigningCertificate
2224
from codemagic.cli import Colors
2325
from codemagic.models import PrivateKey
@@ -31,6 +33,15 @@
3133
from ..errors import AppStoreConnectError
3234

3335

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+
3445
class FetchSigningFilesAction(AbstractBaseAction, metaclass=ABCMeta):
3546
@cli.action(
3647
"fetch-signing-files",
@@ -42,6 +53,7 @@ class FetchSigningFilesAction(AbstractBaseAction, metaclass=ABCMeta):
4253
ProfileArgument.PROFILE_TYPE,
4354
BundleIdArgument.IDENTIFIER_STRICT_MATCH,
4455
CommonArgument.CREATE_RESOURCE,
56+
ProfileArgument.DELETE_STALE_PROFILES,
4557
)
4658
def fetch_signing_files(
4759
self,
@@ -53,6 +65,7 @@ def fetch_signing_files(
5365
profile_type: ProfileType = ProfileType.IOS_APP_DEVELOPMENT,
5466
bundle_id_identifier_strict_match: bool = False,
5567
create_resource: bool = False,
68+
delete_stale_profiles: bool = False,
5669
) -> Tuple[List[Profile], List[SigningCertificate]]:
5770
"""
5871
Fetch provisioning profiles and code signing certificates
@@ -90,6 +103,7 @@ def fetch_signing_files(
90103
profile_type,
91104
create_resource,
92105
platform,
106+
delete_stale_profiles,
93107
)
94108
self.echo("")
95109

@@ -148,40 +162,106 @@ def _get_or_create_certificates(
148162
certificates.append(certificate)
149163
return certificates
150164

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"\nFound {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
167211

168-
def missing_profile(bundle_id) -> bool:
212+
self.logger.info(f"\nFound {len(stale_profiles)} stale profiles, deleting them.")
213+
for stale_profile in stale_profiles:
169214
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)
172216
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}"))
176221

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(
179230
[bundle_id.id for bundle_id in bundle_ids],
180231
profile_type=profile_type,
181232
profile_state=ProfileState.ACTIVE,
182233
should_print=False,
183234
)
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+
)
185265

186266
certificate_names = ", ".join(c.get_display_info() for c in certificates)
187267
message = f"that contain {SigningCertificate.plural(len(certificates))} {certificate_names}"
@@ -190,8 +270,9 @@ def missing_profile(bundle_id) -> bool:
190270
self.logger.info(f"- {profile.get_display_info()}")
191271

192272
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)]
194274
if bundle_ids_without_profiles and not create_resource:
275+
self.logger.info("")
195276
missing = ", ".join(f'"{bid.attributes.identifier}" [{bid.id}]' for bid in bundle_ids_without_profiles)
196277
raise AppStoreConnectError(f"Did not find {profile_type} {Profile.s} for {BundleId.s}: {missing}")
197278

src/codemagic/tools/app_store_connect/arguments.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,21 @@ class ProfileArgument(cli.Argument):
15511551
"metavar": "device-id",
15521552
},
15531553
)
1554+
DELETE_STALE_PROFILES = cli.ArgumentProperties(
1555+
key="delete_stale_profiles",
1556+
flags=("--delete-stale-profiles",),
1557+
type=bool,
1558+
description=(
1559+
"Whether to delete invalid provisioning profiles that are discovered for specified bundle identifiers. "
1560+
"Those profiles have active status with expiration date in the past, rendering them invalid. Such "
1561+
"profiles are not shown in the Apple Developer Portal and they cannot be discovered using conventional "
1562+
"read or list API queries."
1563+
),
1564+
argparse_kwargs={
1565+
"required": False,
1566+
"action": "store_true",
1567+
},
1568+
)
15541569

15551570

15561571
class CommonArgument(cli.Argument):

0 commit comments

Comments
 (0)