@daemux/store-automator 0.10.80 → 0.10.81

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.81"
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.81",
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.81",
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.81",
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
 
@@ -125,55 +126,135 @@ def patch_project_signing(
125
126
  ) -> None:
126
127
  """Set manual signing + per-target profile name for every signable target.
127
128
 
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.
129
+ The pbxproj stays in its original OpenStep format Xcode is very
130
+ opinionated about that file and any conversion (to XML/JSON) risks
131
+ re-ordering keys or dropping comments. Instead, we edit the specific
132
+ ``XCBuildConfiguration`` blocks by their UUID and inject/replace the
133
+ handful of keys we care about. Targets outside ``targets`` (most
134
+ importantly SwiftPM / resource-bundle configs) are untouched.
131
135
  """
132
- pbx = Path(project_path) / "project.pbxproj"
136
+ pbx_path = Path(project_path) / "project.pbxproj"
137
+ text = pbx_path.read_text(encoding="utf-8")
138
+
133
139
  for target in targets:
134
140
  profile_name = f"{PROFILE_PREFIX}{target['bundle_id']}"
135
141
  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")
142
+ text = _apply_signing_to_config(text, cid, team_id, profile_name)
145
143
  print(
146
144
  f"Patched {target['name']!r} -> {profile_name} "
147
145
  f"(configs={len(target['config_ids'])})"
148
146
  )
149
147
 
148
+ pbx_path.write_text(text, encoding="utf-8")
149
+ # Sanity check — plutil -lint rejects malformed output so CI fails
150
+ # fast before ``xcodebuild`` tries (and mangles) the file.
151
+ subprocess.check_call(["plutil", "-lint", str(pbx_path)])
150
152
 
151
- def _keypath(config_id: str, key: str) -> str:
152
- return f"objects.{config_id}.buildSettings.{key}"
153
153
 
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)]
154
+ def _apply_signing_to_config(
155
+ text: str, config_id: str, team_id: str, profile_name: str
156
+ ) -> str:
157
+ """Return ``text`` with the given XCBuildConfiguration's buildSettings
158
+ patched for manual signing. Raises ``SystemExit`` if the config block
159
+ can't be located (indicates a pbxproj format we don't understand).
160
+ """
161
+ start = text.find(f"\t\t{config_id} ")
162
+ if start < 0:
163
+ start = text.find(f"\t\t{config_id}\t")
164
+ if start < 0:
165
+ start = text.find(f"{config_id} = {{")
166
+ if start < 0:
167
+ raise SystemExit(
168
+ f"patch_project_signing: config {config_id} not found in pbxproj"
169
+ )
170
+ settings_key = "buildSettings = {"
171
+ s_idx = text.find(settings_key, start)
172
+ if s_idx < 0:
173
+ raise SystemExit(
174
+ f"patch_project_signing: buildSettings not found for {config_id}"
168
175
  )
176
+ # Find matching closing '};' for buildSettings — brace-balance forward.
177
+ depth = 0
178
+ i = s_idx + len(settings_key) - 1 # position at the opening '{'
179
+ end = -1
180
+ while i < len(text):
181
+ ch = text[i]
182
+ if ch == "{":
183
+ depth += 1
184
+ elif ch == "}":
185
+ depth -= 1
186
+ if depth == 0:
187
+ end = i
188
+ break
189
+ i += 1
190
+ if end < 0:
191
+ raise SystemExit(
192
+ f"patch_project_signing: unbalanced buildSettings for {config_id}"
193
+ )
194
+ inner = text[s_idx + len(settings_key): end]
195
+ patched = _patch_inner_settings(inner, team_id, profile_name)
196
+ return text[: s_idx + len(settings_key)] + patched + text[end:]
169
197
 
170
198
 
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
- )
199
+ _SIGNING_KEYS = {
200
+ "CODE_SIGN_STYLE": "Manual",
201
+ "CODE_SIGN_IDENTITY": '"Apple Distribution"',
202
+ }
203
+
204
+ _STRIP_KEYS = ("PROVISIONING_PROFILE",)
205
+
206
+
207
+ def _patch_inner_settings(
208
+ inner: str, team_id: str, profile_name: str
209
+ ) -> str:
210
+ """Rewrite build setting key/value lines inside one buildSettings block."""
211
+ # Always-set values
212
+ values = dict(_SIGNING_KEYS)
213
+ values["DEVELOPMENT_TEAM"] = team_id
214
+ values["PROVISIONING_PROFILE_SPECIFIER"] = f'"{profile_name}"'
215
+
216
+ lines = inner.splitlines(keepends=True)
217
+ emitted_keys: set[str] = set()
218
+ out: list[str] = []
219
+ # Each setting line looks like (whitespace-prefixed):
220
+ # KEY = VALUE;
221
+ # We match leading indent, the key, `= `, the rest.
222
+ pattern = re.compile(r"^(\s*)([A-Z_][A-Z0-9_]*)\s*=\s*(.+);\s*$")
223
+
224
+ for line in lines:
225
+ m = pattern.match(line)
226
+ if not m:
227
+ out.append(line)
228
+ continue
229
+ indent, key, _old_value = m.group(1), m.group(2), m.group(3)
230
+ if key in _STRIP_KEYS:
231
+ # Drop these keys entirely.
232
+ continue
233
+ if key in values:
234
+ out.append(f"{indent}{key} = {values[key]};\n")
235
+ emitted_keys.add(key)
236
+ continue
237
+ out.append(line)
238
+
239
+ # Any key we wanted to set but didn't find — append just before close.
240
+ missing = [k for k in values if k not in emitted_keys]
241
+ if missing:
242
+ # Determine indent from the first KEY= line we can find, else use
243
+ # two tabs (the pbxproj default).
244
+ indent = "\t\t\t\t"
245
+ for line in lines:
246
+ m = pattern.match(line)
247
+ if m:
248
+ indent = m.group(1)
249
+ break
250
+ tail = out[-1] if out else ""
251
+ # Ensure we inject before the trailing whitespace on the last line.
252
+ new_lines = [f"{indent}{k} = {values[k]};\n" for k in missing]
253
+ if tail.strip() == "":
254
+ out = out[:-1] + new_lines + [tail]
255
+ else:
256
+ out = out + new_lines
257
+ return "".join(out)
177
258
 
178
259
 
179
260
  # --------------------------------------------------------------------------- #