@daemux/store-automator 0.10.37 → 0.10.39
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 +8 -3
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +9 -2
- package/plugins/store-automator/agents/appstore-meta-creator.md +3 -3
- package/templates/CLAUDE.md.template +5 -0
- package/templates/ci.config.yaml.template +1 -0
- package/templates/github/workflows/ios-release.yml +3 -0
- package/templates/scripts/asc_app_setup.py +277 -0
- package/templates/scripts/ci/common/read-config.sh +3 -0
- package/templates/scripts/ci/ios/setup-app-info.sh +43 -0
- package/templates/scripts/__pycache__/asc_iap_api.cpython-311.pyc +0 -0
- package/templates/scripts/__pycache__/asc_subscription_setup.cpython-311.pyc +0 -0
- package/templates/scripts/__pycache__/gplay_iap_api.cpython-311.pyc +0 -0
- package/templates/scripts/__pycache__/sync_iap_android.cpython-311.pyc +0 -0
- package/templates/scripts/__pycache__/sync_iap_ios.cpython-311.pyc +0 -0
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.39"
|
|
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.
|
|
16
|
-
"keywords": [
|
|
15
|
+
"version": "0.10.39",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"flutter",
|
|
18
|
+
"app-store",
|
|
19
|
+
"google-play",
|
|
20
|
+
"fastlane"
|
|
21
|
+
]
|
|
17
22
|
}
|
|
18
23
|
]
|
|
19
24
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "store-automator",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.39",
|
|
4
4
|
"description": "App Store & Google Play automation agents for Flutter app publishing",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Daemux"
|
|
7
7
|
},
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"flutter",
|
|
10
|
+
"app-store",
|
|
11
|
+
"google-play",
|
|
12
|
+
"fastlane",
|
|
13
|
+
"screenshots",
|
|
14
|
+
"metadata"
|
|
15
|
+
]
|
|
9
16
|
}
|
|
@@ -43,11 +43,11 @@ You are a senior ASO (App Store Optimization) specialist and localization expert
|
|
|
43
43
|
| full_description.txt | 4000 chars | Keyword-rich naturally, first 250 chars most important, no keyword stuffing |
|
|
44
44
|
| changelogs/default.txt | 500 chars | What is new in this version |
|
|
45
45
|
|
|
46
|
-
### Shared (fastlane/metadata/
|
|
46
|
+
### Shared (fastlane/metadata/)
|
|
47
47
|
|
|
48
48
|
| File | Content |
|
|
49
49
|
|------|---------|
|
|
50
|
-
| copyright.txt | "Copyright {YEAR} {COMPANY_NAME}" |
|
|
50
|
+
| copyright.txt | "Copyright {YEAR} {COMPANY_NAME}" — placed at metadata root, NOT inside locale dirs |
|
|
51
51
|
|
|
52
52
|
### Age Rating Config (fastlane/app_rating_config.json)
|
|
53
53
|
|
|
@@ -241,6 +241,7 @@ fastlane/
|
|
|
241
241
|
app_rating_config.json
|
|
242
242
|
data_safety.csv
|
|
243
243
|
metadata/
|
|
244
|
+
copyright.txt
|
|
244
245
|
review_information/
|
|
245
246
|
first_name.txt
|
|
246
247
|
last_name.txt
|
|
@@ -250,7 +251,6 @@ fastlane/
|
|
|
250
251
|
demo_password.txt
|
|
251
252
|
notes.txt
|
|
252
253
|
ios/
|
|
253
|
-
copyright.txt
|
|
254
254
|
en-US/
|
|
255
255
|
name.txt
|
|
256
256
|
subtitle.txt
|
|
@@ -58,6 +58,11 @@ IAP and subscription configuration. Durations: ONE_WEEK to ONE_YEAR. Intro offer
|
|
|
58
58
|
### fastlane/screenshots/
|
|
59
59
|
Store screenshots by platform and device size. Generated by app-designer via Stitch MCP.
|
|
60
60
|
|
|
61
|
+
### App Privacy (Manual Step)
|
|
62
|
+
App Privacy declarations must be set manually by an Admin in the App Store Connect web interface.
|
|
63
|
+
This is a one-time setup per app (not per release). Go to App Store Connect > Your App > App Privacy.
|
|
64
|
+
Apple requires this before the first submission. It cannot be automated via API or fastlane.
|
|
65
|
+
|
|
61
66
|
### App Icon Setup
|
|
62
67
|
The app icon must be set before the first release. Flutter creates projects with a default icon that MUST be replaced.
|
|
63
68
|
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
App Store Connect app setup: content rights and pricing.
|
|
4
|
+
|
|
5
|
+
Sets the content rights declaration and configures free (or paid) pricing
|
|
6
|
+
via the App Store Connect API.
|
|
7
|
+
|
|
8
|
+
Required env vars:
|
|
9
|
+
APP_STORE_CONNECT_KEY_IDENTIFIER - Key ID from App Store Connect
|
|
10
|
+
APP_STORE_CONNECT_ISSUER_ID - Issuer ID from App Store Connect
|
|
11
|
+
APP_STORE_CONNECT_PRIVATE_KEY - Contents of the P8 key file
|
|
12
|
+
BUNDLE_ID - App bundle identifier
|
|
13
|
+
|
|
14
|
+
Optional env vars:
|
|
15
|
+
PRICE_TIER - Price tier (default: "0" for free)
|
|
16
|
+
CONTENT_RIGHTS_THIRD_PARTY - "true" if app uses third-party content (default: "false")
|
|
17
|
+
|
|
18
|
+
Exit codes:
|
|
19
|
+
0 - Setup completed successfully
|
|
20
|
+
1 - Any failure (missing env vars, API error, etc.)
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import jwt
|
|
28
|
+
import requests
|
|
29
|
+
except ImportError:
|
|
30
|
+
import subprocess
|
|
31
|
+
subprocess.check_call(
|
|
32
|
+
[sys.executable, "-m", "pip", "install", "--break-system-packages", "PyJWT", "cryptography", "requests"],
|
|
33
|
+
stdout=subprocess.DEVNULL,
|
|
34
|
+
)
|
|
35
|
+
import jwt
|
|
36
|
+
import requests
|
|
37
|
+
|
|
38
|
+
BASE_URL = "https://api.appstoreconnect.apple.com/v1"
|
|
39
|
+
TIMEOUT = (10, 30)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_jwt_token(key_id: str, issuer_id: str, private_key: str) -> str:
|
|
43
|
+
"""Generate a signed JWT for App Store Connect API authentication."""
|
|
44
|
+
now = int(time.time())
|
|
45
|
+
payload = {
|
|
46
|
+
"iss": issuer_id,
|
|
47
|
+
"iat": now,
|
|
48
|
+
"exp": now + 1200,
|
|
49
|
+
"aud": "appstoreconnect-v1",
|
|
50
|
+
}
|
|
51
|
+
return jwt.encode(payload, private_key, algorithm="ES256", headers={"kid": key_id})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_app_id(headers: dict, bundle_id: str) -> str:
|
|
55
|
+
"""Look up the App Store Connect app ID for the given bundle identifier."""
|
|
56
|
+
resp = requests.get(
|
|
57
|
+
f"{BASE_URL}/apps",
|
|
58
|
+
params={"filter[bundleId]": bundle_id},
|
|
59
|
+
headers=headers,
|
|
60
|
+
timeout=TIMEOUT,
|
|
61
|
+
)
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
data = resp.json().get("data", [])
|
|
64
|
+
if not data:
|
|
65
|
+
print(f"ERROR: No app found for bundle ID '{bundle_id}'", file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
return data[0]["id"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_api_errors(resp, action: str) -> None:
|
|
71
|
+
"""Print human-readable API error messages."""
|
|
72
|
+
try:
|
|
73
|
+
errors = resp.json().get("errors", [])
|
|
74
|
+
for err in errors:
|
|
75
|
+
detail = err.get("detail", err.get("title", "Unknown error"))
|
|
76
|
+
print(f"ERROR ({action}): {detail}", file=sys.stderr)
|
|
77
|
+
except (ValueError, KeyError):
|
|
78
|
+
print(f"ERROR ({action}): HTTP {resp.status_code} - {resp.text[:200]}", file=sys.stderr)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def set_content_rights(headers: dict, app_id: str, uses_third_party: bool) -> None:
|
|
82
|
+
"""Set the content rights declaration on the app."""
|
|
83
|
+
desired = "USES_THIRD_PARTY_CONTENT" if uses_third_party else "DOES_NOT_USE_THIRD_PARTY_CONTENT"
|
|
84
|
+
|
|
85
|
+
resp = requests.get(f"{BASE_URL}/apps/{app_id}", headers=headers, timeout=TIMEOUT)
|
|
86
|
+
resp.raise_for_status()
|
|
87
|
+
current = resp.json()["data"]["attributes"].get("contentRightsDeclaration")
|
|
88
|
+
|
|
89
|
+
if current == desired:
|
|
90
|
+
print(f" Content rights already set to '{desired}', skipping.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
patch_resp = requests.patch(
|
|
94
|
+
f"{BASE_URL}/apps/{app_id}",
|
|
95
|
+
json={
|
|
96
|
+
"data": {
|
|
97
|
+
"type": "apps",
|
|
98
|
+
"id": app_id,
|
|
99
|
+
"attributes": {"contentRightsDeclaration": desired},
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
headers=headers,
|
|
103
|
+
timeout=TIMEOUT,
|
|
104
|
+
)
|
|
105
|
+
if not patch_resp.ok:
|
|
106
|
+
print_api_errors(patch_resp, "set content rights")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
print(f" Content rights set to '{desired}'.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_app_price_points(headers: dict, app_id: str, territory: str = "USA") -> list:
|
|
112
|
+
"""Fetch all price points for the app in the given territory, with pagination."""
|
|
113
|
+
url = f"{BASE_URL}/apps/{app_id}/appPricePoints"
|
|
114
|
+
params = {"filter[territory]": territory, "limit": 200}
|
|
115
|
+
all_points = []
|
|
116
|
+
|
|
117
|
+
while url:
|
|
118
|
+
resp = requests.get(url, params=params, headers=headers, timeout=TIMEOUT)
|
|
119
|
+
resp.raise_for_status()
|
|
120
|
+
body = resp.json()
|
|
121
|
+
all_points.extend(body.get("data", []))
|
|
122
|
+
url = body.get("links", {}).get("next")
|
|
123
|
+
params = None
|
|
124
|
+
|
|
125
|
+
return all_points
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def find_price_point_for_tier(price_points: list, tier: int = 0) -> str:
|
|
129
|
+
"""Find the price point ID matching the given tier. Tier 0 = free."""
|
|
130
|
+
if tier == 0:
|
|
131
|
+
for pp in price_points:
|
|
132
|
+
price = pp.get("attributes", {}).get("customerPrice", "")
|
|
133
|
+
if price in ("0", "0.0", "0.00"):
|
|
134
|
+
return pp["id"]
|
|
135
|
+
print("ERROR: Could not find a free (tier 0) price point.", file=sys.stderr)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
for pp in price_points:
|
|
139
|
+
price = pp.get("attributes", {}).get("customerPrice", "")
|
|
140
|
+
try:
|
|
141
|
+
if float(price) > 0 and pp.get("attributes", {}).get("priceTier") == str(tier):
|
|
142
|
+
return pp["id"]
|
|
143
|
+
except (ValueError, TypeError):
|
|
144
|
+
continue
|
|
145
|
+
print(f"ERROR: Could not find price point for tier {tier}.", file=sys.stderr)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _is_pricing_set(headers: dict, app_id: str, price_tier: int) -> bool:
|
|
150
|
+
"""Check if the app already has pricing set for the given tier."""
|
|
151
|
+
schedule_resp = requests.get(
|
|
152
|
+
f"{BASE_URL}/apps/{app_id}/appPriceSchedule",
|
|
153
|
+
params={"include": "manualPrices,baseTerritory"},
|
|
154
|
+
headers=headers,
|
|
155
|
+
timeout=TIMEOUT,
|
|
156
|
+
)
|
|
157
|
+
if not schedule_resp.ok:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
included = schedule_resp.json().get("included", [])
|
|
161
|
+
for item in included:
|
|
162
|
+
if item.get("type") == "appPrices":
|
|
163
|
+
existing_price = item.get("attributes", {}).get("customerPrice", "")
|
|
164
|
+
if price_tier == 0 and existing_price in ("0", "0.0", "0.00"):
|
|
165
|
+
return True
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _create_price_schedule(headers: dict, app_id: str, price_point_id: str) -> requests.Response:
|
|
170
|
+
"""POST a new app price schedule and return the response."""
|
|
171
|
+
price_ref_id = "${price-usa}"
|
|
172
|
+
return requests.post(
|
|
173
|
+
f"{BASE_URL}/appPriceSchedules",
|
|
174
|
+
json={
|
|
175
|
+
"data": {
|
|
176
|
+
"type": "appPriceSchedules",
|
|
177
|
+
"relationships": {
|
|
178
|
+
"app": {"data": {"type": "apps", "id": app_id}},
|
|
179
|
+
"baseTerritory": {"data": {"type": "territories", "id": "USA"}},
|
|
180
|
+
"manualPrices": {"data": [{"type": "appPrices", "id": price_ref_id}]},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
"included": [
|
|
184
|
+
{
|
|
185
|
+
"type": "appPrices",
|
|
186
|
+
"id": price_ref_id,
|
|
187
|
+
"attributes": {"startDate": None},
|
|
188
|
+
"relationships": {
|
|
189
|
+
"appPricePoint": {
|
|
190
|
+
"data": {"type": "appPricePoints", "id": price_point_id}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
headers=headers,
|
|
197
|
+
timeout=TIMEOUT,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def set_app_pricing(headers: dict, app_id: str, price_tier: int = 0) -> None:
|
|
202
|
+
"""Set the app price schedule. Handles idempotency via pre-check and 409 Conflict."""
|
|
203
|
+
if _is_pricing_set(headers, app_id, price_tier):
|
|
204
|
+
print(" Pricing already set to free, skipping.")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
print(f" Fetching price points for USA territory...")
|
|
208
|
+
price_points = get_app_price_points(headers, app_id, territory="USA")
|
|
209
|
+
if not price_points:
|
|
210
|
+
print("ERROR: No price points returned for USA territory.", file=sys.stderr)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
pp_id = find_price_point_for_tier(price_points, tier=price_tier)
|
|
214
|
+
print(f" Found price point ID: {pp_id}")
|
|
215
|
+
|
|
216
|
+
post_resp = _create_price_schedule(headers, app_id, pp_id)
|
|
217
|
+
tier_label = "Free" if price_tier == 0 else f"Tier {price_tier}"
|
|
218
|
+
|
|
219
|
+
if post_resp.status_code == 409:
|
|
220
|
+
print(f" Pricing already set ({tier_label}), skipping (409 Conflict).")
|
|
221
|
+
return
|
|
222
|
+
if not post_resp.ok:
|
|
223
|
+
print_api_errors(post_resp, "set app pricing")
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
print(f" Pricing set to {tier_label}.")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main() -> None:
|
|
229
|
+
key_id = os.environ.get("APP_STORE_CONNECT_KEY_IDENTIFIER", "")
|
|
230
|
+
issuer_id = os.environ.get("APP_STORE_CONNECT_ISSUER_ID", "")
|
|
231
|
+
private_key = os.environ.get("APP_STORE_CONNECT_PRIVATE_KEY", "")
|
|
232
|
+
bundle_id = os.environ.get("BUNDLE_ID", "")
|
|
233
|
+
price_tier_str = os.environ.get("PRICE_TIER", "0")
|
|
234
|
+
third_party_str = os.environ.get("CONTENT_RIGHTS_THIRD_PARTY", "false")
|
|
235
|
+
|
|
236
|
+
missing = []
|
|
237
|
+
if not key_id:
|
|
238
|
+
missing.append("APP_STORE_CONNECT_KEY_IDENTIFIER")
|
|
239
|
+
if not issuer_id:
|
|
240
|
+
missing.append("APP_STORE_CONNECT_ISSUER_ID")
|
|
241
|
+
if not private_key:
|
|
242
|
+
missing.append("APP_STORE_CONNECT_PRIVATE_KEY")
|
|
243
|
+
if not bundle_id:
|
|
244
|
+
missing.append("BUNDLE_ID")
|
|
245
|
+
if missing:
|
|
246
|
+
print(f"ERROR: Missing required environment variables: {', '.join(missing)}", file=sys.stderr)
|
|
247
|
+
sys.exit(1)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
price_tier = int(price_tier_str)
|
|
251
|
+
except ValueError:
|
|
252
|
+
print(f"ERROR: PRICE_TIER must be an integer, got '{price_tier_str}'", file=sys.stderr)
|
|
253
|
+
sys.exit(1)
|
|
254
|
+
|
|
255
|
+
uses_third_party = third_party_str.lower() == "true"
|
|
256
|
+
|
|
257
|
+
token = get_jwt_token(key_id, issuer_id, private_key)
|
|
258
|
+
headers = {
|
|
259
|
+
"Authorization": f"Bearer {token}",
|
|
260
|
+
"Content-Type": "application/json",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
print(f"Looking up app for bundle ID '{bundle_id}'...")
|
|
264
|
+
app_id = get_app_id(headers, bundle_id)
|
|
265
|
+
print(f"Found app ID: {app_id}")
|
|
266
|
+
|
|
267
|
+
print("Setting content rights declaration...")
|
|
268
|
+
set_content_rights(headers, app_id, uses_third_party)
|
|
269
|
+
|
|
270
|
+
print("Setting app pricing...")
|
|
271
|
+
set_app_pricing(headers, app_id, price_tier)
|
|
272
|
+
|
|
273
|
+
print("App setup complete.")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
main()
|
|
@@ -53,6 +53,9 @@ export PRICE_TIER=$(yq '.ios.price_tier // ""' "$CONFIG")
|
|
|
53
53
|
export SUBMIT_FOR_REVIEW=$(yq '.ios.submit_for_review // "false"' "$CONFIG")
|
|
54
54
|
export AUTOMATIC_RELEASE=$(yq '.ios.automatic_release // "false"' "$CONFIG")
|
|
55
55
|
|
|
56
|
+
# App Info settings
|
|
57
|
+
export CONTENT_RIGHTS_THIRD_PARTY=$(yq '.ios.content_rights_third_party // "false"' "$CONFIG")
|
|
58
|
+
|
|
56
59
|
# Android Play Store settings
|
|
57
60
|
export TRACK=$(yq '.android.track // "internal"' "$CONFIG")
|
|
58
61
|
export ROLLOUT_FRACTION=$(yq '.android.rollout_fraction // ""' "$CONFIG")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sets up App Store Connect app info (content rights, age rating, categories) via ASC API.
|
|
3
|
+
# Requires read-config.sh to have been sourced (provides PROJECT_ROOT, credentials, etc.).
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/../common/read-config.sh"
|
|
8
|
+
source "$SCRIPT_DIR/../common/ci-notify.sh"
|
|
9
|
+
|
|
10
|
+
echo "=== App Store Connect App Info Setup ==="
|
|
11
|
+
|
|
12
|
+
# --- Validate ASC credentials ---
|
|
13
|
+
if [ -z "${APPLE_KEY_ID:-}" ] || [ -z "${APPLE_ISSUER_ID:-}" ]; then
|
|
14
|
+
ci_skip "ASC credentials not configured"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# --- Set up App Store Connect API credentials ---
|
|
18
|
+
P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
|
|
19
|
+
if [ ! -f "$P8_FULL_PATH" ]; then
|
|
20
|
+
echo "ERROR: P8 key file not found at $P8_FULL_PATH" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
|
|
25
|
+
export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
|
|
26
|
+
export APP_STORE_CONNECT_PRIVATE_KEY="$(cat "$P8_FULL_PATH")"
|
|
27
|
+
export BUNDLE_ID="$BUNDLE_ID"
|
|
28
|
+
export PROJECT_ROOT="$PROJECT_ROOT"
|
|
29
|
+
|
|
30
|
+
echo "ASC API key configured (Key ID: $APPLE_KEY_ID)"
|
|
31
|
+
|
|
32
|
+
# --- Run app setup via Python ---
|
|
33
|
+
SETUP_SCRIPT="$PROJECT_ROOT/scripts/asc_app_setup.py"
|
|
34
|
+
|
|
35
|
+
if [ ! -f "$SETUP_SCRIPT" ]; then
|
|
36
|
+
echo "ERROR: asc_app_setup.py not found at $SETUP_SCRIPT" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "Setting up App Store Connect app info..."
|
|
41
|
+
python3 "$SETUP_SCRIPT"
|
|
42
|
+
|
|
43
|
+
ci_done "App Store Connect app info configured"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|