ci(store-screenshots): capture each flavor on its own emulator without a second Gradle run (#7500)

This commit is contained in:
James Rich authored and GitHub committed 2026-09-30 17:25:44 +00:00
1 parent 503391e41f
commit d94166be1c
5 files changed
+88 -49

No files matched your search

+33 -46
View File
@@ -1,8 +1,8 @@
name: Store Screenshots
# Captures the store-listing screenshots from the real debug apps, connected to Demo
# Mode's hidden showcase mesh. Android runs `:store-screenshots` for both flavors on one
# emulator: the google flavor feeds the Play listing, the fdroid flavor the fastlane
# Mode's hidden showcase mesh. Android runs `:store-screenshots` once per flavor, each on
# its own emulator: the google flavor feeds the Play listing, the fdroid flavor the fastlane
# tree F-Droid and IzzyOnDroid read, each uploaded as `store-screenshots-<flavor>` laid
# out as `images/<type>Screenshots/<n>_<name>.png`. Desktop runs the real app on a virtual
# display (`store-screenshots/capture-desktop.sh`) and uploads the five Flathub PNGs as
@@ -45,11 +45,19 @@ concurrency:
jobs:
android:
name: Android
name: Android (${{ matrix.flavor }})
# Hosted x64 runners expose KVM; the emulator needs it.
runs-on: ubuntu-26.04
timeout-minutes: 35
continue-on-error: ${{ inputs.soft == true }}
strategy:
fail-fast: false
matrix:
include:
- flavor: google
variant: Google
- flavor: fdroid
variant: Fdroid
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -67,6 +75,7 @@ jobs:
# The google flavor draws Google Maps, which needs the debug Maps key; the fdroid
# flavor draws MapLibre and needs nothing.
- name: Provide the debug Maps key
if: ${{ matrix.flavor == 'google' }}
env:
GOOGLE_MAPS_API_KEY_DEBUG: ${{ secrets.GOOGLE_MAPS_API_KEY_DEBUG }}
run: |
@@ -76,11 +85,10 @@ jobs:
echo "::warning::GOOGLE_MAPS_API_KEY_DEBUG is unset -- the google-flavor map shot will be blank."
fi
# Built before the emulator boots, so the emulator step only installs and runs.
# Built before the emulator boots, so the emulator step only installs and runs, with
# no second Gradle configuration.
- name: Build the app and the capture module
run: >
./gradlew :androidApp:assembleGoogleDebug :store-screenshots:assembleGoogleDebug
:androidApp:assembleFdroidDebug :store-screenshots:assembleFdroidDebug
run: ./gradlew :androidApp:assemble${{ matrix.variant }}Debug :store-screenshots:assemble${{ matrix.variant }}Debug
- name: Enable KVM (for the emulator)
run: |
@@ -89,12 +97,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# The runner's `script:` runs each line in its own shell and stops at the first that
# fails, so a failure is recorded in out/failed and both flavors are still pulled.
# Each flavor leaves its PNGs in /data/local/tmp/store-screenshots/<flavor>, which
# outlives the test app's uninstall at the end of the connected run.
# --no-configuration-cache: connected tasks in a com.android.test module are not
# configuration-cache serializable (see scheduled-baseline.yml).
# The runner's `script:` runs each line in its own shell, so the capture is one script.
- name: Capture on the emulator
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
with:
@@ -105,54 +108,38 @@ jobs:
cores: 4
disable-animations: true
emulator-options: -no-window -gpu swiftshader -noaudio -no-boot-anim -camera-back none
script: |
mkdir -p out/google out/fdroid
./gradlew --continue :store-screenshots:connectedGoogleDebugAndroidTest :store-screenshots:connectedFdroidDebugAndroidTest -Dorg.gradle.isolated-projects=false --no-configuration-cache || touch out/failed
adb pull /data/local/tmp/store-screenshots/google/. out/google/ || touch out/failed
adb pull /data/local/tmp/store-screenshots/fdroid/. out/fdroid/ || touch out/failed
test ! -e out/failed
script: store-screenshots/capture-android.sh ${{ matrix.flavor }} out
- name: Summarize the captures
if: ${{ always() }}
env:
FLAVOR: ${{ matrix.flavor }}
run: |
for flavor in google fdroid; do
{
echo "### Store screenshots (${flavor})"
if [ -n "$(find "out/$flavor" -name '*.png' 2>/dev/null)" ]; then
find "out/$flavor" -name '*.png' | sort | sed "s|^out/$flavor/|- |"
else
echo "- none captured"
fi
} >> "$GITHUB_STEP_SUMMARY"
done
{
echo "### Store screenshots (${FLAVOR})"
if [ -n "$(find "out/$FLAVOR" -name '*.png' 2>/dev/null)" ]; then
find "out/$FLAVOR" -name '*.png' | sort | sed "s|^out/$FLAVOR/|- |"
else
echo "- none captured"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the google captures
- name: Upload the captures
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: store-screenshots-google
path: out/google
name: store-screenshots-${{ matrix.flavor }}
path: out/${{ matrix.flavor }}
if-no-files-found: warn
retention-days: 7
- name: Upload the fdroid captures
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: store-screenshots-fdroid
path: out/fdroid
if-no-files-found: warn
retention-days: 7
# The failing test's exception and the device logcat live only in these directories.
# The failing test's exception is in instrument.txt, the rest of the story in logcat.txt.
- name: Upload the test results
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: store-screenshots-test-results
path: |
store-screenshots/build/outputs/androidTest-results/connected
store-screenshots/build/reports/androidTests/connected
name: store-screenshots-test-results-${{ matrix.flavor }}
path: out/test-results/${{ matrix.flavor }}
if-no-files-found: warn
retention-days: 7
+1 -1
View File
@@ -21,7 +21,7 @@ The entire release process is managed by a single GitHub Action: **`Create or Pr
- **Generates Changelog:** Categorizes merged PRs by their labels (per `.github/release.yml`) into GitHub's auto-generated release notes. The internal draft's notes cover the PRs since the previous published pre-release; a production promotion rewrites them over the whole range since the previous production tag, with the metainfo `<description>` for the version as a Highlights section on top, and opens a PR folding the same notes into `CHANGELOG.md`. Every internal, closed and open run ends by dispatching the `Update Changelog` workflow, which refreshes that file's `[Unreleased]` section through its own PR. Every PR the release automation opens (these two, the screenshot refresh and the version bump) goes through `.github/actions/bot-pr` with `CROWDIN_GITHUB_TOKEN`, so it runs the normal PR checks and merges itself through the queue once they pass.
- **Tags & Builds** *(internal releases)*: Pushes the incremental tag first — there is no lint/test gate in this workflow, that's the separate PR/CI pipeline — then builds the Android bundle/APK and Desktop installers from that tag; if the build fails, an automatic cleanup job deletes the tag so a retry starts clean. Once `publish-play` has uploaded the bundle the tag stays, since Play keeps that versionCode; re-run the failed jobs instead. Promotions skip this entirely and retag the already-built artifact (see below).
- **Deploys Android:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release. An internal cut sends the bundle to Play only after every Android, desktop and Flatpak leg has built, so a failed leg and its deleted tag leave nothing on Play. Each promotion also uploads the Play "What's new" text for every locale from `fastlane/metadata/android/<locale>/changelogs/default.txt`, which `scripts/sync-play-changelog.py` renders from the metainfo `<description>` and Crowdin translates.
- **Captures the store screenshots** *(internal releases)*: `store-screenshots.yml` runs the real debug apps from the tag, connected to Demo Mode's showcase mesh, on one emulator for both flavors and on a virtual display for desktop, and attaches `store-listing-screenshots-google-<versionCode>.zip`, `store-listing-screenshots-fdroid-<versionCode>.zip` and the five desktop PNGs to the draft once it exists, so the draft does not wait on the capture. A set with a shot missing is replaced whole by the committed one, so the metainfo's screenshot URLs still resolve, and the job summary says which set went up. A failed capture never fails the release.
- **Captures the store screenshots** *(internal releases)*: `store-screenshots.yml` runs the real debug apps from the tag, connected to Demo Mode's showcase mesh, on one emulator per flavor and on a virtual display for desktop, and attaches `store-listing-screenshots-google-<versionCode>.zip`, `store-listing-screenshots-fdroid-<versionCode>.zip` and the five desktop PNGs to the draft once it exists, so the draft does not wait on the capture. A set with a shot missing is replaced whole by the committed one, so the metainfo's screenshot URLs still resolve, and the job summary says which set went up. A failed capture never fails the release.
- **Publishes the Play listing:** every promotion runs the `play_listing` lane with the tag's text for every locale and the google-flavor screenshots, as a dry run on closed and open and for real on production, held as "changes not sent for review". Production also opens a self-merging PR that writes the fdroid-flavor screenshots back into `fastlane/`, which F-Droid and IzzyOnDroid read from git, and the desktop set into `desktopApp/packaging/linux/screenshots/`.
- **Publishes docs:** Every promotion dispatches `docs-release.yml` on the new tag (the tag is created with `GITHUB_TOKEN`, so its tag trigger never fires on its own).
- **Refreshes the Obtainium table:** Every promotion dispatches `scheduled-updates.yml` once the release is published, so the Obtainium table in `README.md` follows it without waiting for the scheduled run.
+1 -1
View File
@@ -93,7 +93,7 @@ adb pull /data/local/tmp/store-screenshots/fdroid/. fastlane/metadata/android/en
| `tenInchScreenshots/` | 2560×1440 @320 dpi | expanded: rail, list beside detail | `fastlane supply` |
| `desktopApp/packaging/linux/screenshots/` | 1280×800 | expanded: rail, list beside detail | Flathub, through the release assets `metainfo.xml` names |
`.github/workflows/store-screenshots.yml` runs both on hosted runners, with both Android flavors in one job on one emulator (google for the Play listing, fdroid for the committed tree), on every internal release, on demand, and on pull requests that touch the renderer or the showcase mesh. The release pipeline attaches the captures to the release, publishes the Play listing from them on production, and opens a self-merging PR that writes the fdroid and desktop sets back here (`RELEASE_PROCESS.md`).
`.github/workflows/store-screenshots.yml` runs both on hosted runners, each Android flavor in its own job on its own emulator through `store-screenshots/capture-android.sh` (google for the Play listing, fdroid for the committed tree), on every internal release, on demand, and on pull requests that touch the renderer or the showcase mesh. The release pipeline attaches the captures to the release, publishes the Play listing from them on production, and opens a self-merging PR that writes the fdroid and desktop sets back here (`RELEASE_PROCESS.md`).
### Baseline Profile / startup performance
+1 -1
View File
@@ -21,7 +21,7 @@
// PNGs are left on the device in /data/local/tmp/store-screenshots/<flavor>, laid out like fastlane's images/; pull
// them with
// adb pull /data/local/tmp/store-screenshots/<flavor>/. <dir>
// .github/workflows/store-screenshots.yml does this for both flavors on one emulator.
// .github/workflows/store-screenshots.yml runs each flavor on its own emulator through capture-android.sh.
plugins {
alias(libs.plugins.meshtastic.android.test)
alias(libs.plugins.meshtastic.detekt)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
#
# Copyright (c) 2026 Meshtastic LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Captures one flavor's store screenshots on the connected device from APKs already built
# by `:androidApp:assemble<Flavor>Debug :store-screenshots:assemble<Flavor>Debug`.
#
# capture-android.sh <google|fdroid> <output dir>
#
# The PNGs land in <output dir>/<flavor>. The instrumentation output and the device logcat
# land in <output dir>/test-results/<flavor>, since no Gradle connected task writes a report.
set -euo pipefail
FLAVOR=${1:?usage: capture-android.sh <google|fdroid> <output dir>}
OUT=${2:?usage: capture-android.sh <google|fdroid> <output dir>}
SHOTS="$OUT/$FLAVOR"
RESULTS="$OUT/test-results/$FLAVOR"
DEVICE_OUTPUT=/data/local/tmp/store-screenshots/$FLAVOR
mkdir -p "$SHOTS" "$RESULTS"
app_apk=$(find "androidApp/build/outputs/apk/$FLAVOR/debug" -name '*-universal-debug.apk' | head -1)
test_apk=$(find "store-screenshots/build/outputs/apk/$FLAVOR/debug" -name '*.apk' | head -1)
[ -n "$app_apk" ] && [ -n "$test_apk" ] || { echo "::error::$FLAVOR APKs not built"; exit 1; }
adb install -r -t "$app_apk"
adb install -r -t "$test_apk"
runner=$(adb shell pm list instrumentation | tr -d '\r' | sed -n 's/^instrumentation:\(org\.meshtastic\.storescreenshots\/[^ ]*\).*/\1/p' | head -1)
[ -n "$runner" ] || { echo "::error::no store-screenshots instrumentation on the device"; exit 1; }
adb logcat -c || true
# am instrument exits 0 whatever the test does; the verdict is its "OK (1 test)" line.
adb shell am instrument -w -r \
-e targetAppId "com.geeksville.mesh.$FLAVOR.debug" \
-e flavor "$FLAVOR" \
"$runner" | tr -d '\r' | tee "$RESULTS/instrument.txt"
adb logcat -d > "$RESULTS/logcat.txt"
adb pull "$DEVICE_OUTPUT/." "$SHOTS/" || true
grep -q '^OK (1 test)' "$RESULTS/instrument.txt"