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.
@@ -1 +1 @@
1
- 1.0.6
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
- data = get_json(
104
- f"/bundleIds/{bundle_pk}/bundleIdCapabilities", token,
105
- params={"limit": "200"},
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 Exception as exc: # noqa: BLE001 - Apple's archive error is clearer
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 something was actually turned onthe caller must then
147
- regenerate the profile, because a cached one predates the change and still
148
- lacks the capability.
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
- # List, not a generator: `any` short-circuits, and every missing capability
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
- for capability in sorted(missing)
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 test_existing_capability_race_is_not_a_change(self) -> None:
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=self._response(409)):
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.assertFalse(changed)
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.assertFalse(changed)
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.6
1
+ 1.0.8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {