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, App Attest, Associated Domains, Sign in with Apple,
210
- HealthKit, SiriKit, NFC, HomeKit, network extensions and the other plain
211
- on/off toggles. If it had to turn anything on, it regenerates the profile
212
- rather than reusing the cached one, which predates the change.
213
-
214
- Capabilities that carry values an entitlements file cannot supply App Groups,
215
- iCloud containers, Apple Pay merchant IDs, Wallet pass types, Data Protection —
216
- are **not** enabled automatically. The action emits a `::warning::` naming what
217
- it saw and leaves those for you to configure in the Apple Developer portal.
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
 
@@ -1 +1 @@
1
- 1.0.7
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 something was actually turned onthe caller must then
153
- regenerate the profile, because a cached one predates the change and still
154
- lacks the capability.
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
- # List, not a generator: `any` short-circuits, and every missing capability
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
- for capability in sorted(missing)
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", "APP_ATTEST"},
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, ["APP_ATTEST"])
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": "APP_ATTEST"}},
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), ["APP_ATTEST", "PUSH_NOTIFICATIONS"])
125
+ self.assertEqual(sorted(posted), ["ASSOCIATED_DOMAINS", "PUSH_NOTIFICATIONS"])
111
126
 
112
- def test_existing_capability_race_is_not_a_change(self) -> None:
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=self._response(409)):
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.devicecheck.appattest-environment"},
137
+ {"com.apple.developer.associated-domains"},
118
138
  )
119
139
 
120
- self.assertFalse(changed)
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.devicecheck.appattest-environment"},
163
+ {"com.apple.developer.associated-domains"},
128
164
  )
129
165
 
130
- self.assertFalse(changed)
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.devicecheck.appattest-environment"},
177
+ {"com.apple.developer.associated-domains"},
140
178
  )
141
179
 
142
- self.assertFalse(changed)
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.7
1
+ 1.0.9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {