Skip to content

Commit 7be7439

Browse files
committed
bump to 1.1.29 + add update-whats-new helper
- perry.toml: bump project.version to 1.1.29 for the next release. - update-whats-new.sh: helper that pushes "What's New in This Version" copy to App Store Connect for the current build. - .gitignore: ignore Mango-* and telemetry build artifacts that the release scripts drop in the project root.
1 parent a89f7a3 commit 7be7439

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ _perry_failed_stubs.o
77
# Compiled binaries
88
mango
99
Mango.exe
10+
Mango-*
1011
app
1112
app_debug
1213
mango-android
1314
screenshot-mac
1415
screenshot-ios-sim
1516
libhone_editor_android.so
17+
telemetry
1618

1719
# Runtime data
1820
*.db

perry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
build_number = 26
33
entry = "src/app.ts"
44
name = "Mango"
5-
version = "1.1.28"
5+
version = "1.1.29"
66

77
[project.icons]
88
source = "assets/icon.png"

update-whats-new.sh

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/bin/bash
2+
# Update "What's New in This Version" for Mango on App Store Connect.
3+
# Usage: ./update-whats-new.sh "Bug fixes and improvements"
4+
set -euo pipefail
5+
6+
TEXT="${1:?Usage: ./update-whats-new.sh \"Your release notes text\"}"
7+
8+
BUNDLE_ID="com.skelpo.mango"
9+
LOCALES=(en de ja zh-Hans es-MX fr pt ko it tr th id vi)
10+
11+
# Generate JWT and run all API calls in one python3 invocation
12+
python3 - "$TEXT" "$BUNDLE_ID" "${LOCALES[@]}" << 'PYEOF'
13+
import sys, json, time, struct, hashlib, hmac, urllib.request, urllib.error
14+
15+
text = sys.argv[1]
16+
bundle_id = sys.argv[2]
17+
locales_raw = sys.argv[3:]
18+
19+
# Map perry.toml short codes to App Store Connect locale codes
20+
ASC_LOCALE = {
21+
"en": "en-US", "de": "de-DE", "fr": "fr-FR", "pt": "pt-BR",
22+
"it": "it", "ko": "ko", "ja": "ja", "tr": "tr", "th": "th",
23+
"id": "id", "vi": "vi", "zh-Hans": "zh-Hans", "es-MX": "es-MX",
24+
}
25+
locales = [ASC_LOCALE.get(l, l) for l in locales_raw]
26+
27+
# --- Credentials ---
28+
KEY_ID = "MPJ792KV5Z"
29+
ISSUER_ID = "69a6de6f-e591-47e3-e053-5b8c7c11a4d1"
30+
31+
import os
32+
P8_PATH = os.path.expanduser(f"~/.perry/AuthKey_{KEY_ID}.p8")
33+
34+
# --- JWT generation (ES256) ---
35+
import base64, subprocess
36+
37+
def b64url(data):
38+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
39+
40+
def generate_jwt():
41+
header = b64url(json.dumps({"alg":"ES256","kid":KEY_ID,"typ":"JWT"}).encode())
42+
now = int(time.time())
43+
payload = b64url(json.dumps({"iss":ISSUER_ID,"iat":now,"exp":now+1200,"aud":"appstoreconnect-v1"}).encode())
44+
signing_input = f"{header}.{payload}".encode()
45+
46+
# Sign with openssl, get DER-encoded signature
47+
proc = subprocess.run(
48+
["openssl", "dgst", "-sha256", "-sign", P8_PATH],
49+
input=signing_input, capture_output=True
50+
)
51+
der_sig = proc.stdout
52+
53+
# Convert DER to raw R||S (64 bytes) for JWS
54+
# DER: 30 <total_len> 02 <r_len> <R> 02 <s_len> <S>
55+
assert der_sig[0] == 0x30
56+
idx = 1
57+
# skip length field (1 byte if < 128, else multi-byte)
58+
if der_sig[idx] & 0x80:
59+
idx += (der_sig[idx] & 0x7f) + 1
60+
else:
61+
idx += 1
62+
# R integer
63+
assert der_sig[idx] == 0x02; idx += 1
64+
r_len = der_sig[idx]; idx += 1
65+
r = der_sig[idx:idx+r_len]; idx += r_len
66+
# S integer
67+
assert der_sig[idx] == 0x02; idx += 1
68+
s_len = der_sig[idx]; idx += 1
69+
s = der_sig[idx:idx+s_len]
70+
# Pad/trim to 32 bytes each (strip leading 0x00 padding, right-justify)
71+
r = r[-32:].rjust(32, b'\x00')
72+
s = s[-32:].rjust(32, b'\x00')
73+
74+
sig = b64url(r + s)
75+
return f"{header}.{payload}.{sig}"
76+
77+
token = generate_jwt()
78+
BASE = "https://api.appstoreconnect.apple.com/v1"
79+
80+
def api(method, path, body=None):
81+
url = f"{BASE}/{path}" if not path.startswith("http") else path
82+
data = json.dumps(body).encode() if body else None
83+
req = urllib.request.Request(url, data=data, method=method)
84+
req.add_header("Authorization", f"Bearer {token}")
85+
if data:
86+
req.add_header("Content-Type", "application/json")
87+
try:
88+
with urllib.request.urlopen(req) as resp:
89+
return json.loads(resp.read()), resp.status
90+
except urllib.error.HTTPError as e:
91+
body = e.read().decode() if e.fp else ""
92+
return {"error": body}, e.code
93+
94+
# 1. Find app
95+
print("==> Finding app...")
96+
data, status = api("GET", f"apps?filter[bundleId]={bundle_id}")
97+
if "data" not in data:
98+
print(f" API error (HTTP {status}): {json.dumps(data, indent=2)}", file=sys.stderr); sys.exit(1)
99+
app_id = data["data"][0]["id"]
100+
print(f" App ID: {app_id}")
101+
102+
# 2. Find editable version
103+
print("==> Finding editable version...")
104+
data, _ = api("GET", f"apps/{app_id}/appStoreVersions?filter[appStoreState]=PREPARE_FOR_SUBMISSION,READY_FOR_REVIEW&limit=1")
105+
if not data["data"]:
106+
data, _ = api("GET", f"apps/{app_id}/appStoreVersions?filter[appStoreState]=WAITING_FOR_REVIEW&limit=1")
107+
if not data["data"]:
108+
print("ERROR: No editable App Store version found.", file=sys.stderr); sys.exit(1)
109+
version_id = data["data"][0]["id"]
110+
version_str = data["data"][0]["attributes"]["versionString"]
111+
print(f" Version: {version_str} ({version_id})")
112+
113+
# 3. Get existing localizations
114+
print("==> Getting localizations...")
115+
data, _ = api("GET", f"appStoreVersions/{version_id}/appStoreVersionLocalizations?limit=50")
116+
existing = {l["attributes"]["locale"]: l["id"] for l in data["data"]}
117+
118+
# 4. Set release notes
119+
print(f"==> Setting release notes for {len(locales)} locales...")
120+
ok = 0
121+
for locale in locales:
122+
if locale in existing:
123+
loc_id = existing[locale]
124+
_, status = api("PATCH", f"appStoreVersionLocalizations/{loc_id}", {
125+
"data": {"type": "appStoreVersionLocalizations", "id": loc_id,
126+
"attributes": {"whatsNew": text}}
127+
})
128+
else:
129+
_, status = api("POST", "appStoreVersionLocalizations", {
130+
"data": {"type": "appStoreVersionLocalizations",
131+
"relationships": {"appStoreVersion": {"data": {"type": "appStoreVersions", "id": version_id}}},
132+
"attributes": {"locale": locale, "whatsNew": text}}
133+
})
134+
if 200 <= status < 300:
135+
print(f" OK {locale}")
136+
ok += 1
137+
else:
138+
print(f" FAIL {locale} (HTTP {status})")
139+
140+
print(f"==> Done: {ok}/{len(locales)} locales updated.")
141+
PYEOF

0 commit comments

Comments
 (0)