|
|
@@ -97,6 +97,21 @@ class ASCClient:
|
|
|
)
|
|
|
return response.json()
|
|
|
|
|
|
+ def post(self, resource_type: str, attributes: dict, relationships: dict) -> dict:
|
|
|
+ body = {
|
|
|
+ "data": {
|
|
|
+ "type": resource_type,
|
|
|
+ "attributes": attributes,
|
|
|
+ "relationships": relationships,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ response = self.request("POST", f"/{resource_type}", json=body)
|
|
|
+ if not response.ok:
|
|
|
+ raise RuntimeError(
|
|
|
+ f"POST {resource_type} failed ({response.status_code}): {response.text}"
|
|
|
+ )
|
|
|
+ return response.json()["data"]
|
|
|
+
|
|
|
|
|
|
def find_app(client: ASCClient, bundle_id: str) -> dict:
|
|
|
data = client.get_json("/apps", params={"filter[bundleId]": bundle_id, "limit": 1})
|
|
|
@@ -108,23 +123,31 @@ def find_app(client: ASCClient, bundle_id: str) -> dict:
|
|
|
|
|
|
def find_mac_app_info(client: ASCClient, app_id: str) -> dict:
|
|
|
data = client.get_json(f"/apps/{app_id}/appInfos")
|
|
|
- for info in data.get("data", []):
|
|
|
- attrs = info.get("attributes", {})
|
|
|
- if attrs.get("appStoreState") is not None or attrs.get("brazilAgeRating") is not None:
|
|
|
- # Prefer MAC_OS if platform is listed in included relationships; filter by bundle platform.
|
|
|
- pass
|
|
|
- # App infos are per platform — pick MAC_OS via appStoreAgeRating or secondary query.
|
|
|
- for info in data.get("data", []):
|
|
|
+ infos = data.get("data", [])
|
|
|
+ if not infos:
|
|
|
+ raise RuntimeError(f"No appInfos for app {app_id}")
|
|
|
+
|
|
|
+ editable_states = {
|
|
|
+ "PREPARE_FOR_SUBMISSION",
|
|
|
+ "DEVELOPER_REJECTED",
|
|
|
+ "REJECTED",
|
|
|
+ "METADATA_REJECTED",
|
|
|
+ }
|
|
|
+ for info in infos:
|
|
|
+ state = info.get("attributes", {}).get("appStoreState")
|
|
|
+ if state in editable_states:
|
|
|
+ return info
|
|
|
+
|
|
|
+ for info in infos:
|
|
|
info_id = info["id"]
|
|
|
detail = client.get_json(f"/appInfos/{info_id}")
|
|
|
platform = detail.get("data", {}).get("attributes", {}).get("platform")
|
|
|
if platform == "MAC_OS":
|
|
|
return detail["data"]
|
|
|
- # Fallback: single app info entry.
|
|
|
- infos = data.get("data", [])
|
|
|
+
|
|
|
if len(infos) == 1:
|
|
|
return infos[0]
|
|
|
- raise RuntimeError("Could not determine MAC_OS appInfo; inspect /apps/{id}/appInfos")
|
|
|
+ return infos[0]
|
|
|
|
|
|
|
|
|
def find_app_info_localization(client: ASCClient, app_info_id: str, locale: str) -> dict:
|
|
|
@@ -135,7 +158,7 @@ def find_app_info_localization(client: ASCClient, app_info_id: str, locale: str)
|
|
|
raise RuntimeError(f"No appInfoLocalization for locale {locale!r}")
|
|
|
|
|
|
|
|
|
-def find_version(client: ASCClient, app_id: str, version_string: str) -> dict:
|
|
|
+def find_version(client: ASCClient, app_id: str, version_string: str, *, create: bool = False) -> dict:
|
|
|
data = client.get_json(
|
|
|
f"/apps/{app_id}/appStoreVersions",
|
|
|
params={"filter[platform]": "MAC_OS", "limit": 50},
|
|
|
@@ -146,7 +169,15 @@ def find_version(client: ASCClient, app_id: str, version_string: str) -> dict:
|
|
|
if v.get("attributes", {}).get("versionString") == version_string
|
|
|
]
|
|
|
if not matches:
|
|
|
- raise RuntimeError(f"No MAC_OS appStoreVersion {version_string!r}")
|
|
|
+ if not create:
|
|
|
+ raise RuntimeError(f"No MAC_OS appStoreVersion {version_string!r}")
|
|
|
+ created = client.post(
|
|
|
+ "appStoreVersions",
|
|
|
+ {"platform": "MAC_OS", "versionString": version_string},
|
|
|
+ {"app": {"data": {"type": "apps", "id": app_id}}},
|
|
|
+ )
|
|
|
+ print(f"Created version {version_string}: {created['id']}")
|
|
|
+ return created
|
|
|
# Prefer editable state if multiple.
|
|
|
for v in matches:
|
|
|
state = v.get("attributes", {}).get("appStoreState")
|
|
|
@@ -155,12 +186,22 @@ def find_version(client: ASCClient, app_id: str, version_string: str) -> dict:
|
|
|
return matches[0]
|
|
|
|
|
|
|
|
|
-def find_version_localization(client: ASCClient, version_id: str, locale: str) -> dict:
|
|
|
+def find_version_localization(
|
|
|
+ client: ASCClient, version_id: str, locale: str, *, create: bool = False
|
|
|
+) -> dict:
|
|
|
data = client.get_json(f"/appStoreVersions/{version_id}/appStoreVersionLocalizations")
|
|
|
for loc in data.get("data", []):
|
|
|
if loc.get("attributes", {}).get("locale") == locale:
|
|
|
return loc
|
|
|
- raise RuntimeError(f"No appStoreVersionLocalization for locale {locale!r}")
|
|
|
+ if not create:
|
|
|
+ raise RuntimeError(f"No appStoreVersionLocalization for locale {locale!r}")
|
|
|
+ created = client.post(
|
|
|
+ "appStoreVersionLocalizations",
|
|
|
+ {"locale": locale},
|
|
|
+ {"appStoreVersion": {"data": {"type": "appStoreVersions", "id": version_id}}},
|
|
|
+ )
|
|
|
+ print(f"Created version localization ({locale}): {created['id']}")
|
|
|
+ return created
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
@@ -208,7 +249,8 @@ def main() -> int:
|
|
|
client.patch("appInfoLocalizations", info_loc_id, info_patch)
|
|
|
print(f"Updated App Information localization ({LOCALE}): name, subtitle, privacyPolicyUrl")
|
|
|
|
|
|
- version = find_version(client, app_id, VERSION_STRING)
|
|
|
+ create_version = os.environ.get("APP_STORE_CREATE_VERSION", "").lower() in ("1", "true", "yes")
|
|
|
+ version = find_version(client, app_id, VERSION_STRING, create=create_version)
|
|
|
version_id = version["id"]
|
|
|
version_state = version.get("attributes", {}).get("appStoreState")
|
|
|
print(f"Version {VERSION_STRING}: {version_id} (state: {version_state})")
|
|
|
@@ -217,7 +259,7 @@ def main() -> int:
|
|
|
client.patch("appStoreVersions", version_id, {"copyright": copyright_text})
|
|
|
print(f"Updated version copyright")
|
|
|
|
|
|
- version_loc = find_version_localization(client, version_id, LOCALE)
|
|
|
+ version_loc = find_version_localization(client, version_id, LOCALE, create=create_version)
|
|
|
version_loc_id = version_loc["id"]
|
|
|
|
|
|
version_patch = {
|
|
|
@@ -227,8 +269,14 @@ def main() -> int:
|
|
|
"marketingUrl": read_field("link-marketing.txt"),
|
|
|
"supportUrl": read_field("link-support.txt"),
|
|
|
}
|
|
|
+ whats_new_path = APP_DATA / "whats-new.txt"
|
|
|
+ if whats_new_path.exists():
|
|
|
+ version_patch["whatsNew"] = whats_new_path.read_text(encoding="utf-8").strip()
|
|
|
client.patch("appStoreVersionLocalizations", version_loc_id, version_patch)
|
|
|
- print(f"Updated version {VERSION_STRING} localization ({LOCALE}): description, keywords, promotionalText, marketingUrl, supportUrl")
|
|
|
+ fields = "description, keywords, promotionalText, marketingUrl, supportUrl"
|
|
|
+ if "whatsNew" in version_patch:
|
|
|
+ fields += ", whatsNew"
|
|
|
+ print(f"Updated version {VERSION_STRING} localization ({LOCALE}): {fields}")
|
|
|
|
|
|
# Custom EULA URL (Terms) — attach to app if not using standard Apple EULA.
|
|
|
terms_url = read_field("link-terms.txt")
|