@daemux/store-automator 0.10.88 → 0.10.89

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.88"
8
+ "version": "0.10.89"
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.88",
15
+ "version": "0.10.89",
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.88",
3
+ "version": "0.10.89",
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.88",
3
+ "version": "0.10.89",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
@@ -69,12 +69,32 @@ def _clear_caches() -> None:
69
69
  _settings_cache.clear()
70
70
 
71
71
 
72
+ def _find_xcodegen_spec(workspace: Path) -> Path | None:
73
+ """Locate an xcodegen spec file (project.yml / Project.yml / project.yaml).
74
+
75
+ xcodegen accepts any of these names. We check them in this order. Case
76
+ mismatch on case-sensitive filesystems has bitten us before.
77
+ """
78
+ for candidate in ("project.yml", "project.yaml", "Project.yml", "Project.yaml"):
79
+ path = workspace / candidate
80
+ if path.is_file():
81
+ return path
82
+ return None
83
+
84
+
72
85
  def auto_detect_project(workspace: Path) -> tuple[str, str]:
73
86
  """Discover the repo-root Xcode project / workspace.
74
87
 
75
88
  Returns ``(project, workspace_file)`` where exactly one is a non-empty
76
89
  filename. ``("", "")`` means nothing was found.
90
+
91
+ If no ``.xcworkspace``/``.xcodeproj`` is present but a ``project.yml``
92
+ exists (xcodegen spec), we invoke ``xcodegen generate`` once to
93
+ materialize the ``.xcodeproj`` and retry. xcodegen-based projects
94
+ commonly gitignore the generated ``.xcodeproj``, so the runner sees
95
+ only the spec.
77
96
  """
97
+ log(f"auto-detect: scanning workspace={workspace}")
78
98
  workspaces = sorted(workspace.glob("*.xcworkspace"))
79
99
  if workspaces:
80
100
  picked = _pick_by_basename(workspaces, workspace, "xcworkspace")
@@ -82,6 +102,21 @@ def auto_detect_project(workspace: Path) -> tuple[str, str]:
82
102
  return "", picked.name
83
103
 
84
104
  projects = sorted(workspace.glob("*.xcodeproj"))
105
+ if not projects:
106
+ spec = _find_xcodegen_spec(workspace)
107
+ if spec is not None:
108
+ log(f"auto-detect: xcodegen spec found at {spec.name}")
109
+ if _run_xcodegen(workspace):
110
+ projects = sorted(workspace.glob("*.xcodeproj"))
111
+ else:
112
+ # Surface what we DID see so CI logs are actionable when the
113
+ # expected spec file is missing / misnamed / gitignored.
114
+ entries = sorted(p.name for p in workspace.iterdir() if not p.name.startswith("."))
115
+ log(
116
+ f"auto-detect: no .xcodeproj / .xcworkspace / project.yml at "
117
+ f"{workspace}; root entries={entries[:20]}"
118
+ )
119
+
85
120
  if not projects:
86
121
  return "", ""
87
122
  picked = _pick_by_basename(projects, workspace, "xcodeproj")
@@ -89,6 +124,88 @@ def auto_detect_project(workspace: Path) -> tuple[str, str]:
89
124
  return picked.name, ""
90
125
 
91
126
 
127
+ def _run_xcodegen(workspace: Path) -> bool:
128
+ """Run ``xcodegen generate`` in ``workspace``. Returns True on success.
129
+
130
+ Logs a notice on failure but never raises — the caller will see an
131
+ empty project/workspace result and the normal ``::error::`` path
132
+ will guide the user.
133
+ """
134
+ log("auto-detect: no .xcodeproj/.xcworkspace found, xcodegen spec present — running xcodegen")
135
+ try:
136
+ result = subprocess.run(
137
+ ["xcodegen", "generate"],
138
+ cwd=str(workspace),
139
+ capture_output=True,
140
+ text=True,
141
+ timeout=LIST_TIMEOUT,
142
+ )
143
+ except FileNotFoundError:
144
+ notice(
145
+ "xcodegen not on PATH — installing via Homebrew (one-shot). "
146
+ "If this fails, pre-install xcodegen or commit the generated .xcodeproj."
147
+ )
148
+ if not _install_xcodegen():
149
+ return False
150
+ try:
151
+ result = subprocess.run(
152
+ ["xcodegen", "generate"],
153
+ cwd=str(workspace),
154
+ capture_output=True,
155
+ text=True,
156
+ timeout=LIST_TIMEOUT,
157
+ )
158
+ except (OSError, subprocess.TimeoutExpired) as exc:
159
+ notice(f"xcodegen still unavailable after install: {exc!r}")
160
+ return False
161
+ except (OSError, subprocess.TimeoutExpired) as exc:
162
+ notice(
163
+ f"xcodegen not available or timed out ({exc!r}); "
164
+ "install xcodegen or commit the generated .xcodeproj."
165
+ )
166
+ return False
167
+
168
+ if result.returncode != 0:
169
+ notice(
170
+ f"xcodegen generate failed (rc={result.returncode}): "
171
+ f"{result.stderr.strip() or result.stdout.strip()}"
172
+ )
173
+ return False
174
+
175
+ log("auto-detect: xcodegen generate succeeded")
176
+ return True
177
+
178
+
179
+ def _install_xcodegen() -> bool:
180
+ """Install xcodegen via Homebrew. Returns True on success.
181
+
182
+ Called only when `xcodegen` is missing from PATH. macOS GitHub runners
183
+ normally preinstall it, but when they don't we need a self-bootstrap
184
+ path — otherwise the entire action fails at auto-detect for
185
+ xcodegen-based projects with no actionable error.
186
+ """
187
+ try:
188
+ result = subprocess.run(
189
+ ["brew", "install", "xcodegen"],
190
+ capture_output=True,
191
+ text=True,
192
+ timeout=300,
193
+ )
194
+ except (OSError, subprocess.TimeoutExpired) as exc:
195
+ notice(f"brew install xcodegen failed: {exc!r}")
196
+ return False
197
+
198
+ if result.returncode != 0:
199
+ notice(
200
+ f"brew install xcodegen failed (rc={result.returncode}): "
201
+ f"{result.stderr.strip() or result.stdout.strip()}"
202
+ )
203
+ return False
204
+
205
+ log("auto-detect: xcodegen installed via Homebrew")
206
+ return True
207
+
208
+
92
209
  def _pick_by_basename(matches: list[Path], workspace: Path, label: str) -> Path:
93
210
  """Pick the match whose name stem equals the repo basename, else first."""
94
211
  if len(matches) == 1:
@@ -70,7 +70,18 @@ def _result(decision: str, version: str, vid: str) -> dict:
70
70
  def env(name: str) -> str:
71
71
  val = os.environ.get(name)
72
72
  if not val:
73
- print(f"::error::Missing required env var: {name}", file=sys.stderr)
73
+ if name == "APP_STORE_APPLE_ID":
74
+ print(
75
+ "::error::APP_STORE_APPLE_ID is empty. This usually means "
76
+ "auto-detect could not resolve PRODUCT_BUNDLE_IDENTIFIER from "
77
+ "your Xcode project (check the auto-detect: ... log lines in the "
78
+ "'Resolve credentials + auto-detect' step above). Fix options: "
79
+ "(1) commit your .xcodeproj or add an xcodegen project.yml; "
80
+ "(2) pass `app-store-apple-id:` explicitly via the action's `with:` block.",
81
+ file=sys.stderr,
82
+ )
83
+ else:
84
+ print(f"::error::Missing required env var: {name}", file=sys.stderr)
74
85
  raise SystemExit(1)
75
86
  return val
76
87
 
@@ -22,6 +22,15 @@ from asc_common import get_json, make_jwt
22
22
  def env(name: str) -> str:
23
23
  val = os.environ.get(name)
24
24
  if not val:
25
+ if name == "APP_STORE_APPLE_ID":
26
+ raise SystemExit(
27
+ "::error::APP_STORE_APPLE_ID is empty. This usually means "
28
+ "auto-detect could not resolve PRODUCT_BUNDLE_IDENTIFIER from "
29
+ "your Xcode project (check the auto-detect: ... log lines in the "
30
+ "'Resolve credentials + auto-detect' step above). Fix options: "
31
+ "(1) commit your .xcodeproj or add an xcodegen project.yml; "
32
+ "(2) pass `app-store-apple-id:` explicitly via the action's `with:` block."
33
+ )
25
34
  raise SystemExit(f"missing env var: {name}")
26
35
  return val
27
36