@daemux/store-automator 0.10.68 → 0.10.70
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.70"
|
|
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.70",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Set TestFlight "What to Test" (betaBuildLocalization.whatsNew) for the build
|
|
4
|
+
just uploaded by this workflow.
|
|
5
|
+
|
|
6
|
+
Runs after the altool upload step. Polls ASC until the build record appears
|
|
7
|
+
(altool upload returns before Apple has fully ingested the build), then either
|
|
8
|
+
PATCHes an existing betaBuildLocalization for the requested locale or POSTs a
|
|
9
|
+
new one.
|
|
10
|
+
|
|
11
|
+
Environment:
|
|
12
|
+
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH -- App Store Connect API credentials
|
|
13
|
+
APP_STORE_APPLE_ID -- numeric ASC app id
|
|
14
|
+
MARKETING_VERSION -- e.g. "1.2.3"
|
|
15
|
+
BUILD_NUMBER -- e.g. "42"
|
|
16
|
+
TESTFLIGHT_WHATS_NEW -- release notes text (empty = skip)
|
|
17
|
+
TESTFLIGHT_LOCALE -- locale code, default "en-US"
|
|
18
|
+
|
|
19
|
+
Non-fatal by design: if the build can't be found within the polling window we
|
|
20
|
+
emit a ::warning:: and exit 0, since the TestFlight upload itself may still
|
|
21
|
+
have succeeded.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
from asc_common import get_json, make_jwt, request
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
POLL_ATTEMPTS = 20
|
|
34
|
+
POLL_FIRST_DELAY = 10
|
|
35
|
+
POLL_DELAY = 30
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _require_env(name: str) -> str:
|
|
39
|
+
value = os.environ.get(name, "").strip()
|
|
40
|
+
if not value:
|
|
41
|
+
print(f"::error::{name} env var is required", file=sys.stderr)
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _find_build(token: str, app_id: str, marketing_version: str, build_number: str) -> str | None:
|
|
47
|
+
"""Return the build id matching (marketing_version, build_number), or None."""
|
|
48
|
+
params = {
|
|
49
|
+
"filter[app]": app_id,
|
|
50
|
+
"filter[preReleaseVersion.version]": marketing_version,
|
|
51
|
+
"filter[version]": build_number,
|
|
52
|
+
"limit": "5",
|
|
53
|
+
}
|
|
54
|
+
data = get_json("/builds", token, params=params)
|
|
55
|
+
items = data.get("data") or []
|
|
56
|
+
return items[0].get("id") if items else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _poll_for_build(token: str, app_id: str, marketing_version: str, build_number: str) -> str | None:
|
|
60
|
+
"""Poll ASC for the uploaded build. Returns build id or None on timeout."""
|
|
61
|
+
for attempt in range(POLL_ATTEMPTS):
|
|
62
|
+
delay = POLL_FIRST_DELAY if attempt == 0 else POLL_DELAY
|
|
63
|
+
print(
|
|
64
|
+
f"Waiting {delay}s for build {marketing_version} ({build_number}) to appear "
|
|
65
|
+
f"(attempt {attempt + 1}/{POLL_ATTEMPTS})",
|
|
66
|
+
file=sys.stderr,
|
|
67
|
+
)
|
|
68
|
+
time.sleep(delay)
|
|
69
|
+
build_id = _find_build(token, app_id, marketing_version, build_number)
|
|
70
|
+
if build_id:
|
|
71
|
+
print(f"Found build id {build_id}", file=sys.stderr)
|
|
72
|
+
return build_id
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _existing_localization(token: str, build_id: str, locale: str) -> str | None:
|
|
77
|
+
"""Return the betaBuildLocalization id for `locale` on this build, or None."""
|
|
78
|
+
data = get_json(f"/builds/{build_id}/betaBuildLocalizations", token)
|
|
79
|
+
for item in data.get("data") or []:
|
|
80
|
+
if (item.get("attributes") or {}).get("locale") == locale:
|
|
81
|
+
return item.get("id")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _patch_localization(token: str, localization_id: str, whats_new: str) -> None:
|
|
86
|
+
body = {
|
|
87
|
+
"data": {
|
|
88
|
+
"type": "betaBuildLocalizations",
|
|
89
|
+
"id": localization_id,
|
|
90
|
+
"attributes": {"whatsNew": whats_new},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
request("PATCH", f"/betaBuildLocalizations/{localization_id}", token, json_body=body)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _create_localization(token: str, build_id: str, locale: str, whats_new: str) -> None:
|
|
97
|
+
body = {
|
|
98
|
+
"data": {
|
|
99
|
+
"type": "betaBuildLocalizations",
|
|
100
|
+
"attributes": {"locale": locale, "whatsNew": whats_new},
|
|
101
|
+
"relationships": {
|
|
102
|
+
"build": {"data": {"type": "builds", "id": build_id}},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
request("POST", "/betaBuildLocalizations", token, json_body=body)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> int:
|
|
110
|
+
whats_new = os.environ.get("TESTFLIGHT_WHATS_NEW", "")
|
|
111
|
+
if not whats_new.strip():
|
|
112
|
+
print("empty whatsNew; skipping", file=sys.stderr)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
key_id = _require_env("ASC_KEY_ID")
|
|
116
|
+
issuer_id = _require_env("ASC_ISSUER_ID")
|
|
117
|
+
key_path = _require_env("ASC_KEY_PATH")
|
|
118
|
+
app_id = _require_env("APP_STORE_APPLE_ID")
|
|
119
|
+
marketing_version = _require_env("MARKETING_VERSION")
|
|
120
|
+
build_number = _require_env("BUILD_NUMBER")
|
|
121
|
+
locale = os.environ.get("TESTFLIGHT_LOCALE", "").strip() or "en-US"
|
|
122
|
+
|
|
123
|
+
token = make_jwt(key_id, issuer_id, key_path)
|
|
124
|
+
|
|
125
|
+
build_id = _poll_for_build(token, app_id, marketing_version, build_number)
|
|
126
|
+
if not build_id:
|
|
127
|
+
print(
|
|
128
|
+
f"::warning::build {marketing_version} ({build_number}) not found in ASC "
|
|
129
|
+
f"after {POLL_ATTEMPTS} polls; skipping whatsNew update",
|
|
130
|
+
file=sys.stderr,
|
|
131
|
+
)
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
existing = _existing_localization(token, build_id, locale)
|
|
135
|
+
if existing:
|
|
136
|
+
_patch_localization(token, existing, whats_new)
|
|
137
|
+
action = "updated"
|
|
138
|
+
else:
|
|
139
|
+
_create_localization(token, build_id, locale, whats_new)
|
|
140
|
+
action = "created"
|
|
141
|
+
print(f"Set betaBuildLocalization whatsNew for build {build_id} locale {locale} ({action})")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
sys.exit(main())
|