@daemux/store-automator 0.10.80 → 0.10.82

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.80"
8
+ "version": "0.10.82"
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.80",
15
+ "version": "0.10.82",
16
16
  "keywords": [
17
17
  "flutter",
18
18
  "app-store",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemux/store-automator",
3
- "version": "0.10.80",
3
+ "version": "0.10.82",
4
4
  "description": "Full App Store & Google Play automation for Flutter apps with Claude Code agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "store-automator",
3
- "version": "0.10.80",
3
+ "version": "0.10.82",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
@@ -29,6 +29,7 @@ from __future__ import annotations
29
29
  import base64
30
30
  import json
31
31
  import plistlib
32
+ import re
32
33
  import subprocess
33
34
  from pathlib import Path
34
35
 
@@ -36,7 +37,14 @@ from asc_common import get_json, request
36
37
 
37
38
 
38
39
  PROFILE_PREFIX = "CI-"
39
- PROFILES_DIR = Path.home() / "Library/MobileDevice/Provisioning Profiles"
40
+
41
+ # Xcode 16+ moved the canonical profile directory. Older Xcode releases used
42
+ # ~/Library/MobileDevice/Provisioning Profiles/. We write to BOTH so the same
43
+ # script works on macos-14 (Xcode 15) and macos-15 (Xcode 26).
44
+ _PROFILE_DIRS = [
45
+ Path.home() / "Library/Developer/Xcode/UserData/Provisioning Profiles",
46
+ Path.home() / "Library/MobileDevice/Provisioning Profiles",
47
+ ]
40
48
 
41
49
  # Product types that must be signed with a provisioning profile. App
42
50
  # extensions share the common ``com.apple.product-type.app-extension`` prefix
@@ -125,55 +133,135 @@ def patch_project_signing(
125
133
  ) -> None:
126
134
  """Set manual signing + per-target profile name for every signable target.
127
135
 
128
- Uses ``plutil -replace`` to mutate the pbxproj in place. Scope is limited
129
- to native app + extension targets so SwiftPM / resource-bundle targets
130
- (which reject PROVISIONING_PROFILE_SPECIFIER) are untouched.
136
+ The pbxproj stays in its original OpenStep format Xcode is very
137
+ opinionated about that file and any conversion (to XML/JSON) risks
138
+ re-ordering keys or dropping comments. Instead, we edit the specific
139
+ ``XCBuildConfiguration`` blocks by their UUID and inject/replace the
140
+ handful of keys we care about. Targets outside ``targets`` (most
141
+ importantly SwiftPM / resource-bundle configs) are untouched.
131
142
  """
132
- pbx = Path(project_path) / "project.pbxproj"
143
+ pbx_path = Path(project_path) / "project.pbxproj"
144
+ text = pbx_path.read_text(encoding="utf-8")
145
+
133
146
  for target in targets:
134
147
  profile_name = f"{PROFILE_PREFIX}{target['bundle_id']}"
135
148
  for cid in target["config_ids"]:
136
- _set_build_setting(pbx, cid, "CODE_SIGN_STYLE", "Manual")
137
- _set_build_setting(pbx, cid, "CODE_SIGN_IDENTITY", "Apple Distribution")
138
- _set_build_setting(pbx, cid, "DEVELOPMENT_TEAM", team_id)
139
- _set_build_setting(
140
- pbx, cid, "PROVISIONING_PROFILE_SPECIFIER", profile_name
141
- )
142
- # Clear any legacy autogenerated PROVISIONING_PROFILE uuid that
143
- # would otherwise override the specifier we just set.
144
- _clear_build_setting(pbx, cid, "PROVISIONING_PROFILE")
149
+ text = _apply_signing_to_config(text, cid, team_id, profile_name)
145
150
  print(
146
151
  f"Patched {target['name']!r} -> {profile_name} "
147
152
  f"(configs={len(target['config_ids'])})"
148
153
  )
149
154
 
155
+ pbx_path.write_text(text, encoding="utf-8")
156
+ # Sanity check — plutil -lint rejects malformed output so CI fails
157
+ # fast before ``xcodebuild`` tries (and mangles) the file.
158
+ subprocess.check_call(["plutil", "-lint", str(pbx_path)])
150
159
 
151
- def _keypath(config_id: str, key: str) -> str:
152
- return f"objects.{config_id}.buildSettings.{key}"
153
160
 
154
-
155
- def _set_build_setting(
156
- pbx_path: Path, config_id: str, key: str, value: str
157
- ) -> None:
158
- keypath = _keypath(config_id, key)
159
- # -replace fails if the key doesn't exist, so try replace first then
160
- # fall back to -insert for a fresh add.
161
- rc = subprocess.run(
162
- ["plutil", "-replace", keypath, "-string", value, str(pbx_path)],
163
- capture_output=True,
164
- ).returncode
165
- if rc != 0:
166
- subprocess.check_call(
167
- ["plutil", "-insert", keypath, "-string", value, str(pbx_path)]
161
+ def _apply_signing_to_config(
162
+ text: str, config_id: str, team_id: str, profile_name: str
163
+ ) -> str:
164
+ """Return ``text`` with the given XCBuildConfiguration's buildSettings
165
+ patched for manual signing. Raises ``SystemExit`` if the config block
166
+ can't be located (indicates a pbxproj format we don't understand).
167
+ """
168
+ start = text.find(f"\t\t{config_id} ")
169
+ if start < 0:
170
+ start = text.find(f"\t\t{config_id}\t")
171
+ if start < 0:
172
+ start = text.find(f"{config_id} = {{")
173
+ if start < 0:
174
+ raise SystemExit(
175
+ f"patch_project_signing: config {config_id} not found in pbxproj"
176
+ )
177
+ settings_key = "buildSettings = {"
178
+ s_idx = text.find(settings_key, start)
179
+ if s_idx < 0:
180
+ raise SystemExit(
181
+ f"patch_project_signing: buildSettings not found for {config_id}"
168
182
  )
183
+ # Find matching closing '};' for buildSettings — brace-balance forward.
184
+ depth = 0
185
+ i = s_idx + len(settings_key) - 1 # position at the opening '{'
186
+ end = -1
187
+ while i < len(text):
188
+ ch = text[i]
189
+ if ch == "{":
190
+ depth += 1
191
+ elif ch == "}":
192
+ depth -= 1
193
+ if depth == 0:
194
+ end = i
195
+ break
196
+ i += 1
197
+ if end < 0:
198
+ raise SystemExit(
199
+ f"patch_project_signing: unbalanced buildSettings for {config_id}"
200
+ )
201
+ inner = text[s_idx + len(settings_key): end]
202
+ patched = _patch_inner_settings(inner, team_id, profile_name)
203
+ return text[: s_idx + len(settings_key)] + patched + text[end:]
169
204
 
170
205
 
171
- def _clear_build_setting(pbx_path: Path, config_id: str, key: str) -> None:
172
- subprocess.run(
173
- ["plutil", "-remove", _keypath(config_id, key), str(pbx_path)],
174
- capture_output=True,
175
- check=False,
176
- )
206
+ _SIGNING_KEYS = {
207
+ "CODE_SIGN_STYLE": "Manual",
208
+ "CODE_SIGN_IDENTITY": '"Apple Distribution"',
209
+ }
210
+
211
+ _STRIP_KEYS = ("PROVISIONING_PROFILE",)
212
+
213
+
214
+ def _patch_inner_settings(
215
+ inner: str, team_id: str, profile_name: str
216
+ ) -> str:
217
+ """Rewrite build setting key/value lines inside one buildSettings block."""
218
+ # Always-set values
219
+ values = dict(_SIGNING_KEYS)
220
+ values["DEVELOPMENT_TEAM"] = team_id
221
+ values["PROVISIONING_PROFILE_SPECIFIER"] = f'"{profile_name}"'
222
+
223
+ lines = inner.splitlines(keepends=True)
224
+ emitted_keys: set[str] = set()
225
+ out: list[str] = []
226
+ # Each setting line looks like (whitespace-prefixed):
227
+ # KEY = VALUE;
228
+ # We match leading indent, the key, `= `, the rest.
229
+ pattern = re.compile(r"^(\s*)([A-Z_][A-Z0-9_]*)\s*=\s*(.+);\s*$")
230
+
231
+ for line in lines:
232
+ m = pattern.match(line)
233
+ if not m:
234
+ out.append(line)
235
+ continue
236
+ indent, key, _old_value = m.group(1), m.group(2), m.group(3)
237
+ if key in _STRIP_KEYS:
238
+ # Drop these keys entirely.
239
+ continue
240
+ if key in values:
241
+ out.append(f"{indent}{key} = {values[key]};\n")
242
+ emitted_keys.add(key)
243
+ continue
244
+ out.append(line)
245
+
246
+ # Any key we wanted to set but didn't find — append just before close.
247
+ missing = [k for k in values if k not in emitted_keys]
248
+ if missing:
249
+ # Determine indent from the first KEY= line we can find, else use
250
+ # two tabs (the pbxproj default).
251
+ indent = "\t\t\t\t"
252
+ for line in lines:
253
+ m = pattern.match(line)
254
+ if m:
255
+ indent = m.group(1)
256
+ break
257
+ tail = out[-1] if out else ""
258
+ # Ensure we inject before the trailing whitespace on the last line.
259
+ new_lines = [f"{indent}{k} = {values[k]};\n" for k in missing]
260
+ if tail.strip() == "":
261
+ out = out[:-1] + new_lines + [tail]
262
+ else:
263
+ out = out + new_lines
264
+ return "".join(out)
177
265
 
178
266
 
179
267
  # --------------------------------------------------------------------------- #
@@ -261,15 +349,26 @@ def create_profile(
261
349
 
262
350
 
263
351
  def install_profile(profile_der: bytes) -> str:
264
- PROFILES_DIR.mkdir(parents=True, exist_ok=True)
265
- tmp_path = PROFILES_DIR / "tmp.mobileprovision"
352
+ primary = _PROFILE_DIRS[0]
353
+ primary.mkdir(parents=True, exist_ok=True)
354
+ tmp_path = primary / "tmp.mobileprovision"
266
355
  tmp_path.write_bytes(profile_der)
267
356
  decoded = subprocess.check_output(
268
357
  ["security", "cms", "-D", "-i", str(tmp_path)]
269
358
  )
270
- uuid = plistlib.loads(decoded)["UUID"]
271
- final_path = PROFILES_DIR / f"{uuid}.mobileprovision"
272
- tmp_path.rename(final_path)
359
+ plist = plistlib.loads(decoded)
360
+ uuid = plist["UUID"]
361
+ team_ids = plist.get("TeamIdentifier") or []
362
+ profile_name = plist.get("Name") or "<unknown>"
363
+ final_name = f"{uuid}.mobileprovision"
364
+ for directory in _PROFILE_DIRS:
365
+ directory.mkdir(parents=True, exist_ok=True)
366
+ (directory / final_name).write_bytes(profile_der)
367
+ tmp_path.unlink(missing_ok=True)
368
+ print(
369
+ f" profile {profile_name!r}: uuid={uuid} team={team_ids} "
370
+ f"dirs={[str(d) for d in _PROFILE_DIRS]}"
371
+ )
273
372
  return uuid
274
373
 
275
374