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
26 changes: 22 additions & 4 deletions .github/workflows/flutter-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ on:
type: string
default: beta
publish-track:
description: "Manual runs from a release's tag: `production` or the open testing track (play-beta-track)."
description: "Manual runs from a release's tag: `production` or the open testing track (play-beta-track). `listing`, from any tag, only replaces the Google Play screenshots (screenshots-task)."
type: string
publish-platforms:
description: "Manual runs: `all` (the default), `android` or `ios`."
Expand Down Expand Up @@ -134,6 +134,23 @@ jobs:
exit 1
;;
esac
if [ "$EVENT" = workflow_dispatch ] && [ "$TRACK" = listing ]; then
# The store listing alone: no version, release or build.
if [ "$PLATFORMS" = ios ]; then
echo "::error::App Store screenshots change only with a new App Store version: publish a production release."
exit 1
fi
echo "::notice::App Store screenshots change only with a new App Store version, so the next production release updates them."
{
echo "mode=listing"
echo "manual=true"
echo "channel=listing"
echo "android=true"
echo "ios=false"
echo "tag=$TAG"
} >> "$GITHUB_OUTPUT"
exit 0
fi
# [v]X.Y.Z, or [v]X.Y.Z-<label><N> such as 1.2.0-beta.3 or 1.2.0-rc2;
# the label is free text, only N counts.
semver='^v?(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]?)\.(0|[1-9][0-9]?)(-([0-9A-Za-z.-]*[A-Za-z.-])?([1-9][0-9]?))?$'
Expand Down Expand Up @@ -480,7 +497,8 @@ jobs:
if: >-
${{ !cancelled() && inputs.android-package != '' && vars.GOOGLE_PLAY_SERVICE_ACCOUNT != ''
&& needs.plan.outputs.android == 'true' && needs.screenshots.result != 'failure'
&& (needs.publish.result == 'success' || needs.promote.result == 'success') }}
&& (needs.publish.result == 'success' || needs.promote.result == 'success'
|| needs.plan.outputs.mode == 'listing') }}
runs-on: ubuntu-24.04
timeout-minutes: 30
environment: ${{ inputs.environment }}
Expand Down Expand Up @@ -785,8 +803,8 @@ jobs:
# Rendered from the tag for the store listings, which go with production.
needs: plan
if: >-
${{ inputs.screenshots-task != '' && needs.plan.outputs.channel == 'prod'
&& needs.plan.outputs.mode != 'skip' }}
${{ inputs.screenshots-task != '' && (needs.plan.outputs.mode == 'listing'
|| needs.plan.outputs.channel == 'prod' && needs.plan.outputs.mode != 'skip') }}
permissions:
contents: read # clone the repository
uses: $/.github/workflows/flutter-screenshots.yml
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ them.
devices `phone`, `tablet-7` and `tablet-10`, and the App Store version for
`iphone-6.9` (the required large iPhone size, 1320×2868) and `ipad-13`
(2064×2752). Testing tracks keep the listing.
A manual run with the track `listing`, from any tag, replaces only the
Google Play screenshots (Google reviews the listing). App Store screenshots
change only with a new App Store version, so production releases update
them.
- **Release notes:** the release description becomes the store release notes
(Google Play: up to 500 characters; App Store: "What's New" in every
language).
Expand Down
18 changes: 16 additions & 2 deletions scripts/google_play.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ def screenshot_folder(device, language):


def update_screenshots(edit):
"""Replaces the listings' screenshots that differ from SCREENSHOTS_DIR."""
"""Replaces the listings' screenshots that differ from SCREENSHOTS_DIR;
returns how many image sets it replaced."""
replaced = 0
listings = call("GET", f"{API}/edits/{edit}/listings").get("listings", [])
for language in sorted(listing["language"] for listing in listings):
for device, image_type in SCREENSHOT_TYPES.items():
Expand All @@ -147,14 +149,26 @@ def update_screenshots(edit):
f"{image_type}?uploadType=media", data=image,
content_type="image/png")
print(f"Replaced the {language} {image_type} with {folder}.")
replaced += 1
return replaced


def main():
mode = os.environ["MODE"]
tag = os.environ["TAG"]
edit = call("POST", f"{API}/edits", body={})["id"]
if mode == "listing":
# The store listing alone, sent for review if anything changed.
if not os.environ.get("SCREENSHOTS_DIR"):
sys.exit("No screenshots to publish: set screenshots-task.")
if update_screenshots(edit):
commit(edit)
else:
call("DELETE", f"{API}/edits/{edit}")
print("The store screenshots are up to date.")
return
version_code = os.environ["VERSION_CODE"]
notes = release_notes(tag)
edit = call("POST", f"{API}/edits", body={})["id"]

track = TRACK
if mode == "build":
Expand Down
37 changes: 35 additions & 2 deletions tests/test_google_play.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def __call__(self, request):
assert body["track"] == track
self.puts.append(body)
return 200, body
if request.method == "DELETE" and path == f"{EDITS}/e1":
return 204, None
if request.method == "GET" and path == f"{EDITS}/e1/listings":
return 200, {"listings": [
{"language": language, "title": "App"} for language in self.images
Expand Down Expand Up @@ -300,8 +302,35 @@ def test_promote_names_the_release_after_the_version(self):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(mock.puts[0]["releases"][0]["name"], "4.3.0")

if __name__ == "__main__":
unittest.main()
def test_listing_replaces_the_screenshots_alone(self):
root = self.screenshots({
"phone/en/01-schedule.png": b"en 1",
"phone/pl/01-schedule.png": b"pl 1",
})
mock = PlayMock(images={"en-US": {"phoneScreenshots": [b"old"]}, "pl-PL": {}})
result, api = self.run_play(
mock, MODE="listing", TRACK="", TAG="store-listing", SCREENSHOTS_DIR=root
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(mock.images["en-US"]["phoneScreenshots"], [b"en 1"])
self.assertEqual(mock.images["pl-PL"]["phoneScreenshots"], [b"pl 1"])
self.assertEqual(mock.puts, [])
self.assertEqual(api.calls()[-1], f"POST {EDITS}/e1:commit")

def test_listing_without_changes_drops_the_edit(self):
root = self.screenshots({"phone/en/01-schedule.png": b"same"})
mock = PlayMock(images={"en-US": {"phoneScreenshots": [b"same"]}})
result, api = self.run_play(
mock, MODE="listing", TRACK="", TAG="store-listing", SCREENSHOTS_DIR=root
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("up to date", result.stdout)
self.assertEqual(api.calls()[-1], f"DELETE {EDITS}/e1")

def test_listing_without_screenshots_fails(self):
result, api = self.run_play(PlayMock(), MODE="listing", TRACK="", TAG="x")
self.assertNotEqual(result.returncode, 0)
self.assertIn("screenshots-task", result.stderr)

def screenshots(self, files):
"""A screenshots folder with {"<device>/<language>/<name>": contents}."""
Expand Down Expand Up @@ -346,3 +375,7 @@ def test_testing_tracks_keep_the_listing(self):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn(f"GET {EDITS}/e1/listings", api.calls())
self.assertEqual(mock.images["en-US"]["phoneScreenshots"], [b"old"])


if __name__ == "__main__":
unittest.main()