Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ them.
the tracks they joined, production included.
- **Production:** publish a release without ticking "Set as a pre-release".
Google Play gets it on the production track, App Review gets the iOS build
(released once approved), and Pages serves it at the site root.
(released once approved), and Pages serves it at the site root. An earlier
version still waiting for App Review is withdrawn and replaced, as Google
Play does; one that App Review has started stops the run.
- **Closed and internal tests** are not automated: upload the builds attached
to a release in Play Console and App Store Connect.
- **Promotion:** untick "Set as a pre-release" on a tested pre-release. The
Expand Down
49 changes: 39 additions & 10 deletions scripts/app_store_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,14 +459,37 @@ def submit(submission_id, version_id):
time.sleep(30)


def open_submission(app_id):
"""The unsent review submission, if any, with the versions it contains."""
submissions = call("GET", "/v1/reviewSubmissions", {
def review_submissions(app_id):
return call("GET", "/v1/reviewSubmissions", {
"filter[app]": app_id,
"filter[platform]": "IOS",
"filter[state]": "READY_FOR_REVIEW,WAITING_FOR_REVIEW,IN_REVIEW,UNRESOLVED_ISSUES",
})["data"]


def withdraw(submission_id, app_id):
"""Takes a submission that App Review has not started out of review."""
call("PATCH", f"/v1/reviewSubmissions/{submission_id}", body={"data": {
"type": "reviewSubmissions",
"id": submission_id,
"attributes": {"canceled": True},
}})
for _ in range(40):
if all(s["id"] != submission_id for s in review_submissions(app_id)):
print("Withdrew the version waiting for review; this release replaces it.")
return
time.sleep(15)
sys.exit("The submission waiting for review was not withdrawn in 10 minutes.")


def open_submission(app_id, replace_waiting=False):
"""The unsent review submission, if any, with the versions it contains;
with replace_waiting, withdraws one that waits for App Review first."""
submissions = review_submissions(app_id)
busy = [s for s in submissions if s["attributes"]["state"] != "READY_FOR_REVIEW"]
if busy and replace_waiting and busy[0]["attributes"]["state"] == "WAITING_FOR_REVIEW":
withdraw(busy[0]["id"], app_id)
return open_submission(app_id)
if busy:
sys.exit(
f"A submission is already {busy[0]['attributes']['state']}; "
Expand Down Expand Up @@ -591,6 +614,14 @@ def update_screenshots(version_id):
wait_for_screenshots(uploaded)


def app_store_versions(app_id):
return call("GET", f"/v1/apps/{app_id}/appStoreVersions", {
"filter[platform]": "IOS",
"fields[appStoreVersions]": "versionString,appVersionState,releaseType",
"limit": "200",
})["data"]


def main():
bundle_id = os.environ["BUNDLE_ID"]
version = os.environ["VERSION"]
Expand All @@ -615,11 +646,7 @@ def main():
public_beta(app, build, name)
return

versions = call("GET", f"/v1/apps/{app_id}/appStoreVersions", {
"filter[platform]": "IOS",
"fields[appStoreVersions]": "versionString,appVersionState,releaseType",
"limit": "200",
})["data"]
versions = app_store_versions(app_id)
same = next((v for v in versions if v["attributes"]["versionString"] == version), None)
state = same["attributes"]["appVersionState"] if same else None
if state in SUBMITTED or state == "READY_FOR_REVIEW":
Expand Down Expand Up @@ -647,11 +674,13 @@ def main():
"App Store updates need \"What's New\": write it in the GitHub "
"release description and re-run this job."
)
submission_id, contents = open_submission(app_id)
# An earlier version still waiting for App Review gives way to this one,
# as on Google Play; a withdrawn version becomes editable and is reused.
submission_id, contents = open_submission(app_id, replace_waiting=True)
if contents:
sys.exit("An unsent submission already has items; check it in App Store Connect.")

version_id = app_store_version(app_id, version, versions)
version_id = app_store_version(app_id, version, app_store_versions(app_id))
call("PATCH", f"/v1/appStoreVersions/{version_id}", body={"data": {
"type": "appStoreVersions",
"id": version_id,
Expand Down
31 changes: 28 additions & 3 deletions tests/test_app_store_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,10 @@ def __call__(self, request):
return 200, {"data": {"type": "appStoreVersionLocalizations", "id": "l1",
"attributes": {}}}
if path == "/v1/reviewSubmissions" and method == "GET":
states = request.query["filter[state]"].split(",")
return 200, {"data": [
{"type": "reviewSubmissions", "id": s, "attributes": {"state": v["state"]}}
for s, v in self.submissions.items()
for s, v in self.submissions.items() if v["state"] in states
]}
if path == "/v1/reviewSubmissions" and method == "POST":
self.submissions["rs-new"] = {"state": "READY_FOR_REVIEW", "items": []}
Expand Down Expand Up @@ -346,6 +347,13 @@ def __call__(self, request):
"Version is not ready to be submitted yet, "
"please try again later."}]}
submission = self.submissions[match.group(1)]
if body["data"]["attributes"].get("canceled"):
assert submission["state"] == "WAITING_FOR_REVIEW", submission
submission["state"] = "COMPLETE"
for version_id in submission["items"]:
self.versions[version_id]["state"] = "DEVELOPER_REJECTED"
return 200, {"data": {"type": "reviewSubmissions", "id": match.group(1),
"attributes": {"state": "CANCELING"}}}
submission["state"] = "WAITING_FOR_REVIEW"
for version_id in submission["items"]:
self.versions[version_id]["state"] = "WAITING_FOR_REVIEW"
Expand Down Expand Up @@ -443,14 +451,31 @@ def test_editable_version_gets_the_new_version_number(self):

def test_submission_in_review_blocks_a_new_one(self):
mock = AppStoreMock(
submissions={"rs1": {"state": "WAITING_FOR_REVIEW", "items": []}},
submissions={"rs1": {"state": "IN_REVIEW", "items": []}},
)
result, api = self.submit(mock, "build", "prod", notes="x")
self.assertNotEqual(result.returncode, 0)
self.assertIn("A submission is already WAITING_FOR_REVIEW", result.stderr)
self.assertIn("A submission is already IN_REVIEW", result.stderr)
self.assertNotIn("POST /v1/appStoreVersions", api.calls())
self.assertNotIn("POST /v1/reviewSubmissions", api.calls())

def test_new_release_replaces_a_version_waiting_for_review(self):
versions = copy.deepcopy(LIVE)
versions["v-wait"] = {"version": "4.2.1", "state": "WAITING_FOR_REVIEW",
"build": "b-old"}
mock = AppStoreMock(versions=versions, submissions={
"rs-wait": {"state": "WAITING_FOR_REVIEW", "items": ["v-wait"]},
})
result, api = self.submit(mock, "build", "prod", notes="Poprawki")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Withdrew the version waiting for review", result.stdout)
# The withdrawn version is reused for this one, with the new build.
self.assertNotIn("POST /v1/appStoreVersions", api.calls())
self.assertEqual(mock.versions["v-wait"]["version"], "4.3.0")
self.assertEqual(mock.versions["v-wait"]["build"], "b1")
self.assertEqual(mock.versions["v-wait"]["state"], "WAITING_FOR_REVIEW")
self.assertEqual(mock.submissions["rs-wait"]["state"], "COMPLETE")

def test_beta_becomes_a_public_testflight_beta(self):
mock = AppStoreMock(versions={
"v-older": {
Expand Down