gowalk-cicd 1.0.6 → 1.0.8
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/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.8
|
|
@@ -28,6 +28,13 @@ from pathlib import Path
|
|
|
28
28
|
from asc_common import get_json, request
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# asc_common.request signals a non-retryable failure with SystemExit, which is
|
|
32
|
+
# a BaseException — `except Exception` sails straight past it. Spelling both
|
|
33
|
+
# out is what makes the "best-effort" promise below actually hold; without it
|
|
34
|
+
# a capability call that Apple rejects kills the whole signing step.
|
|
35
|
+
_API_FAILURES = (Exception, SystemExit)
|
|
36
|
+
|
|
37
|
+
|
|
31
38
|
# Entitlement key -> App Store Connect capabilityType, for capabilities that
|
|
32
39
|
# are a bare toggle. An entitlement absent from this map is not an error: most
|
|
33
40
|
# entitlements (keychain-access-groups, get-task-allow, ...) need no App ID
|
|
@@ -100,11 +107,10 @@ def warn_about_manual_entitlements(entitlement_keys: set[str], bundle_id: str) -
|
|
|
100
107
|
|
|
101
108
|
def enabled_capabilities(token: str, bundle_pk: str) -> set[str]:
|
|
102
109
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
except Exception as exc: # noqa: BLE001 - never fail signing over a read
|
|
110
|
+
# No `limit` here: this relationship rejects it outright with
|
|
111
|
+
# PARAMETER_ERROR.ILLEGAL. Apple returns the full capability set.
|
|
112
|
+
data = get_json(f"/bundleIds/{bundle_pk}/bundleIdCapabilities", token)
|
|
113
|
+
except _API_FAILURES as exc:
|
|
108
114
|
print(f"::warning::could not list capabilities for {bundle_pk}: {exc!r}")
|
|
109
115
|
return set()
|
|
110
116
|
found = set()
|
|
@@ -112,6 +118,7 @@ def enabled_capabilities(token: str, bundle_pk: str) -> set[str]:
|
|
|
112
118
|
capability = (item.get("attributes") or {}).get("capabilityType")
|
|
113
119
|
if capability:
|
|
114
120
|
found.add(capability)
|
|
121
|
+
print(f"App ID {bundle_pk} capabilities: {', '.join(sorted(found)) or '(none)'}")
|
|
115
122
|
return found
|
|
116
123
|
|
|
117
124
|
|
|
@@ -126,15 +133,22 @@ def enable_capability(token: str, bundle_pk: str, capability: str) -> bool:
|
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
try:
|
|
129
|
-
# 409 means another writer got there first — the desired end state.
|
|
130
136
|
response = request(
|
|
131
137
|
"POST", "/bundleIdCapabilities", token,
|
|
132
138
|
json_body=body, allow_status={200, 201, 409},
|
|
133
139
|
)
|
|
134
|
-
except
|
|
140
|
+
except _API_FAILURES as exc:
|
|
135
141
|
print(f"::warning::could not enable {capability}: {exc!r}")
|
|
136
142
|
return False
|
|
137
143
|
if response.status_code == 409:
|
|
144
|
+
# Could be "already on" — or Apple refusing outright, which reads
|
|
145
|
+
# identically from the status code alone. Surface the body: without it
|
|
146
|
+
# the run just reports a profile missing a capability we claimed to
|
|
147
|
+
# have enabled, and there is nothing in the log to explain why.
|
|
148
|
+
print(
|
|
149
|
+
f"::warning::App Store Connect returned 409 for {capability}: "
|
|
150
|
+
f"{response.text[:600]}"
|
|
151
|
+
)
|
|
138
152
|
return False
|
|
139
153
|
print(f"Enabled {capability} on the App ID")
|
|
140
154
|
return True
|
|
@@ -143,9 +157,13 @@ def enable_capability(token: str, bundle_pk: str, capability: str) -> bool:
|
|
|
143
157
|
def reconcile(token: str, bundle_pk: str, bundle_id: str, entitlement_keys: set[str]) -> bool:
|
|
144
158
|
"""Enable every simple capability the entitlements imply.
|
|
145
159
|
|
|
146
|
-
Returns True when
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
Returns True when the caller must regenerate the profile — which is
|
|
161
|
+
whenever the App ID was missing something, NOT merely when a POST
|
|
162
|
+
succeeded. A cached profile was issued while the App ID lacked these, so
|
|
163
|
+
reusing it reproduces the same archive failure either way; regenerating at
|
|
164
|
+
least picks up the App ID's current state, and if the capability is still
|
|
165
|
+
absent the error comes from Apple describing the real problem rather than
|
|
166
|
+
from a stale cache.
|
|
149
167
|
"""
|
|
150
168
|
warn_about_manual_entitlements(entitlement_keys, bundle_id)
|
|
151
169
|
wanted = required_capabilities(entitlement_keys)
|
|
@@ -158,9 +176,6 @@ def reconcile(token: str, bundle_pk: str, bundle_id: str, entitlement_keys: set[
|
|
|
158
176
|
f"{bundle_id}: entitlements require {', '.join(sorted(missing))} "
|
|
159
177
|
f"but the App ID does not have them; enabling"
|
|
160
178
|
)
|
|
161
|
-
|
|
162
|
-
# must be attempted, not just up to the first success.
|
|
163
|
-
return any([
|
|
179
|
+
for capability in sorted(missing):
|
|
164
180
|
enable_capability(token, bundle_pk, capability)
|
|
165
|
-
|
|
166
|
-
])
|
|
181
|
+
return True
|
|
@@ -109,15 +109,36 @@ class ReconcileTest(unittest.TestCase):
|
|
|
109
109
|
|
|
110
110
|
self.assertEqual(sorted(posted), ["APP_ATTEST", "PUSH_NOTIFICATIONS"])
|
|
111
111
|
|
|
112
|
-
def
|
|
112
|
+
def test_regenerates_even_when_the_capability_could_not_be_enabled(self) -> None:
|
|
113
|
+
# A 409 (or any refusal) still means the cached profile was issued
|
|
114
|
+
# while the App ID lacked the capability. Reusing it reproduces the
|
|
115
|
+
# archive failure and hides Apple's real error behind a stale cache.
|
|
116
|
+
response = self._response(409)
|
|
117
|
+
response.text = '{"errors":[{"detail":"nope"}]}'
|
|
113
118
|
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
114
|
-
mock.patch.object(capabilities, "request", return_value=
|
|
119
|
+
mock.patch.object(capabilities, "request", return_value=response):
|
|
115
120
|
changed = capabilities.reconcile(
|
|
116
121
|
"tok", "PK", "com.gowalk.form",
|
|
117
122
|
{"com.apple.developer.devicecheck.appattest-environment"},
|
|
118
123
|
)
|
|
119
124
|
|
|
120
|
-
self.
|
|
125
|
+
self.assertTrue(changed)
|
|
126
|
+
|
|
127
|
+
def test_surfaces_the_409_body(self) -> None:
|
|
128
|
+
response = self._response(409)
|
|
129
|
+
response.text = '{"errors":[{"detail":"capability is not modifiable"}]}'
|
|
130
|
+
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
131
|
+
mock.patch.object(capabilities, "request", return_value=response), \
|
|
132
|
+
mock.patch("builtins.print") as printed:
|
|
133
|
+
capabilities.reconcile(
|
|
134
|
+
"tok", "PK", "com.gowalk.form",
|
|
135
|
+
{"com.apple.developer.devicecheck.appattest-environment"},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
messages = [c.args[0] for c in printed.call_args_list if c.args]
|
|
139
|
+
self.assertTrue(
|
|
140
|
+
any("capability is not modifiable" in m for m in messages), messages
|
|
141
|
+
)
|
|
121
142
|
|
|
122
143
|
def test_api_failure_does_not_raise(self) -> None:
|
|
123
144
|
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
@@ -127,7 +148,29 @@ class ReconcileTest(unittest.TestCase):
|
|
|
127
148
|
{"com.apple.developer.devicecheck.appattest-environment"},
|
|
128
149
|
)
|
|
129
150
|
|
|
130
|
-
self.
|
|
151
|
+
self.assertTrue(changed)
|
|
152
|
+
|
|
153
|
+
def test_systemexit_from_asc_does_not_kill_signing(self) -> None:
|
|
154
|
+
# asc_common.request raises SystemExit, not Exception, so a bare
|
|
155
|
+
# `except Exception` would let an Apple 4xx abort the whole run.
|
|
156
|
+
# An unreadable App ID is also unknown state, so the profile is
|
|
157
|
+
# regenerated rather than trusted from cache.
|
|
158
|
+
with mock.patch.object(capabilities, "get_json", side_effect=SystemExit("400")), \
|
|
159
|
+
mock.patch.object(capabilities, "request", side_effect=SystemExit("400")):
|
|
160
|
+
changed = capabilities.reconcile(
|
|
161
|
+
"tok", "PK", "com.gowalk.form",
|
|
162
|
+
{"com.apple.developer.devicecheck.appattest-environment"},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.assertTrue(changed)
|
|
166
|
+
|
|
167
|
+
def test_capability_listing_sends_no_limit_parameter(self) -> None:
|
|
168
|
+
# /bundleIds/{id}/bundleIdCapabilities rejects `limit` with
|
|
169
|
+
# PARAMETER_ERROR.ILLEGAL rather than ignoring it.
|
|
170
|
+
with mock.patch.object(capabilities, "get_json", return_value={"data": []}) as get:
|
|
171
|
+
capabilities.enabled_capabilities("tok", "PK")
|
|
172
|
+
|
|
173
|
+
get.assert_called_once_with("/bundleIds/PK/bundleIdCapabilities", "tok")
|
|
131
174
|
|
|
132
175
|
def test_warns_about_capabilities_it_will_not_guess(self) -> None:
|
|
133
176
|
with mock.patch.object(capabilities, "get_json", return_value={"data": []}), \
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.8
|