@daemux/store-automator 0.10.89 → 0.10.91
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/plugins/store-automator/agents/appstore-meta-creator.md +2 -5
- package/plugins/store-automator/agents/architect.md +16 -35
- package/plugins/store-automator/hooks/hooks.json +15 -0
- package/plugins/store-automator/hooks/sync-claude-md.sh +54 -0
- package/{templates → plugins/store-automator/templates}/CLAUDE.md.template +2 -119
- package/src/templates.mjs +7 -1
- package/templates/scripts/ci/ios-native/prepare_signing.py +39 -9
- package/templates/scripts/ci/ios-native/set_app_store_whats_new.py +47 -6
|
@@ -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.91"
|
|
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.91",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -264,11 +264,8 @@ af, am, ar, hy-AM, az-AZ, eu-ES, be, bn-BD, bg, my-MM, ca, zh-HK, zh-CN, zh-TW,
|
|
|
264
264
|
- South/Southeast Asian: hi, th, vi, id, ms, bn-BD, ta-IN, te-IN, ml-IN, mr-IN, kn-IN, gu, my-MM, km-KH, lo-LA, si-LK
|
|
265
265
|
- Middle Eastern: ar, ar-SA, he, tr, fa, ur
|
|
266
266
|
- Other: hu, el, et, lv, lt, ka-GE, hy-AM, az-AZ, kk, ky-KG, mn-MN, ne-NP, af, am, sw, zu, fil, is-IS, eu-ES, rm, pa
|
|
267
|
-
3.
|
|
268
|
-
|
|
269
|
-
- Teammate prompt includes: English source texts, target locale codes (Apple + Google variants), character limits per field, translation instructions
|
|
270
|
-
- Each teammate writes files directly to the correct locale directories
|
|
271
|
-
4. Wait for all teammates to complete, then verify all languages are covered
|
|
267
|
+
3. Translate each language group sequentially, writing files directly to the correct locale directories as you go. Use the English source texts, target locale codes (Apple + Google variants), and character limits per field for each group.
|
|
268
|
+
4. After all groups are translated, verify every configured language has complete metadata files for both platforms.
|
|
272
269
|
|
|
273
270
|
### Translation Instructions for Sub-Agents
|
|
274
271
|
|
|
@@ -138,48 +138,29 @@ If a `.tasks/` file path is provided, read ONLY that file for requirements.
|
|
|
138
138
|
Scan the codebase for already-implemented items. Pick 3-5 UNIMPLEMENTED
|
|
139
139
|
related requirements. Design only those. Report: "Batch: N of ~M remaining."
|
|
140
140
|
|
|
141
|
-
##
|
|
141
|
+
## Worktree Recommendation
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
For parallel development with overlapping file scopes, recommend git worktrees for isolation.
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
Include in your output when applicable:
|
|
146
146
|
|
|
147
147
|
```
|
|
148
|
-
###
|
|
149
|
-
|
|
150
|
-
**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
#### Reviewer Team
|
|
160
|
-
- **Teammates:** 2-3
|
|
161
|
-
- **Reviewer 1 focus:** Dart code quality, patterns, maintainability — files: {list}
|
|
162
|
-
- **Reviewer 2 focus:** Security, Firebase rules, data validation — files: {list}
|
|
163
|
-
- **Reviewer 3 focus:** Performance, store compliance — files: {list} (if 5+ files)
|
|
164
|
-
|
|
165
|
-
#### Tester Team
|
|
166
|
-
- **Teammates:** 2-4
|
|
167
|
-
- **Tester 1 focus:** Flutter unit and widget tests — {scope}
|
|
168
|
-
- **Tester 2 focus:** Mobile UI testing (iOS) — {scope}
|
|
169
|
-
- **Tester 3 focus:** Mobile UI testing (Android) — {scope} (if applicable)
|
|
170
|
-
|
|
171
|
-
#### App Designer Team (if design stage applies)
|
|
172
|
-
- **Teammates:** 2-3
|
|
173
|
-
- **Designer 1 scope:** App screen designs — {deliverables}
|
|
174
|
-
- **Designer 2 scope:** Store screenshots — {deliverables}
|
|
175
|
-
- **Designer 3 scope:** Web page design — {deliverables} (if applicable)
|
|
176
|
-
|
|
177
|
-
**TEAM EXCEPTION:** If task touches <3 files, output: "Files touched: {N} — below team threshold, single agents recommended."
|
|
148
|
+
### Worktree Strategy (if parallel development needed)
|
|
149
|
+
|
|
150
|
+
**Recommended:** YES | NO
|
|
151
|
+
|
|
152
|
+
**Rationale:** {why worktrees help or why they are unnecessary}
|
|
153
|
+
|
|
154
|
+
**Worktree layout (if YES):**
|
|
155
|
+
- `worktree/{name-1}` — {scope, files, branch purpose}
|
|
156
|
+
- `worktree/{name-2}` — {scope, files, branch purpose}
|
|
157
|
+
|
|
158
|
+
**Merge order:** {which worktree merges first, to minimize conflicts}
|
|
178
159
|
```
|
|
179
160
|
|
|
180
|
-
|
|
161
|
+
Skip this section when a single developer suffices or file scopes do not overlap.
|
|
181
162
|
|
|
182
163
|
## Output Footer
|
|
183
164
|
```
|
|
184
|
-
NEXT: product-manager(PRE) to validate approach
|
|
165
|
+
NEXT: product-manager(PRE) to validate approach before implementation
|
|
185
166
|
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
trap 'exit 0' ERR
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
TARGET="$HOME/.claude/CLAUDE.md"
|
|
6
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
|
|
7
|
+
[[ -n "$PLUGIN_ROOT" ]] || exit 0
|
|
8
|
+
TEMPLATE="$PLUGIN_ROOT/templates/CLAUDE.md.template"
|
|
9
|
+
PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json"
|
|
10
|
+
|
|
11
|
+
[[ -f "$TEMPLATE" && -f "$PLUGIN_JSON" ]] || exit 0
|
|
12
|
+
|
|
13
|
+
version=$(grep -o '"version": *"[^"]*"' "$PLUGIN_JSON" | head -1 | cut -d'"' -f4)
|
|
14
|
+
[[ -n "$version" ]] || exit 0
|
|
15
|
+
|
|
16
|
+
marker="<!-- daemux-store-automator@${version} -->"
|
|
17
|
+
|
|
18
|
+
if [[ -f "$TARGET" ]] && grep -Fq "$marker" "$TARGET"; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
mkdir -p "$(dirname "$TARGET")"
|
|
23
|
+
touch "$TARGET"
|
|
24
|
+
|
|
25
|
+
tmp=$(mktemp)
|
|
26
|
+
{
|
|
27
|
+
printf '<!-- BEGIN daemux-store-automator -->\n'
|
|
28
|
+
printf '%s\n' "$marker"
|
|
29
|
+
cat "$TEMPLATE"
|
|
30
|
+
printf '\n<!-- END daemux-store-automator -->\n'
|
|
31
|
+
} > "$tmp"
|
|
32
|
+
|
|
33
|
+
# Replace the existing sentinel block if present; otherwise append
|
|
34
|
+
if grep -q 'BEGIN daemux-store-automator' "$TARGET"; then
|
|
35
|
+
awk -v block_file="$tmp" '
|
|
36
|
+
BEGIN {
|
|
37
|
+
while ((getline line < block_file) > 0) {
|
|
38
|
+
block = block (block ? "\n" : "") line
|
|
39
|
+
}
|
|
40
|
+
close(block_file)
|
|
41
|
+
}
|
|
42
|
+
/<!-- BEGIN daemux-store-automator -->/ { in_block=1; print block; next }
|
|
43
|
+
/<!-- END daemux-store-automator -->/ { in_block=0; next }
|
|
44
|
+
!in_block { print }
|
|
45
|
+
' "$TARGET" > "$TARGET.new"
|
|
46
|
+
elif [[ -s "$TARGET" ]]; then
|
|
47
|
+
{ cat "$TARGET"; printf '\n'; cat "$tmp"; } > "$TARGET.new"
|
|
48
|
+
else
|
|
49
|
+
cp "$tmp" "$TARGET.new"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
mv "$TARGET.new" "$TARGET"
|
|
53
|
+
rm -f "$tmp"
|
|
54
|
+
exit 0
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- This block is managed by the daemux-store-automator plugin. Edits inside the sentinels will be overwritten on plugin update. Customize outside the sentinels. -->
|
|
2
|
+
|
|
1
3
|
# {APP_NAME} - Development Standards
|
|
2
4
|
|
|
3
5
|
## Project Overview
|
|
@@ -195,39 +197,6 @@ devops (standalone)
|
|
|
195
197
|
architect → product-manager(PRE) → app-designer → developer(flutter) → simplifier → reviewer → tester(flutter) → tester(mobile-ui) → appstore-meta-creator → appstore-reviewer → product-manager(POST) → devops(deploy)
|
|
196
198
|
```
|
|
197
199
|
|
|
198
|
-
#### Team Size Annotations
|
|
199
|
-
|
|
200
|
-
Flows above use single agents by default. For tasks touching 3+ files, apply team sizes:
|
|
201
|
-
|
|
202
|
-
| Files Touched | Team Size |
|
|
203
|
-
|--------------|-----------|
|
|
204
|
-
| 1-2 files | Single agent (no team) |
|
|
205
|
-
| 3-4 files | 2 teammates |
|
|
206
|
-
| 5-7 files | 3 teammates |
|
|
207
|
-
| 8+ files | 4 teammates |
|
|
208
|
-
|
|
209
|
-
**Team-annotated flows** (apply when task scope exceeds 2 files):
|
|
210
|
-
|
|
211
|
-
##### Standard Flow (with teams)
|
|
212
|
-
```
|
|
213
|
-
architect{T:2} → product-manager(PRE) → developer{T:2-4} → simplifier → reviewer{T:2} → tester{T:2} → product-manager(POST) → [devops]
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
##### Flutter Flow (with teams)
|
|
217
|
-
```
|
|
218
|
-
architect{T:2} → product-manager(PRE) → [app-designer{T:2-3}] → developer(flutter){T:2-4} → simplifier → reviewer{T:2-3} → tester(flutter){T:2} → tester(mobile-ui){T:2} → product-manager(POST) → [devops(deploy)]
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
##### Backend Flow (with teams)
|
|
222
|
-
```
|
|
223
|
-
architect{T:2} → product-manager(PRE) → developer(backend){T:2-4} → simplifier → reviewer{T:2} → tester(backend){T:2} → product-manager(POST) → [devops(deploy)]
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
##### Full Publishing Flow (with teams)
|
|
227
|
-
```
|
|
228
|
-
architect{T:2} → product-manager(PRE) → app-designer{T:2-3} → developer(flutter){T:2-4} → simplifier → reviewer{T:2-3} → tester(flutter){T:2} → tester(mobile-ui){T:2} → appstore-meta-creator{T:2-5} → appstore-reviewer → product-manager(POST) → devops(deploy)
|
|
229
|
-
```
|
|
230
|
-
|
|
231
200
|
### Agents Reference
|
|
232
201
|
|
|
233
202
|
| Agent | When to use |
|
|
@@ -323,90 +292,6 @@ iOS: create app record locally first (`fastlane create_app_ios` + `upload_privac
|
|
|
323
292
|
### Phase 7: Ongoing Updates
|
|
324
293
|
Push to main. GitHub Actions detects changes and uploads modified assets. Version auto-incremented.
|
|
325
294
|
|
|
326
|
-
---
|
|
327
|
-
## FOR ORCHESTRATORS ONLY
|
|
328
|
-
**If you are a TEAMMATE, skip to "For Teammates" section below.**
|
|
329
|
-
---
|
|
330
|
-
|
|
331
|
-
### Agent Teams (MANDATORY by default)
|
|
332
|
-
|
|
333
|
-
**Teams are the DEFAULT for most workflow stages.** Single-agent is the exception, not the norm.
|
|
334
|
-
|
|
335
|
-
#### Mandatory Team Stages
|
|
336
|
-
|
|
337
|
-
These stages MUST use teams unless the task touches fewer than 3 files:
|
|
338
|
-
|
|
339
|
-
| Stage | Default Size | Split Strategy |
|
|
340
|
-
|-------|-------------|----------------|
|
|
341
|
-
| architect | 2 | Split by concern: structure/patterns vs integration/data-flow |
|
|
342
|
-
| developer | 2-4 | Split by file group: screens/widgets vs providers/repos vs Firebase/backend |
|
|
343
|
-
| reviewer | 2-3 | Split by focus: Dart quality vs security+Firebase rules vs performance+store compliance |
|
|
344
|
-
| tester | 2-4 | Split by type: flutter unit/widget vs mobile-ui iOS vs mobile-ui Android vs web |
|
|
345
|
-
| app-designer | 2-3 | Split by deliverable: app screens vs store screenshots vs web page |
|
|
346
|
-
| appstore-meta-creator | 2-5 | Split by language group: each teammate handles 2-5 languages grouped by similarity |
|
|
347
|
-
|
|
348
|
-
#### Single-Agent Stages (exceptions)
|
|
349
|
-
|
|
350
|
-
These stages stay single-agent — teams add no value:
|
|
351
|
-
|
|
352
|
-
| Stage | Reason |
|
|
353
|
-
|-------|--------|
|
|
354
|
-
| simplifier | Works on same files developer just touched — no parallelism possible |
|
|
355
|
-
| product-manager | Single evaluation checkpoint — multiple perspectives don't help |
|
|
356
|
-
| devops | Single deployment target — cannot parallelize |
|
|
357
|
-
| appstore-reviewer | Single compliance evaluation — one holistic review needed |
|
|
358
|
-
|
|
359
|
-
#### Team Size Triggers
|
|
360
|
-
|
|
361
|
-
| Files Touched | Team Size |
|
|
362
|
-
|--------------|-----------|
|
|
363
|
-
| 1-2 files | Single agent — TEAM EXCEPTION |
|
|
364
|
-
| 3-4 files | 2 teammates |
|
|
365
|
-
| 5-7 files | 3 teammates |
|
|
366
|
-
| 8+ files | 4 teammates |
|
|
367
|
-
|
|
368
|
-
**Team Enforcement (MANDATORY):**
|
|
369
|
-
If `TEAMS:` output includes any stage using teams, you MUST call `TeamCreate` before spawning agents for that stage. Spawning agents via `Task` without `team_name` does NOT count as using teams. Violation = workflow non-compliance.
|
|
370
|
-
|
|
371
|
-
**When creating a team:**
|
|
372
|
-
|
|
373
|
-
```
|
|
374
|
-
Create agent team with [N] teammates, all using Opus model:
|
|
375
|
-
- [role-1]: [Specific responsibility and task details]
|
|
376
|
-
- [role-2]: [Specific responsibility and task details]
|
|
377
|
-
- [role-3]: [Specific responsibility and task details]
|
|
378
|
-
...
|
|
379
|
-
|
|
380
|
-
Spawn each teammate with detailed prompt including:
|
|
381
|
-
- Their specific role and responsibilities
|
|
382
|
-
- The task details
|
|
383
|
-
- How to coordinate with other teammates
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
**Guidelines:**
|
|
387
|
-
- Use 2-10 teammates per stage
|
|
388
|
-
- Always specify Opus model for all teammates
|
|
389
|
-
- Assign distinct, non-overlapping scopes to each teammate
|
|
390
|
-
- Include full instructions in spawn prompts (don't reference external files)
|
|
391
|
-
- One team at a time - dissolve before next stage
|
|
392
|
-
- Assign 5-6 tasks per teammate
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
## FOR TEAMMATES ONLY
|
|
396
|
-
**If you are the ORCHESTRATOR, skip this section.**
|
|
397
|
-
---
|
|
398
|
-
|
|
399
|
-
### Teammate Instructions
|
|
400
|
-
|
|
401
|
-
**Ignore orchestration workflows above.**
|
|
402
|
-
|
|
403
|
-
1. Your spawn prompt contains your full instructions
|
|
404
|
-
2. Message teammates directly to coordinate
|
|
405
|
-
3. Focus only on your assigned scope
|
|
406
|
-
4. Complete your work, let team dissolve
|
|
407
|
-
|
|
408
|
-
---
|
|
409
|
-
|
|
410
295
|
### Large Task Protocol
|
|
411
296
|
|
|
412
297
|
For tasks with 5+ requirements:
|
|
@@ -433,8 +318,6 @@ TASK TYPE: [flutter/backend/design/metadata/database/infra/standard]
|
|
|
433
318
|
RECOMMENDED FLOW:
|
|
434
319
|
<copy the EXACT flow from Agent Flows section - include ALL agents with [optional] ones>
|
|
435
320
|
|
|
436
|
-
TEAMS: [MANDATORY — list team stages with sizes. Single-agent exceptions: list which stages and why. "NO" only valid for tasks <3 files]
|
|
437
|
-
|
|
438
321
|
DEPLOYMENT: [AVAILABLE - deployment configured | NOT CONFIGURED - workflow ends at product-manager(POST)]
|
|
439
322
|
|
|
440
323
|
TASK TRACKING: ALWAYS use TaskCreate/TaskUpdate/TaskList tools for multi-step tasks (3+ steps)
|
package/src/templates.mjs
CHANGED
|
@@ -93,7 +93,13 @@ function composeNewFileContent(existing, wrappedBlock) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function installClaudeMd(targetPath, packageDir, appName) {
|
|
96
|
-
const template = join(
|
|
96
|
+
const template = join(
|
|
97
|
+
packageDir,
|
|
98
|
+
'plugins',
|
|
99
|
+
'store-automator',
|
|
100
|
+
'templates',
|
|
101
|
+
'CLAUDE.md.template',
|
|
102
|
+
);
|
|
97
103
|
if (!existsSync(template)) return;
|
|
98
104
|
const targetExists = existsSync(targetPath);
|
|
99
105
|
const action = targetExists ? 'Updating' : 'Installing';
|
|
@@ -83,7 +83,31 @@ def generate_key_and_csr() -> tuple[rsa.RSAPrivateKey, bytes]:
|
|
|
83
83
|
return private_key, csr_payload
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def
|
|
86
|
+
def oldest_distribution_cert_id(token: str) -> str | None:
|
|
87
|
+
"""Return the DISTRIBUTION cert with the EARLIEST expiration date.
|
|
88
|
+
|
|
89
|
+
Apple's per-team distribution-cert cap is 2. When CI hits the cap we
|
|
90
|
+
must revoke one to make room for a fresh cert. We deliberately pick
|
|
91
|
+
the OLDEST cert (earliest expirationDate) because:
|
|
92
|
+
|
|
93
|
+
* Each CI run signs an IPA, uploads it to ASC, and that build then
|
|
94
|
+
spends minutes-to-hours in App Store Connect's processing
|
|
95
|
+
pipeline. Processing re-validates the binary's leaf cert against
|
|
96
|
+
Apple's current cert state. A revoked cert during processing
|
|
97
|
+
produces ITMS-90035 ("invalid signature ... signed with an ad-hoc
|
|
98
|
+
certificate, not a distribution certificate") and rejects the
|
|
99
|
+
build that was already accepted at upload time.
|
|
100
|
+
|
|
101
|
+
* The NEWEST cert is, by construction, the one that signed the most
|
|
102
|
+
recent build — i.e. the build that is right now in ASC processing
|
|
103
|
+
and most exposed to the revocation race. Revoking it (the prior
|
|
104
|
+
behavior) reliably broke the build the previous CI run produced.
|
|
105
|
+
|
|
106
|
+
* The OLDEST cert is the one most likely to be from a build whose
|
|
107
|
+
ASC processing has long since completed (each CI run only adds
|
|
108
|
+
builds; processing finishes in minutes). Revoking it is the
|
|
109
|
+
safest choice.
|
|
110
|
+
"""
|
|
87
111
|
data = get_json(
|
|
88
112
|
"/certificates",
|
|
89
113
|
token,
|
|
@@ -93,15 +117,17 @@ def newest_distribution_cert_id(token: str) -> str | None:
|
|
|
93
117
|
"filter[certificateType]": "DISTRIBUTION",
|
|
94
118
|
},
|
|
95
119
|
)
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
oldest_id = None
|
|
121
|
+
oldest_exp: str | None = None
|
|
98
122
|
for cert in data.get("data", []):
|
|
99
123
|
attrs = cert.get("attributes") or {}
|
|
100
124
|
exp = attrs.get("expirationDate") or ""
|
|
101
|
-
if exp
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
if not exp:
|
|
126
|
+
continue
|
|
127
|
+
if oldest_exp is None or exp < oldest_exp:
|
|
128
|
+
oldest_exp = exp
|
|
129
|
+
oldest_id = cert["id"]
|
|
130
|
+
return oldest_id
|
|
105
131
|
|
|
106
132
|
|
|
107
133
|
def create_distribution_cert(token: str, csr_b64: str) -> tuple[str, bytes]:
|
|
@@ -118,8 +144,12 @@ def create_distribution_cert(token: str, csr_b64: str) -> tuple[str, bytes]:
|
|
|
118
144
|
"POST", "/certificates", token, json_body=body, allow_status={409}
|
|
119
145
|
)
|
|
120
146
|
if resp.status_code == 409:
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
# Revoke the OLDEST cert, NOT the newest. The newest cert signed
|
|
148
|
+
# the previous build that may still be in ASC processing — revoking
|
|
149
|
+
# it during processing yields ITMS-90035 on the prior build.
|
|
150
|
+
# See oldest_distribution_cert_id() for the full rationale.
|
|
151
|
+
print("Distribution cert cap hit; revoking oldest existing cert")
|
|
152
|
+
target = oldest_distribution_cert_id(token)
|
|
123
153
|
if not target:
|
|
124
154
|
raise SystemExit(
|
|
125
155
|
"409 from cert create but no existing DISTRIBUTION cert "
|
|
@@ -78,8 +78,31 @@ def _log(msg: str) -> None:
|
|
|
78
78
|
print(msg, file=sys.stderr)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
def _patch_localization(
|
|
82
|
-
|
|
81
|
+
def _patch_localization(
|
|
82
|
+
token: str, localization_id: str, whats_new: str
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""PATCH a single localization's whatsNew. Returns True on success,
|
|
85
|
+
False when Apple reports the localization is locked (409 STATE_ERROR).
|
|
86
|
+
|
|
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.
|
|
104
|
+
"""
|
|
105
|
+
resp = request(
|
|
83
106
|
"PATCH",
|
|
84
107
|
f"/appStoreVersionLocalizations/{localization_id}",
|
|
85
108
|
token,
|
|
@@ -90,7 +113,17 @@ def _patch_localization(token: str, localization_id: str, whats_new: str) -> Non
|
|
|
90
113
|
"attributes": {"whatsNew": whats_new},
|
|
91
114
|
}
|
|
92
115
|
},
|
|
116
|
+
allow_status={409},
|
|
93
117
|
)
|
|
118
|
+
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
|
+
)
|
|
125
|
+
return False
|
|
126
|
+
return True
|
|
94
127
|
|
|
95
128
|
|
|
96
129
|
def _create_localization(
|
|
@@ -155,18 +188,26 @@ def _update_all_localizations(
|
|
|
155
188
|
return 1
|
|
156
189
|
|
|
157
190
|
count = 0
|
|
191
|
+
skipped = 0
|
|
158
192
|
for item in entries:
|
|
159
193
|
loc_id = item.get("id") or ""
|
|
160
194
|
loc = (item.get("attributes") or {}).get("locale") or "?"
|
|
161
195
|
if not loc_id:
|
|
162
196
|
_warn(f"skipping localization without id: {item!r}")
|
|
163
197
|
continue
|
|
164
|
-
_patch_localization(token, loc_id, whats_new)
|
|
198
|
+
if _patch_localization(token, loc_id, whats_new):
|
|
199
|
+
_log(
|
|
200
|
+
f"PATCHed appStoreVersionLocalization {loc_id} whatsNew "
|
|
201
|
+
f"for {version} ({loc})"
|
|
202
|
+
)
|
|
203
|
+
count += 1
|
|
204
|
+
else:
|
|
205
|
+
skipped += 1
|
|
206
|
+
if skipped:
|
|
165
207
|
_log(
|
|
166
|
-
f"
|
|
167
|
-
f"
|
|
208
|
+
f"whatsNew skipped for {skipped} locked localization(s) "
|
|
209
|
+
f"(see warnings above)"
|
|
168
210
|
)
|
|
169
|
-
count += 1
|
|
170
211
|
return count
|
|
171
212
|
|
|
172
213
|
|