From b03d20e0515327859b17f31d2e195f8737be80ec Mon Sep 17 00:00:00 2001 From: Usama <83345144+usamaiqb@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:54:28 +0500 Subject: [PATCH] ci: upload releases and the store listing to Google Play A tagged release now builds the bundle alongside the APK and uploads it to Play's closed testing track with fastlane supply, together with the R8 mapping file and everything under fastlane/metadata/android: the listing text, the graphics, the screenshots and the changelog for the new versionCode as release notes. These are the files F-Droid reads at the same tag, so both stores describe the same build. The upload is its own job in a `play` environment, so a Play failure never holds back the GitHub Release and the service account key is only exposed there. The track comes from the PLAY_TRACK variable and defaults to `alpha`, Play's default closed testing track. A manual workflow pushes the listing alone, for corrections between releases. supply anchors even a listing-only edit to a release on a track, so it is pointed at the testing track and the current versionCode. Release branches now also check the listing against Play's length limits, which would otherwise only fail the upload after the tag. fastlane is pinned in a Gemfile, with its dependencies locked. --- .github/workflows/play-listing.yml | 46 +++++ .github/workflows/release-checks.yml | 27 +++ .github/workflows/release.yml | 62 ++++++- .ruby-version | 1 + Gemfile | 5 + Gemfile.lock | 268 +++++++++++++++++++++++++++ 6 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/play-listing.yml create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/.github/workflows/play-listing.yml b/.github/workflows/play-listing.yml new file mode 100644 index 0000000..f972f7f --- /dev/null +++ b/.github/workflows/play-listing.yml @@ -0,0 +1,46 @@ +name: Play store listing + +# Pushes fastlane/metadata/android to Google Play without a release: the title, the +# descriptions, the graphics and the screenshots. A tagged release already uploads all of +# this with the bundle (release.yml); this is for correcting the listing between releases. +# +# Run it from the Actions tab. It reads the branch it is run on, so run it on main. +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + listing: + name: Upload store listing + runs-on: ubuntu-24.04 + environment: play + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0 + with: + bundler-cache: true + + # Release notes belong to a build, so they are left to the release upload. + # + # supply anchors even a listing-only edit to a release: it looks one up on the track, + # and fails if it finds none (the default track is production) or several. So it is + # pointed at the testing track and at the versionCode this branch was released as. + - name: Upload store listing + env: + SUPPLY_JSON_KEY_DATA: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + TRACK: ${{ vars.PLAY_TRACK || 'alpha' }} + run: | + VERSION_CODE=$(grep -oP 'versionCode\s*=\s*\K\d+' app/build.gradle.kts) + bundle exec fastlane supply \ + --package_name com.markreader \ + --track "$TRACK" \ + --version_code "$VERSION_CODE" \ + --metadata_path fastlane/metadata/android \ + --sync_image_upload true \ + --skip_upload_apk true \ + --skip_upload_aab true \ + --skip_upload_changelogs true diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index f927b3b..f2274ab 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -98,6 +98,33 @@ jobs: echo "--- contents ---" cat "$FILE" + # Google Play rejects the whole upload when one field is over its limit, and that + # upload runs only after the tag. Characters, not bytes: `wc -m` under a UTF-8 locale. + # supply sends a file as it is, so line breaks count; only the final one is dropped. + - name: Store listing fits Google Play's limits + env: + LC_ALL: C.UTF-8 + VERSION_CODE: ${{ steps.pr.outputs.version_code }} + run: | + FAIL=0 + check() { + [ -f "$1" ] || return 0 + LEN=$(printf '%s' "$(cat "$1")" | wc -m) + if [ "$LEN" -gt "$2" ]; then + echo "❌ $1 is $LEN characters; Google Play allows $2" + FAIL=1 + else + echo "✅ $1: $LEN / $2" + fi + } + for DIR in fastlane/metadata/android/*/; do + check "${DIR}title.txt" 30 + check "${DIR}short_description.txt" 80 + check "${DIR}full_description.txt" 4000 + check "${DIR}changelogs/${VERSION_CODE}.txt" 500 + done + exit $FAIL + # The table is normally kept current by translation-table.yml's bot PR; this # catches a release cut while that PR is still open. - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e37ca4..c676e66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,8 +33,10 @@ jobs: - name: Decode keystore run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > keystore.jks - - name: Build signed release APK - run: ./gradlew assembleRelease + # One Gradle run, one R8 pass: the APK (GitHub, F-Droid's comparison) and the bundle + # (Google Play) come from the same minified code, so they share a mapping file. + - name: Build signed release APK and bundle + run: ./gradlew assembleRelease bundleRelease env: KEYSTORE_PATH: ${{ github.workspace }}/keystore.jks KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} @@ -53,3 +55,59 @@ jobs: name: ${{ github.ref_name }} generate_release_notes: true files: app/build/outputs/apk/release/mark-reader-${{ github.ref_name }}.apk + + - name: Keep the bundle for the Play upload + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: play-bundle + path: | + app/build/outputs/bundle/release/*.aab + app/build/outputs/mapping/release/mapping.txt + if-no-files-found: error + retention-days: 7 + + # A separate job so a Play failure never holds back the GitHub Release, and so the service + # account key is only exposed here. The `play` environment can require an approval first. + play: + name: Upload to Google Play + needs: release + runs-on: ubuntu-24.04 + environment: play + permissions: + contents: read + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: play-bundle + path: play-bundle + + - uses: ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0 + with: + bundler-cache: true + + # Uploads the bundle to the testing track along with everything under + # fastlane/metadata/android: the listing text, the graphics, the screenshots and + # changelogs/.txt as the release notes. The same files F-Droid reads at + # this tag, so both stores describe the same build. + # + # PLAY_TRACK is a repository variable; `alpha` is Play's default closed testing track. + # A custom closed track is addressed by its name. + - name: Upload bundle and store listing + env: + SUPPLY_JSON_KEY_DATA: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + TRACK: ${{ vars.PLAY_TRACK || 'alpha' }} + run: | + AAB=$(find play-bundle -name '*.aab' | head -n 1) + MAPPING=$(find play-bundle -name 'mapping.txt' | head -n 1) + bundle exec fastlane supply \ + --package_name com.markreader \ + --track "$TRACK" \ + --release_status completed \ + --aab "$AAB" \ + --mapping "$MAPPING" \ + --metadata_path fastlane/metadata/android \ + --sync_image_upload true \ + --skip_upload_apk true diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..eb39e53 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..a9b3b10 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +# Uploads the bundle and the store listing to Google Play. Pinned exactly; the lockfile pins +# the rest. +gem "fastlane", "2.241.2" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..9e70620 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,268 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.2) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.1) + aws-partitions (1.1301.0) + aws-sdk-core (3.264.0) + aws-eventstream (~> 1, >= 1.4.1) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + rexml (~> 3.4, >= 3.4.2) + aws-sdk-kms (1.139.0) + aws-sdk-core (~> 3, >= 3.264.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.238.0) + aws-sdk-core (~> 3, >= 3.264.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.3) + cgi (0.5.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.7) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.7.20261031) + dotenv (2.8.1) + emoji_regex (3.2.3) + erb (6.0.7) + excon (1.10.0) + faraday (2.15.1) + faraday-net_http (>= 2.0, < 3.6) + json + logger + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (3.5.0) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) + fastimage (2.4.3) + fastlane (2.241.2) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + cgi (~> 0.4) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + erb (>= 4.0, < 7.0) + excon (>= 1.0.0, < 2.0.0) + faraday (~> 2.7) + faraday-cookie_jar (~> 0.0.8) + faraday-follow_redirects (~> 0.3) + faraday-multipart (~> 1.0) + faraday-retry (~> 2.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.99) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.5.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + irb (>= 1.8) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 3.4.0, < 4.0.0) + security (~> 0.3) + simctl (~> 1.6.3) + terminal-notifier (>= 3.1.0, < 4.0.0) + terminal-table (~> 4) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.111.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (1.4.1) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.21, >= 1.21.2) + representable (~> 3.3, >= 3.3.1) + retriable (>= 3.1, < 5.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.70.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.4.1) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.1) + google-cloud-storage (1.62.1) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.2) + googleauth (1.17.7) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.1.6) + domain_name (~> 0.5) + io-console (0.9.5) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.21.2) + jwt (3.3.0) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.2) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + net-http (0.10.0) + net-protocol (>= 0.3.0) + uri (>= 0.11.1) + net-protocol (0.4.0) + timeout + nkf (0.3.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.1) + prism (2.1.0) + prism (2.1.0-x86_64-linux-gnu) + pstore (0.3.0) + public_suffix (7.0.5) + rake (13.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.1.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.7.0) + io-console (~> 0.5) + representable (3.3.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (4.2.0) + rexml (3.4.4) + rouge (3.28.0) + rubyzip (3.7.0) + security (0.4.0) + signet (0.22.2) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (3.1.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + timeout (0.6.1) + trailblazer-option (0.1.2) + tsort (0.2.0) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) + uri (1.1.2) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + x86_64-linux-gnu + +DEPENDENCIES + fastlane (= 2.241.2) + +BUNDLED WITH + 2.5.22