@daemux/store-automator 0.10.94 → 0.10.95

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.94"
8
+ "version": "0.10.95"
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.94",
15
+ "version": "0.10.95",
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.94",
3
+ "version": "0.10.95",
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.94",
3
+ "version": "0.10.95",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
@@ -69,5 +69,51 @@ git config user.email >/dev/null 2>&1 || \
69
69
  # modifying its own triggers). Consumer-side deploy.yml updates require
70
70
  # manual `npx --yes @daemux/swift-app-ci` by the user. Document this in
71
71
  # the action README and changelog when shipping a deploy.yml schema change.
72
- git add -A .github/actions/swift-app/ 2>/dev/null || true
73
- echo "autoupdate: staged refreshed action files (deploy.yml not staged see comment)"
72
+
73
+ # Stage everything under the vendored action dir EXCEPT Python bytecode
74
+ # caches. The action's own scripts run during this CI job and Python
75
+ # generates `__pycache__/*.pyc` files inside scripts/ as a side effect of
76
+ # importing them. Those caches are build artifacts of *this* run, not part
77
+ # of the action distribution -- sweeping them into the autoupdate bot
78
+ # commit is benign but noisy (every clean run emits a "refresh
79
+ # __pycache__" diff). `git ls-files -mo` enumerates modified+other
80
+ # (untracked) paths under the directory; we filter out anything matching
81
+ # `/__pycache__/` or ending in `.pyc`, then feed the rest to `git add -f`.
82
+ #
83
+ # We deliberately do NOT pass `--exclude-standard`: that flag would make
84
+ # ls-files honor the consumer's .gitignore at discovery time. If a
85
+ # consumer ignores e.g. `.daemux-version` or any other autoupdate-managed
86
+ # path, ls-files would silently drop it and the bot commit would never
87
+ # refresh that file -- defeating the autoupdate. The matching `-f` on
88
+ # `git add` already overrides .gitignore at staging time, so the only
89
+ # files we want to filter are the bytecode caches above, which our
90
+ # explicit grep handles deterministically.
91
+ #
92
+ # Empty-input guard: BSD xargs (default on macos-15 runners) does NOT
93
+ # support `-r`/`--no-run-if-empty`, so on a clean tree where the grep
94
+ # filter strips everything, piping an empty stream into `xargs git add`
95
+ # would invoke `git add -f --` with no positional arguments. That exits
96
+ # 129 ("Nothing specified, nothing added"), which `set -euo pipefail`
97
+ # treats as fatal even though it's the no-op case we want. Capture to a
98
+ # variable first and skip the xargs call when empty -- portable across
99
+ # both BSD and GNU xargs.
100
+ #
101
+ # We deliberately do NOT pass `--directory`: with that flag, `git ls-files`
102
+ # collapses any *new* untracked directory to a single entry (the directory
103
+ # path itself), e.g. it would emit `.github/actions/swift-app/scripts/`
104
+ # instead of enumerating its files. If such a collapsed parent contains
105
+ # `__pycache__/` children, our grep would NOT match `__pycache__/` against
106
+ # the parent path -- the directory entry would slip past the filter and
107
+ # `git add -f <dir>` would recurse into it, dragging the bytecode caches
108
+ # in with everything else. Without `--directory`, ls-files enumerates
109
+ # every individual untracked file path so the grep filter sees each
110
+ # `__pycache__/...` and `*.pyc` entry by name and strips it
111
+ # deterministically.
112
+ files="$(git ls-files -mo .github/actions/swift-app/ \
113
+ | grep -v -E '(/__pycache__/|\.pyc$)' || true)"
114
+ if [[ -n "$files" ]]; then
115
+ printf '%s\n' "$files" \
116
+ | tr '\n' '\0' \
117
+ | xargs -0 git add -f -- 2>/dev/null || true
118
+ fi
119
+ echo "autoupdate: staged refreshed action files (deploy.yml not staged — see comment; __pycache__ excluded)"
@@ -78,29 +78,24 @@ def _log(msg: str) -> None:
78
78
  print(msg, file=sys.stderr)
79
79
 
80
80
 
81
+ # Apple's 409 STATE_ERROR detail string for the known-benign case where
82
+ # the per-localization slot is locked because the parent version is
83
+ # transitioning between states. Substring match (not regex / equality) so
84
+ # we don't break if Apple appends or reformats surrounding context.
85
+ _WHATSNEW_LOCKED_DETAIL = "cannot be edited at this time"
86
+
87
+
81
88
  def _patch_localization(
82
89
  token: str, localization_id: str, whats_new: str
83
90
  ) -> bool:
84
91
  """PATCH a single localization's whatsNew. Returns True on success,
85
92
  False when Apple reports the localization is locked (409 STATE_ERROR).
86
93
 
87
- Apple returns 409 STATE_ERROR ("Attribute 'whatsNew' cannot be edited
88
- at this time") on individual localizations even when the parent
89
- appStoreVersion's appStoreState is in the editable allow-list checked
90
- upstream. This happens when the per-localization state is locked
91
- independently (e.g. submitted, in-review at the localization level,
92
- or transitioning) -- a race the version-level state check at the top
93
- of main() cannot see.
94
-
95
- Without this allow-list, asc_common.request() raises SystemExit on
96
- the 409, which the script's top-level try/except cannot swallow
97
- (SystemExit is explicitly re-raised). The whole CI run then fails
98
- at exit code 1 even though the IPA upload already succeeded.
99
-
100
- Other non-2xx statuses (auth, 5xx, malformed payloads, real ASC
101
- outages) are NOT in the allow-list and continue to fail loud --
102
- asc_common.request() retries 5xx automatically and SystemExits on
103
- everything else.
94
+ See _handle_localization_409 for the rationale behind allow_status={409}
95
+ and the benign-lock vs unexpected-409 routing. Other non-2xx statuses
96
+ (auth, 5xx, malformed payloads, real ASC outages) are NOT in the
97
+ allow-list and continue to fail loud -- asc_common.request() retries
98
+ 5xx automatically and SystemExits on everything else.
104
99
  """
105
100
  resp = request(
106
101
  "PATCH",
@@ -116,16 +111,75 @@ def _patch_localization(
116
111
  allow_status={409},
117
112
  )
118
113
  if resp.status_code == 409:
119
- _warn(
120
- f"appStoreVersionLocalization {localization_id} returned 409 "
121
- f"STATE_ERROR (locked); whatsNew not patched for this "
122
- f"localization. Other localizations and the rest of the run "
123
- f"continue."
124
- )
114
+ _handle_localization_409(localization_id, resp)
125
115
  return False
126
116
  return True
127
117
 
128
118
 
119
+ def _handle_localization_409(localization_id: str, resp) -> None:
120
+ """Route an ASC 409 STATE_ERROR on a localization PATCH to the right
121
+ log channel based on the response detail.
122
+
123
+ Apple returns 409 STATE_ERROR ("Attribute 'whatsNew' cannot be edited
124
+ at this time") on individual localizations even when the parent
125
+ appStoreVersion's appStoreState is in the editable allow-list checked
126
+ upstream. This happens when the per-localization state is locked
127
+ independently (e.g. submitted, in-review at the localization level,
128
+ or transitioning) -- a race the version-level state check at the top
129
+ of main() cannot see.
130
+
131
+ Without an allow_status={409} entry in the request, asc_common.request()
132
+ raises SystemExit on the 409, which the script's top-level try/except
133
+ cannot swallow (SystemExit is explicitly re-raised). The whole CI run
134
+ then fails at exit code 1 even though the IPA upload already succeeded.
135
+
136
+ The benign-lock detail is logged plain -- it's the expected,
137
+ non-actionable case and emitting `::warning::` annotations every clean
138
+ run produces noise in the GH workflow summary. Any OTHER 409 detail
139
+ (genuinely unexpected) is still surfaced as `::warning::`.
140
+ """
141
+ detail = _extract_409_detail(resp)
142
+ if _WHATSNEW_LOCKED_DETAIL in detail:
143
+ _log(
144
+ f"[whatsNew] localization {localization_id} is currently "
145
+ f"locked, skipping (detail: {detail!r})"
146
+ )
147
+ return
148
+ _warn(
149
+ f"appStoreVersionLocalization {localization_id} returned 409 "
150
+ f"with unexpected detail {detail!r}; whatsNew not patched for "
151
+ f"this localization. Other localizations and the rest of the "
152
+ f"run continue."
153
+ )
154
+
155
+
156
+ def _extract_409_detail(resp) -> str:
157
+ """Pull the human-readable detail string out of an ASC 409 response.
158
+
159
+ ASC error envelope: {"errors": [{"status": "409", "code": "STATE_ERROR",
160
+ "title": "...", "detail": "Attribute 'whatsNew' cannot be edited at this
161
+ time"}]}. We collapse all error details into a single string so the
162
+ benign-lock substring match survives Apple returning multiple errors
163
+ in one response. Falls back to raw response text if JSON parsing fails
164
+ (defensive -- Apple's error envelopes have been stable for years but
165
+ the network layer occasionally returns HTML on infrastructure faults).
166
+ """
167
+ try:
168
+ body = resp.json()
169
+ except ValueError:
170
+ return resp.text or ""
171
+ errors = body.get("errors") if isinstance(body, dict) else None
172
+ if not errors:
173
+ return ""
174
+ parts = []
175
+ for err in errors:
176
+ if isinstance(err, dict):
177
+ detail = err.get("detail") or err.get("title") or ""
178
+ if detail:
179
+ parts.append(detail)
180
+ return " | ".join(parts)
181
+
182
+
129
183
  def _create_localization(
130
184
  token: str, version_id: str, locale: str, whats_new: str
131
185
  ) -> str:
@@ -204,10 +258,13 @@ def _update_all_localizations(
204
258
  else:
205
259
  skipped += 1
206
260
  if skipped:
207
- _log(
208
- f"whatsNew skipped for {skipped} locked localization(s) "
209
- f"(see warnings above)"
210
- )
261
+ # Per-localization detail (lock state, unexpected 409, etc.) was
262
+ # already emitted above by _patch_localization -- this is just the
263
+ # rollup count so the workflow log shows a single summary line.
264
+ # We deliberately avoid "see warnings above": for the benign-lock
265
+ # case _patch_localization emits plain _log entries (no ::warning::),
266
+ # so a "warnings above" pointer would mislead the reader.
267
+ _log(f"whatsNew skipped for {skipped} locked localization(s)")
211
268
  return count
212
269
 
213
270