@daemux/store-automator 0.10.76 → 0.10.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.78"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.78",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Prepare iOS code signing on a fresh CI runner (
|
|
3
|
+
Prepare iOS code signing on a fresh CI runner (manual-signing variant).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`-allowProvisioningUpdates` against the App Store Connect API:
|
|
5
|
+
Steps:
|
|
7
6
|
|
|
8
|
-
1. Generate an RSA private key + CSR
|
|
7
|
+
1. Generate an RSA private key + CSR.
|
|
9
8
|
2. Create a new Apple Distribution certificate from the CSR; if the per-team
|
|
10
9
|
cap is hit (409), revoke the newest existing DISTRIBUTION cert and retry.
|
|
11
|
-
3.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
3. Enumerate PRODUCT_BUNDLE_IDENTIFIER for every signable target in the
|
|
11
|
+
Xcode project / workspace (main app + extensions).
|
|
12
|
+
4. Register each bundle ID on App Store Connect if missing.
|
|
13
|
+
5. Create one IOS_APP_STORE provisioning profile per bundle ID, named
|
|
14
|
+
``CI-<bundle_id>``, linked to the new cert (deleting any stale profile
|
|
15
|
+
with the same name so the reference matches the fresh cert).
|
|
16
|
+
6. Install every profile into ``~/Library/MobileDevice/Provisioning Profiles/``.
|
|
17
|
+
7. Import cert + private key into a temporary keychain placed at the head of
|
|
18
|
+
the user search list so codesign / xcodebuild can find it.
|
|
19
|
+
|
|
20
|
+
Pairs with a temporary xcconfig that sets
|
|
21
|
+
``PROVISIONING_PROFILE_SPECIFIER = CI-$(PRODUCT_BUNDLE_IDENTIFIER)``, which
|
|
22
|
+
Xcode resolves per target at build time. This lets apps with Network
|
|
23
|
+
Extensions (and any other aux targets) sign manually in CI without
|
|
24
|
+
per-project pbxproj edits.
|
|
18
25
|
|
|
19
26
|
Environment inputs:
|
|
20
|
-
ASC_KEY_ID
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
RUNNER_TEMP
|
|
27
|
+
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH - App Store Connect API key trio
|
|
28
|
+
PROJECT or WORKSPACE - Xcode project / workspace path
|
|
29
|
+
SCHEME, CONFIGURATION - Scheme + configuration to inspect
|
|
30
|
+
RUNNER_TEMP - GitHub Actions temp dir
|
|
24
31
|
|
|
25
32
|
Writes nothing to stdout that would leak secrets.
|
|
26
33
|
"""
|
|
@@ -38,6 +45,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
|
|
38
45
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
39
46
|
from cryptography.hazmat.primitives.serialization import pkcs12
|
|
40
47
|
from cryptography.x509.oid import NameOID
|
|
48
|
+
from profile_manager import discover_bundle_ids, provision_all_bundles
|
|
41
49
|
|
|
42
50
|
|
|
43
51
|
def env(name: str) -> str:
|
|
@@ -47,6 +55,10 @@ def env(name: str) -> str:
|
|
|
47
55
|
return val
|
|
48
56
|
|
|
49
57
|
|
|
58
|
+
def optional_env(name: str) -> str:
|
|
59
|
+
return os.environ.get(name, "") or ""
|
|
60
|
+
|
|
61
|
+
|
|
50
62
|
def generate_key_and_csr() -> tuple[rsa.RSAPrivateKey, bytes]:
|
|
51
63
|
"""Return (private_key, csr_payload_b64_bytes)."""
|
|
52
64
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
@@ -55,14 +67,13 @@ def generate_key_and_csr() -> tuple[rsa.RSAPrivateKey, bytes]:
|
|
|
55
67
|
.subject_name(
|
|
56
68
|
x509.Name(
|
|
57
69
|
[
|
|
58
|
-
x509.NameAttribute(NameOID.COMMON_NAME, "
|
|
70
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "Daemux CI"),
|
|
59
71
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
|
60
72
|
]
|
|
61
73
|
)
|
|
62
74
|
)
|
|
63
75
|
.sign(private_key, hashes.SHA256())
|
|
64
76
|
)
|
|
65
|
-
# Apple wants the CSR PEM base64 (without headers)
|
|
66
77
|
csr_pem = csr.public_bytes(serialization.Encoding.PEM)
|
|
67
78
|
csr_payload = b"".join(
|
|
68
79
|
line for line in csr_pem.splitlines() if not line.startswith(b"-----")
|
|
@@ -85,8 +96,6 @@ def newest_distribution_cert_id(token: str) -> str | None:
|
|
|
85
96
|
newest_exp = ""
|
|
86
97
|
for cert in data.get("data", []):
|
|
87
98
|
attrs = cert.get("attributes") or {}
|
|
88
|
-
# Use expirationDate as a proxy for creation time (cert expiration
|
|
89
|
-
# is exactly creation + 1 year for Apple Distribution certs).
|
|
90
99
|
exp = attrs.get("expirationDate") or ""
|
|
91
100
|
if exp > newest_exp:
|
|
92
101
|
newest_exp = exp
|
|
@@ -94,17 +103,11 @@ def newest_distribution_cert_id(token: str) -> str | None:
|
|
|
94
103
|
return newest_id
|
|
95
104
|
|
|
96
105
|
|
|
97
|
-
def revoke_cert(token: str, cert_id: str) -> None:
|
|
98
|
-
print(f"Revoking distribution cert {cert_id}")
|
|
99
|
-
request("DELETE", f"/certificates/{cert_id}", token)
|
|
100
|
-
|
|
101
|
-
|
|
102
106
|
def create_distribution_cert(token: str, csr_b64: str) -> tuple[str, bytes]:
|
|
103
107
|
"""Return (certificate_id, DER-encoded certificate bytes).
|
|
104
108
|
|
|
105
109
|
Apple enforces a per-team cap on active Distribution certs. If the POST
|
|
106
|
-
comes back 409
|
|
107
|
-
one and retry once. This is the CI-owned cert from a prior run.
|
|
110
|
+
comes back 409, revoke the newest existing one and retry once.
|
|
108
111
|
"""
|
|
109
112
|
body = {
|
|
110
113
|
"data": {
|
|
@@ -126,12 +129,11 @@ def create_distribution_cert(token: str, csr_b64: str) -> tuple[str, bytes]:
|
|
|
126
129
|
"409 from cert create but no existing DISTRIBUTION cert "
|
|
127
130
|
"found to revoke"
|
|
128
131
|
)
|
|
129
|
-
|
|
132
|
+
request("DELETE", f"/certificates/{target}", token)
|
|
130
133
|
resp = request("POST", "/certificates", token, json_body=body)
|
|
131
134
|
data = resp.json()["data"]
|
|
132
135
|
cert_id = data["id"]
|
|
133
|
-
|
|
134
|
-
cert_der = base64.b64decode(cert_content_b64)
|
|
136
|
+
cert_der = base64.b64decode(data["attributes"]["certificateContent"])
|
|
135
137
|
print(f"Created DISTRIBUTION cert {cert_id}")
|
|
136
138
|
return cert_id, cert_der
|
|
137
139
|
|
|
@@ -141,7 +143,7 @@ def write_p12(
|
|
|
141
143
|
) -> None:
|
|
142
144
|
cert = x509.load_der_x509_certificate(cert_der)
|
|
143
145
|
p12 = pkcs12.serialize_key_and_certificates(
|
|
144
|
-
name=b"
|
|
146
|
+
name=b"Daemux CI",
|
|
145
147
|
key=private_key,
|
|
146
148
|
cert=cert,
|
|
147
149
|
cas=None,
|
|
@@ -153,7 +155,6 @@ def write_p12(
|
|
|
153
155
|
|
|
154
156
|
|
|
155
157
|
def _create_keychain(keychain_path: str, keychain_pass: str) -> None:
|
|
156
|
-
"""Delete any stale keychain at this path, then create + unlock a fresh one."""
|
|
157
158
|
subprocess.run(
|
|
158
159
|
["security", "delete-keychain", keychain_path],
|
|
159
160
|
check=False,
|
|
@@ -173,18 +174,16 @@ def _create_keychain(keychain_path: str, keychain_pass: str) -> None:
|
|
|
173
174
|
def _import_p12(
|
|
174
175
|
keychain_path: str, keychain_pass: str, p12_path: Path, p12_pass: str
|
|
175
176
|
) -> None:
|
|
176
|
-
"""Import p12 into the keychain and authorize codesign to use the key."""
|
|
177
177
|
subprocess.check_call(
|
|
178
178
|
[
|
|
179
179
|
"security", "import", str(p12_path),
|
|
180
180
|
"-P", p12_pass,
|
|
181
|
-
"-A",
|
|
181
|
+
"-A",
|
|
182
182
|
"-t", "cert",
|
|
183
183
|
"-f", "pkcs12",
|
|
184
184
|
"-k", keychain_path,
|
|
185
185
|
]
|
|
186
186
|
)
|
|
187
|
-
# Allow codesign to use the key without prompts (modern macOS)
|
|
188
187
|
subprocess.check_call(
|
|
189
188
|
[
|
|
190
189
|
"security", "set-key-partition-list",
|
|
@@ -197,7 +196,6 @@ def _import_p12(
|
|
|
197
196
|
|
|
198
197
|
|
|
199
198
|
def _prepend_to_user_search_list(keychain_path: str) -> None:
|
|
200
|
-
"""Put `keychain_path` at the head of the user search list + make it default."""
|
|
201
199
|
existing = subprocess.check_output(
|
|
202
200
|
["security", "list-keychains", "-d", "user"]
|
|
203
201
|
).decode()
|
|
@@ -233,14 +231,28 @@ def main() -> None:
|
|
|
233
231
|
asc_key_path = env("ASC_KEY_PATH")
|
|
234
232
|
runner_temp = env("RUNNER_TEMP")
|
|
235
233
|
|
|
234
|
+
project = optional_env("PROJECT")
|
|
235
|
+
workspace = optional_env("WORKSPACE")
|
|
236
|
+
scheme = env("SCHEME")
|
|
237
|
+
configuration = env("CONFIGURATION")
|
|
238
|
+
if not project and not workspace:
|
|
239
|
+
raise SystemExit(
|
|
240
|
+
"prepare_signing: either PROJECT or WORKSPACE env var must be set"
|
|
241
|
+
)
|
|
242
|
+
|
|
236
243
|
token = make_jwt(key_id, issuer_id, asc_key_path)
|
|
237
244
|
|
|
245
|
+
bundle_ids = discover_bundle_ids(project, workspace, scheme, configuration)
|
|
246
|
+
print(f"Bundle ids to provision ({len(bundle_ids)}): {bundle_ids}")
|
|
247
|
+
|
|
238
248
|
private_key, csr_b64 = generate_key_and_csr()
|
|
239
|
-
|
|
249
|
+
cert_id, cert_der = create_distribution_cert(token, csr_b64.decode())
|
|
250
|
+
|
|
251
|
+
mappings = provision_all_bundles(token, bundle_ids, cert_id)
|
|
252
|
+
print("Profile map:")
|
|
253
|
+
for bid, pname, uuid in mappings:
|
|
254
|
+
print(f" {bid} -> {pname} ({uuid})")
|
|
240
255
|
|
|
241
|
-
# Provisioning profile(s) are created on-demand by xcodebuild via
|
|
242
|
-
# `-allowProvisioningUpdates` — we only need the signing identity
|
|
243
|
-
# (cert + private key) to be present in a keychain Xcode can see.
|
|
244
256
|
p12_pass = "ci"
|
|
245
257
|
p12_path = Path(runner_temp) / "cert.p12"
|
|
246
258
|
write_p12(private_key, cert_der, p12_path, p12_pass)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Provisioning profile + bundleId helpers for multi-target iOS projects.
|
|
4
|
+
|
|
5
|
+
Handles:
|
|
6
|
+
|
|
7
|
+
* Enumerating every PRODUCT_BUNDLE_IDENTIFIER used by signable targets in an
|
|
8
|
+
Xcode project / workspace (main app + extensions like Network Extensions,
|
|
9
|
+
Widgets, WatchKit apps, etc).
|
|
10
|
+
* Ensuring each bundle ID is registered on App Store Connect.
|
|
11
|
+
* Creating one IOS_APP_STORE provisioning profile per bundle ID, named
|
|
12
|
+
``CI-<bundle_id>``, linked to the just-issued distribution cert. If a
|
|
13
|
+
profile with that name already exists, it is deleted first so we always
|
|
14
|
+
end up with a profile that references the fresh cert.
|
|
15
|
+
* Installing every profile into the standard Xcode profile directory.
|
|
16
|
+
|
|
17
|
+
Kept separate from ``prepare_signing.py`` so both files stay well under the
|
|
18
|
+
400-line cap.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import json
|
|
25
|
+
import plistlib
|
|
26
|
+
import subprocess
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from asc_common import get_json, request
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
PROFILE_PREFIX = "CI-"
|
|
33
|
+
PROFILES_DIR = Path.home() / "Library/MobileDevice/Provisioning Profiles"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------- #
|
|
37
|
+
# Bundle ID discovery #
|
|
38
|
+
# --------------------------------------------------------------------------- #
|
|
39
|
+
|
|
40
|
+
def discover_bundle_ids(
|
|
41
|
+
project: str,
|
|
42
|
+
workspace: str,
|
|
43
|
+
scheme: str,
|
|
44
|
+
configuration: str,
|
|
45
|
+
) -> list[str]:
|
|
46
|
+
"""Return the sorted, de-duplicated list of signable bundle IDs.
|
|
47
|
+
|
|
48
|
+
Uses ``xcodebuild -showBuildSettings -json`` which returns one settings
|
|
49
|
+
dictionary per target in the dependency graph for the given scheme. We
|
|
50
|
+
keep every ``PRODUCT_BUNDLE_IDENTIFIER`` that looks real (non-empty,
|
|
51
|
+
not a placeholder, contains at least one dot).
|
|
52
|
+
"""
|
|
53
|
+
proj_args = ["-workspace", workspace] if workspace else ["-project", project]
|
|
54
|
+
cmd = [
|
|
55
|
+
"xcodebuild",
|
|
56
|
+
*proj_args,
|
|
57
|
+
"-scheme", scheme,
|
|
58
|
+
"-configuration", configuration,
|
|
59
|
+
"-showBuildSettings",
|
|
60
|
+
"-json",
|
|
61
|
+
]
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
cmd, capture_output=True, text=True, check=True,
|
|
64
|
+
)
|
|
65
|
+
settings_list = json.loads(result.stdout or "[]")
|
|
66
|
+
found: set[str] = set()
|
|
67
|
+
for entry in settings_list:
|
|
68
|
+
attrs = entry.get("buildSettings") or {}
|
|
69
|
+
bundle_id = (attrs.get("PRODUCT_BUNDLE_IDENTIFIER") or "").strip()
|
|
70
|
+
if _is_real_bundle_id(bundle_id):
|
|
71
|
+
found.add(bundle_id)
|
|
72
|
+
if not found:
|
|
73
|
+
raise SystemExit(
|
|
74
|
+
"discover_bundle_ids: no PRODUCT_BUNDLE_IDENTIFIER values found "
|
|
75
|
+
f"for scheme {scheme!r} (configuration={configuration!r})"
|
|
76
|
+
)
|
|
77
|
+
return sorted(found)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_real_bundle_id(value: str) -> bool:
|
|
81
|
+
if not value:
|
|
82
|
+
return False
|
|
83
|
+
if "$(" in value or "${" in value:
|
|
84
|
+
return False
|
|
85
|
+
if "." not in value:
|
|
86
|
+
return False
|
|
87
|
+
# Xcode emits these when a target has no explicit identifier.
|
|
88
|
+
if value.lower().startswith("com.apple."):
|
|
89
|
+
return False
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------- #
|
|
94
|
+
# ASC bundle ID registration #
|
|
95
|
+
# --------------------------------------------------------------------------- #
|
|
96
|
+
|
|
97
|
+
def ensure_bundle_id(token: str, identifier: str) -> str:
|
|
98
|
+
"""Return the ASC primary key for ``identifier``; register if missing."""
|
|
99
|
+
data = get_json(
|
|
100
|
+
"/bundleIds",
|
|
101
|
+
token,
|
|
102
|
+
params={"filter[identifier]": identifier, "limit": "5"},
|
|
103
|
+
)
|
|
104
|
+
for item in data.get("data", []):
|
|
105
|
+
if item["attributes"]["identifier"] == identifier:
|
|
106
|
+
return item["id"]
|
|
107
|
+
return _register_bundle_id(token, identifier)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _register_bundle_id(token: str, identifier: str) -> str:
|
|
111
|
+
body = {
|
|
112
|
+
"data": {
|
|
113
|
+
"type": "bundleIds",
|
|
114
|
+
"attributes": {
|
|
115
|
+
"identifier": identifier,
|
|
116
|
+
"name": identifier.replace(".", "-"),
|
|
117
|
+
"platform": "IOS",
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
resp = request("POST", "/bundleIds", token, json_body=body)
|
|
122
|
+
bundle_pk = resp.json()["data"]["id"]
|
|
123
|
+
print(f"Registered new bundle id {identifier!r} (pk={bundle_pk})")
|
|
124
|
+
return bundle_pk
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --------------------------------------------------------------------------- #
|
|
128
|
+
# Profile lifecycle #
|
|
129
|
+
# --------------------------------------------------------------------------- #
|
|
130
|
+
|
|
131
|
+
def profile_name_for(bundle_id: str) -> str:
|
|
132
|
+
"""Return the deterministic CI profile name for a bundle id."""
|
|
133
|
+
return f"{PROFILE_PREFIX}{bundle_id}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def delete_profile_by_name(token: str, name: str) -> None:
|
|
137
|
+
"""Delete every existing profile with the given name (paginated scan)."""
|
|
138
|
+
deleted_any = False
|
|
139
|
+
next_path: str | None = "/profiles?limit=200"
|
|
140
|
+
while next_path:
|
|
141
|
+
data = get_json(next_path, token)
|
|
142
|
+
for p in data.get("data", []):
|
|
143
|
+
if (p["attributes"].get("name") or "") == name:
|
|
144
|
+
pid = p["id"]
|
|
145
|
+
print(f"Deleting stale profile {pid} ({name})")
|
|
146
|
+
request("DELETE", f"/profiles/{pid}", token)
|
|
147
|
+
deleted_any = True
|
|
148
|
+
next_link = (data.get("links") or {}).get("next")
|
|
149
|
+
if not next_link:
|
|
150
|
+
break
|
|
151
|
+
# Convert absolute next URL to path component the helper expects.
|
|
152
|
+
idx = next_link.find("/v1")
|
|
153
|
+
next_path = next_link[idx + len("/v1"):] if idx >= 0 else None
|
|
154
|
+
if not deleted_any:
|
|
155
|
+
print(f"No existing profile named {name!r}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_profile(
|
|
159
|
+
token: str, name: str, bundle_pk: str, cert_id: str
|
|
160
|
+
) -> bytes:
|
|
161
|
+
"""Create an IOS_APP_STORE profile and return its raw (CMS-signed) bytes."""
|
|
162
|
+
body = {
|
|
163
|
+
"data": {
|
|
164
|
+
"type": "profiles",
|
|
165
|
+
"attributes": {
|
|
166
|
+
"name": name,
|
|
167
|
+
"profileType": "IOS_APP_STORE",
|
|
168
|
+
},
|
|
169
|
+
"relationships": {
|
|
170
|
+
"bundleId": {"data": {"type": "bundleIds", "id": bundle_pk}},
|
|
171
|
+
"certificates": {
|
|
172
|
+
"data": [{"type": "certificates", "id": cert_id}]
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
resp = request("POST", "/profiles", token, json_body=body)
|
|
178
|
+
payload = resp.json()["data"]["attributes"]["profileContent"]
|
|
179
|
+
return base64.b64decode(payload)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def install_profile(profile_der: bytes) -> str:
|
|
183
|
+
"""Write the .mobileprovision into Xcode's profile dir; return UUID."""
|
|
184
|
+
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
tmp_path = PROFILES_DIR / "tmp.mobileprovision"
|
|
186
|
+
tmp_path.write_bytes(profile_der)
|
|
187
|
+
decoded = subprocess.check_output(
|
|
188
|
+
["security", "cms", "-D", "-i", str(tmp_path)]
|
|
189
|
+
)
|
|
190
|
+
uuid = plistlib.loads(decoded)["UUID"]
|
|
191
|
+
final_path = PROFILES_DIR / f"{uuid}.mobileprovision"
|
|
192
|
+
tmp_path.rename(final_path)
|
|
193
|
+
return uuid
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --------------------------------------------------------------------------- #
|
|
197
|
+
# Orchestration #
|
|
198
|
+
# --------------------------------------------------------------------------- #
|
|
199
|
+
|
|
200
|
+
def provision_all_bundles(
|
|
201
|
+
token: str,
|
|
202
|
+
bundle_ids: list[str],
|
|
203
|
+
cert_id: str,
|
|
204
|
+
) -> list[tuple[str, str, str]]:
|
|
205
|
+
"""Create + install a CI profile for each bundle id.
|
|
206
|
+
|
|
207
|
+
Returns a list of ``(bundle_id, profile_name, uuid)`` tuples in input
|
|
208
|
+
order so callers can log the full mapping.
|
|
209
|
+
"""
|
|
210
|
+
results: list[tuple[str, str, str]] = []
|
|
211
|
+
for bid in bundle_ids:
|
|
212
|
+
name = profile_name_for(bid)
|
|
213
|
+
bundle_pk = ensure_bundle_id(token, bid)
|
|
214
|
+
delete_profile_by_name(token, name)
|
|
215
|
+
profile_der = create_profile(token, name, bundle_pk, cert_id)
|
|
216
|
+
uuid = install_profile(profile_der)
|
|
217
|
+
print(f"Installed provisioning profile {name} -> {uuid} ({bid})")
|
|
218
|
+
results.append((bid, name, uuid))
|
|
219
|
+
return results
|