diff --git a/.github/workflows/store-screenshots.yml b/.github/workflows/store-screenshots.yml index 29ef3fcb42..6f2b985583 100644 --- a/.github/workflows/store-screenshots.yml +++ b/.github/workflows/store-screenshots.yml @@ -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-` laid # out as `images/Screenshots/_.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/, 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 diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index eae0aa437c..66107cbd34 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -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 `` 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//changelogs/default.txt`, which `scripts/sync-play-changelog.py` renders from the metainfo `` 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-.zip`, `store-listing-screenshots-fdroid-.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-.zip`, `store-listing-screenshots-fdroid-.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. diff --git a/docs/en/developer/testing.md b/docs/en/developer/testing.md index 4b94987af2..ac0de1a147 100644 --- a/docs/en/developer/testing.md +++ b/docs/en/developer/testing.md @@ -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 diff --git a/store-screenshots/build.gradle.kts b/store-screenshots/build.gradle.kts index 136d43ffd4..d4d126c720 100644 --- a/store-screenshots/build.gradle.kts +++ b/store-screenshots/build.gradle.kts @@ -21,7 +21,7 @@ // PNGs are left on the device in /data/local/tmp/store-screenshots/, laid out like fastlane's images/; pull // them with // adb pull /data/local/tmp/store-screenshots//. -// .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) diff --git a/store-screenshots/capture-android.sh b/store-screenshots/capture-android.sh new file mode 100755 index 0000000000..d28c21a82f --- /dev/null +++ b/store-screenshots/capture-android.sh @@ -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 . +# +# Captures one flavor's store screenshots on the connected device from APKs already built +# by `:androidApp:assembleDebug :store-screenshots:assembleDebug`. +# +# capture-android.sh +# +# The PNGs land in /. The instrumentation output and the device logcat +# land in /test-results/, since no Gradle connected task writes a report. +set -euo pipefail + +FLAVOR=${1:?usage: capture-android.sh } +OUT=${2:?usage: capture-android.sh } +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"