gowalk-cicd 1.0.7 → 1.0.9
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.
package/README.md
CHANGED
|
@@ -206,15 +206,23 @@ Provisioning profile "CI-com.example.app" doesn't include the App Attest capabil
|
|
|
206
206
|
|
|
207
207
|
Before creating a profile, the action reads the target's
|
|
208
208
|
`CODE_SIGN_ENTITLEMENTS` plist and enables the matching capabilities on the App
|
|
209
|
-
ID — Push Notifications,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
ID — Push Notifications, Associated Domains, HealthKit, SiriKit, HomeKit,
|
|
210
|
+
network extensions and the other plain on/off toggles. Whenever the App ID was
|
|
211
|
+
missing something it regenerates the profile rather than reusing the cached
|
|
212
|
+
one, which was issued before the capability existed.
|
|
213
|
+
|
|
214
|
+
Two classes of capability are **not** enabled automatically, and each gets a
|
|
215
|
+
`::warning::` naming what was seen:
|
|
216
|
+
|
|
217
|
+
- **Values the action cannot infer** — App Groups, iCloud containers, Apple Pay
|
|
218
|
+
merchant IDs, Wallet pass types, Data Protection. Configure these on the App
|
|
219
|
+
ID yourself.
|
|
220
|
+
- **App Attest** — a real App ID capability in the developer portal, but absent
|
|
221
|
+
from the ASC API's `capabilityType` enum, so *no* API call can turn it on.
|
|
222
|
+
Any app using Firebase App Check's `AppAttestProvider` must have App Attest
|
|
223
|
+
ticked once by hand under **Certificates, Identifiers & Profiles → Identifiers
|
|
224
|
+
→ <your App ID>**. Until then the archive fails with `Provisioning profile
|
|
225
|
+
"CI-<bundle>" doesn't include the App Attest capability`.
|
|
218
226
|
|
|
219
227
|
## AI metadata auto-fill
|
|
220
228
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.9
|
|
@@ -41,7 +41,6 @@ _API_FAILURES = (Exception, SystemExit)
|
|
|
41
41
|
# capability at all.
|
|
42
42
|
CAPABILITY_BY_ENTITLEMENT = {
|
|
43
43
|
"aps-environment": "PUSH_NOTIFICATIONS",
|
|
44
|
-
"com.apple.developer.devicecheck.appattest-environment": "APP_ATTEST",
|
|
45
44
|
"com.apple.developer.associated-domains": "ASSOCIATED_DOMAINS",
|
|
46
45
|
"com.apple.developer.applesignin": "APPLE_ID_AUTH",
|
|
47
46
|
"com.apple.developer.networking.wifi-info": "ACCESS_WIFI_INFORMATION",
|
|
@@ -66,6 +65,14 @@ CAPABILITY_BY_ENTITLEMENT = {
|
|
|
66
65
|
# unambiguously. Enabling these blind would create a half-configured App ID,
|
|
67
66
|
# so we say what is missing and stop.
|
|
68
67
|
MANUAL_ENTITLEMENTS = {
|
|
68
|
+
# App Attest is a real App ID capability in the developer portal, but it is
|
|
69
|
+
# absent from the ASC API's capabilityType enum entirely — POSTing it
|
|
70
|
+
# returns 409 ENTITY_ERROR.ATTRIBUTE.TYPE listing the valid values, which
|
|
71
|
+
# do not include it. There is no API to turn this one on.
|
|
72
|
+
"com.apple.developer.devicecheck.appattest-environment": (
|
|
73
|
+
"App Attest, which the App Store Connect API cannot enable — tick it "
|
|
74
|
+
"on the App ID under Certificates, Identifiers & Profiles"
|
|
75
|
+
),
|
|
69
76
|
"com.apple.security.application-groups": "App Groups",
|
|
70
77
|
"com.apple.developer.icloud-container-identifiers": "iCloud",
|
|
71
78
|
"com.apple.developer.icloud-services": "iCloud",
|
|
@@ -118,6 +125,7 @@ def enabled_capabilities(token: str, bundle_pk: str) -> set[str]:
|
|
|
118
125
|
capability = (item.get("attributes") or {}).get("capabilityType")
|
|
119
126
|
if capability:
|
|
120
127
|
found.add(capability)
|
|
128
|
+
print(f"App ID {bundle_pk} capabilities: {', '.join(sorted(found)) or '(none)'}")
|
|
121
129
|
return found
|
|
122
130
|
|
|
123
131
|
|
|
@@ -132,7 +140,6 @@ def enable_capability(token: str, bundle_pk: str, capability: str) -> bool:
|
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
try:
|
|
135
|
-
# 409 means another writer got there first — the desired end state.
|
|
136
143
|
response = request(
|
|
137
144
|
"POST", "/bundleIdCapabilities", token,
|
|
138
145
|
json_body=body, allow_status={200, 201, 409},
|
|
@@ -141,6 +148,17 @@ def enable_capability(token: str, bundle_pk: str, capability: str) -> bool:
|
|
|
141
148
|
print(f"::warning::could not enable {capability}: {exc!r}")
|
|
142
149
|
return False
|
|
143
150
|
if response.status_code == 409:
|
|
151
|
+
# Could be "already on" — or Apple refusing outright, which reads
|
|
152
|
+
# identically from the status code alone. Surface the body: without it
|
|
153
|
+
# the run just reports a profile missing a capability we claimed to
|
|
154
|
+
# have enabled, and there is nothing in the log to explain why.
|
|
155
|
+
# Untruncated enough to show Apple's full list of valid capabilityType
|
|
156
|
+
# values, which is the only way to learn that a name is simply not
|
|
157
|
+
# API-manageable (see App Attest in MANUAL_ENTITLEMENTS).
|
|
158
|
+
print(
|
|
159
|
+
f"::warning::App Store Connect returned 409 for {capability}: "
|
|
160
|
+
f"{response.text[:2000]}"
|
|
161
|
+
)
|
|
144
162
|
return False
|
|
145
163
|
print(f"Enabled {capability} on the App ID")
|
|
146
164
|
return True
|
|
@@ -149,9 +167,13 @@ def enable_capability(token: str, bundle_pk: str, capability: str) -> bool:
|
|
|
149
167
|
def reconcile(token: str, bundle_pk: str, bundle_id: str, entitlement_keys: set[str]) -> bool:
|
|
150
168
|
"""Enable every simple capability the entitlements imply.
|
|
151
169
|
|
|
152
|
-
Returns True when
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
Returns True when the caller must regenerate the profile — which is
|
|
171
|
+
whenever the App ID was missing something, NOT merely when a POST
|
|
172
|
+
succeeded. A cached profile was issued while the App ID lacked these, so
|
|
173
|
+
reusing it reproduces the same archive failure either way; regenerating at
|
|
174
|
+
least picks up the App ID's current state, and if the capability is still
|
|
175
|
+
absent the error comes from Apple describing the real problem rather than
|
|
176
|
+
from a stale cache.
|
|
155
177
|
"""
|
|
156
178
|
warn_about_manual_entitlements(entitlement_keys, bundle_id)
|
|
157
179
|
wanted = required_capabilities(entitlement_keys)
|
|
@@ -164,9 +186,6 @@ def reconcile(token: str, bundle_pk: str, bundle_id: str, entitlement_keys: set[
|
|
|
164
186
|
f"{bundle_id}: entitlements require {', '.join(sorted(missing))} "
|
|
165
187
|
f"but the App ID does not have them; enabling"
|
|
166
188
|
)
|
|
167
|
-
|
|
168
|
-
# must be attempted, not just up to the first success.
|
|
169
|
-
return any([
|
|
189
|
+
for capability in sorted(missing):
|
|
170
190
|
enable_capability(token, bundle_pk, capability)
|
|
171
|
-
|
|
172
|
-
])
|
|
191
|
+
return True
|
|
@@ -19,6 +19,7 @@ import capabilities
|
|
|
19
19
|
|
|
20
20
|
ENTITLEMENTS = {
|
|
21
21
|
"aps-environment": "production",
|
|
22
|
+
"com.apple.developer.associated-domains": ["applinks:gowalk.example"],
|
|
22
23
|
"com.apple.developer.devicecheck.appattest-environment": "production",
|
|
23
24
|
"keychain-access-groups": ["$(AppIdentifierPrefix)com.gowalk.form"],
|
|
24
25
|
}
|
|
@@ -46,12 +47,26 @@ class ReadEntitlementsTest(unittest.TestCase):
|
|
|
46
47
|
|
|
47
48
|
class RequiredCapabilitiesTest(unittest.TestCase):
|
|
48
49
|
def test_maps_only_known_toggles(self) -> None:
|
|
49
|
-
# keychain-access-groups needs no App ID capability at all
|
|
50
|
+
# keychain-access-groups needs no App ID capability at all, and App
|
|
51
|
+
# Attest is not in the ASC API's capabilityType enum, so neither can
|
|
52
|
+
# produce a POST.
|
|
50
53
|
self.assertEqual(
|
|
51
54
|
capabilities.required_capabilities(set(ENTITLEMENTS)),
|
|
52
|
-
{"PUSH_NOTIFICATIONS", "
|
|
55
|
+
{"PUSH_NOTIFICATIONS", "ASSOCIATED_DOMAINS"},
|
|
53
56
|
)
|
|
54
57
|
|
|
58
|
+
def test_app_attest_is_reported_not_attempted(self) -> None:
|
|
59
|
+
key = "com.apple.developer.devicecheck.appattest-environment"
|
|
60
|
+
|
|
61
|
+
self.assertNotIn(key, capabilities.CAPABILITY_BY_ENTITLEMENT)
|
|
62
|
+
self.assertIn(key, capabilities.MANUAL_ENTITLEMENTS)
|
|
63
|
+
|
|
64
|
+
with mock.patch("builtins.print") as printed:
|
|
65
|
+
capabilities.warn_about_manual_entitlements({key}, "com.gowalk.form")
|
|
66
|
+
|
|
67
|
+
messages = [c.args[0] for c in printed.call_args_list if c.args]
|
|
68
|
+
self.assertTrue(any("App Attest" in m for m in messages), messages)
|
|
69
|
+
|
|
55
70
|
def test_ignores_entitlements_with_no_capability(self) -> None:
|
|
56
71
|
self.assertEqual(
|
|
57
72
|
capabilities.required_capabilities({"get-task-allow", "com.apple.security.get-task-allow"}),
|
|
@@ -78,13 +93,13 @@ class ReconcileTest(unittest.TestCase):
|
|
|
78
93
|
changed = capabilities.reconcile("tok", "PK", "com.gowalk.form", set(ENTITLEMENTS))
|
|
79
94
|
|
|
80
95
|
self.assertTrue(changed)
|
|
81
|
-
self.assertEqual(posted, ["
|
|
96
|
+
self.assertEqual(posted, ["ASSOCIATED_DOMAINS"])
|
|
82
97
|
|
|
83
98
|
def test_no_op_when_the_app_id_already_has_everything(self) -> None:
|
|
84
99
|
listed = {
|
|
85
100
|
"data": [
|
|
86
101
|
{"attributes": {"capabilityType": "PUSH_NOTIFICATIONS"}},
|
|
87
|
-
{"attributes": {"capabilityType": "
|
|
102
|
+
{"attributes": {"capabilityType": "ASSOCIATED_DOMAINS"}},
|
|
88
103
|
]
|
|
89
104
|
}
|
|
90
105
|
with mock.patch.object(capabilities, "get_json", return_value=listed), \
|
|
@@ -107,39 +122,62 @@ class ReconcileTest(unittest.TestCase):
|
|
|
107
122
|
mock.patch.object(capabilities, "request", side_effect=fake_request):
|
|
108
123
|
capabilities.reconcile("tok", "PK", "com.gowalk.form", set(ENTITLEMENTS))
|
|
109
124
|
|
|
110
|
-
self.assertEqual(sorted(posted), ["
|
|
125
|
+
self.assertEqual(sorted(posted), ["ASSOCIATED_DOMAINS", "PUSH_NOTIFICATIONS"])
|
|
111
126
|
|
|
112
|
-
def
|
|
127
|
+
def test_regenerates_even_when_the_capability_could_not_be_enabled(self) -> None:
|
|
128
|
+
# A 409 (or any refusal) still means the cached profile was issued
|
|
129
|
+
# while the App ID lacked the capability. Reusing it reproduces the
|
|
130
|
+
# archive failure and hides Apple's real error behind a stale cache.
|
|
131
|
+
response = self._response(409)
|
|
132
|
+
response.text = '{"errors":[{"detail":"nope"}]}'
|
|
113
133
|
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
114
|
-
mock.patch.object(capabilities, "request", return_value=
|
|
134
|
+
mock.patch.object(capabilities, "request", return_value=response):
|
|
115
135
|
changed = capabilities.reconcile(
|
|
116
136
|
"tok", "PK", "com.gowalk.form",
|
|
117
|
-
{"com.apple.developer.
|
|
137
|
+
{"com.apple.developer.associated-domains"},
|
|
118
138
|
)
|
|
119
139
|
|
|
120
|
-
self.
|
|
140
|
+
self.assertTrue(changed)
|
|
141
|
+
|
|
142
|
+
def test_surfaces_the_409_body(self) -> None:
|
|
143
|
+
response = self._response(409)
|
|
144
|
+
response.text = '{"errors":[{"detail":"capability is not modifiable"}]}'
|
|
145
|
+
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
146
|
+
mock.patch.object(capabilities, "request", return_value=response), \
|
|
147
|
+
mock.patch("builtins.print") as printed:
|
|
148
|
+
capabilities.reconcile(
|
|
149
|
+
"tok", "PK", "com.gowalk.form",
|
|
150
|
+
{"com.apple.developer.associated-domains"},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
messages = [c.args[0] for c in printed.call_args_list if c.args]
|
|
154
|
+
self.assertTrue(
|
|
155
|
+
any("capability is not modifiable" in m for m in messages), messages
|
|
156
|
+
)
|
|
121
157
|
|
|
122
158
|
def test_api_failure_does_not_raise(self) -> None:
|
|
123
159
|
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
124
160
|
mock.patch.object(capabilities, "request", side_effect=RuntimeError("boom")):
|
|
125
161
|
changed = capabilities.reconcile(
|
|
126
162
|
"tok", "PK", "com.gowalk.form",
|
|
127
|
-
{"com.apple.developer.
|
|
163
|
+
{"com.apple.developer.associated-domains"},
|
|
128
164
|
)
|
|
129
165
|
|
|
130
|
-
self.
|
|
166
|
+
self.assertTrue(changed)
|
|
131
167
|
|
|
132
168
|
def test_systemexit_from_asc_does_not_kill_signing(self) -> None:
|
|
133
169
|
# asc_common.request raises SystemExit, not Exception, so a bare
|
|
134
170
|
# `except Exception` would let an Apple 4xx abort the whole run.
|
|
171
|
+
# An unreadable App ID is also unknown state, so the profile is
|
|
172
|
+
# regenerated rather than trusted from cache.
|
|
135
173
|
with mock.patch.object(capabilities, "get_json", side_effect=SystemExit("400")), \
|
|
136
174
|
mock.patch.object(capabilities, "request", side_effect=SystemExit("400")):
|
|
137
175
|
changed = capabilities.reconcile(
|
|
138
176
|
"tok", "PK", "com.gowalk.form",
|
|
139
|
-
{"com.apple.developer.
|
|
177
|
+
{"com.apple.developer.associated-domains"},
|
|
140
178
|
)
|
|
141
179
|
|
|
142
|
-
self.
|
|
180
|
+
self.assertTrue(changed)
|
|
143
181
|
|
|
144
182
|
def test_capability_listing_sends_no_limit_parameter(self) -> None:
|
|
145
183
|
# /bundleIds/{id}/bundleIdCapabilities rejects `limit` with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.9
|