@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.
|
|
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.
|
|
15
|
+
"version": "0.10.95",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|