|
| 1 | +name: Release Android to Play Store (internal testing) |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + inputs: |
| 6 | + publish: |
| 7 | + description: 'Actually upload to Play Console internal track (unchecked = build + sign only, nothing leaves this workflow run)' |
| 8 | + type: boolean |
| 9 | + required: true |
| 10 | + default: false |
| 11 | + |
| 12 | +concurrency: |
| 13 | + group: release-playstore |
| 14 | + cancel-in-progress: false |
| 15 | + |
| 16 | +jobs: |
| 17 | + release-android: |
| 18 | + if: github.ref == 'refs/heads/master' |
| 19 | + runs-on: ubuntu-22.04 |
| 20 | + |
| 21 | + steps: |
| 22 | + - name: Checkout repository |
| 23 | + uses: actions/checkout@v3 |
| 24 | + with: |
| 25 | + token: ${{ secrets.GITHUB_TOKEN }} |
| 26 | + submodules: 'false' |
| 27 | + |
| 28 | + - name: Checkout submodules with specific branches |
| 29 | + run: | |
| 30 | + git submodule init |
| 31 | + git submodule update --init --recursive |
| 32 | +
|
| 33 | + - name: Install packages required to run QZ inside workflow |
| 34 | + run: sudo apt update -y && sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev-tools qtquickcontrols2-5-dev libqt5bluetooth5 libqt5widgets5 libqt5positioning5 libqt5xml5 qtconnectivity5-dev qtpositioning5-dev libqt5charts5-dev libqt5charts5 libqt5networkauth5-dev libqt5websockets5* libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev |
| 35 | + |
| 36 | + - name: Install Qt Android |
| 37 | + uses: jdpurcell/install-qt-action@v5 |
| 38 | + with: |
| 39 | + version: '5.15.0' |
| 40 | + host: 'linux' |
| 41 | + target: 'android' |
| 42 | + arch: 'android' |
| 43 | + modules: 'qtcharts qtnetworkauth qtpurchasing' |
| 44 | + dir: '${{ github.workspace }}/output/android/' |
| 45 | + cache: 'false' |
| 46 | + |
| 47 | + - name: Install custom 16K-page-size Qt (overlay onto stock Qt install) |
| 48 | + run: | |
| 49 | + QT_DIR="${{ github.workspace }}/output/android/Qt/5.15.0/android" |
| 50 | + curl -fsSL -o /tmp/qt5.15.0-android-page16.zip https://github.com/cagnulein/qt-5.15.0-android-page16k/releases/download/1.0.0/qt5.15.0-android-page16.zip |
| 51 | + unzip -q /tmp/qt5.15.0-android-page16.zip -d /tmp/qt-page16k |
| 52 | + # Overlay the 16K-page-aligned target libs/plugins/qml/mkspecs/jar/include onto the |
| 53 | + # stock Qt install; keep the stock Linux host tools (bin/) since the custom Qt's |
| 54 | + # host tools were built on macOS (Mach-O, not usable on this Linux runner). |
| 55 | + rsync -a --exclude='bin/' "/tmp/qt-page16k/qt5.15.0-android-page16/usr/local/Qt-5.15.0/" "$QT_DIR/" |
| 56 | + # qdevice.pri comes from the overlay carrying the macOS origin's NDK host value; |
| 57 | + # fix it back to linux so qmake resolves the NDK toolchain path correctly. |
| 58 | + sed -i 's/darwin-x86_64/linux-x86_64/' "$QT_DIR/mkspecs/qdevice.pri" |
| 59 | + rm -rf /tmp/qt5.15.0-android-page16.zip /tmp/qt-page16k |
| 60 | +
|
| 61 | + - name: Install Java |
| 62 | + uses: actions/setup-java@v4 |
| 63 | + with: |
| 64 | + distribution: 'temurin' |
| 65 | + java-version: '17' |
| 66 | + |
| 67 | + - name: patching qt for bluetooth |
| 68 | + run: cp qt-patches/android/5.15.0/jar/*.* ${{ github.workspace }}/output/android/Qt/5.15.0/android/jar/ |
| 69 | + |
| 70 | + - name: download 3rd party files for qthttpserver |
| 71 | + run: cp qHttpServerBin/5.15.2/headers/* src/qthttpserver/src/3rdparty/http-parser/ |
| 72 | + |
| 73 | + - name: Cache Android NDK |
| 74 | + id: cache-ndk |
| 75 | + uses: actions/cache@v4 |
| 76 | + with: |
| 77 | + path: /usr/local/lib/android/sdk/ndk/21.1.6352462 |
| 78 | + key: android-ndk-21.1.6352462-${{ runner.os }} |
| 79 | + |
| 80 | + - name: Install Android SDK packages |
| 81 | + run: | |
| 82 | + SDKMANAGER="/usr/local/lib/android/sdk/cmdline-tools/latest/bin/sdkmanager" |
| 83 | + echo "y" | $SDKMANAGER "ndk;21.1.6352462" "platforms;android-35" "build-tools;35.0.0" |
| 84 | +
|
| 85 | + - name: Setup ccache |
| 86 | + run: | |
| 87 | + sudo apt-get install -y ccache |
| 88 | + echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV |
| 89 | + ccache --set-config=max_size=2G |
| 90 | + ccache --set-config=compression=true |
| 91 | + ccache --set-config=compression_level=6 |
| 92 | + ccache --set-config=base_dir=${{ github.workspace }} |
| 93 | + ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,file_macro |
| 94 | + ccache --zero-stats |
| 95 | +
|
| 96 | + - name: Restore ccache |
| 97 | + uses: actions/cache@v4 |
| 98 | + with: |
| 99 | + path: .ccache |
| 100 | + key: ccache-android-ndk21_1-${{ hashFiles('src/**/*.cpp', 'src/**/*.h', 'src/*.pro') }} |
| 101 | + restore-keys: | |
| 102 | + ccache-android-ndk21_1- |
| 103 | +
|
| 104 | + - name: Setup NDK environment with ccache |
| 105 | + run: | |
| 106 | + NDK_PATH=/usr/local/lib/android/sdk/ndk/21.1.6352462 |
| 107 | + NDK_BIN=$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin |
| 108 | + ln -sfn $NDK_PATH /usr/local/lib/android/sdk/ndk-bundle |
| 109 | + rm -rf /usr/local/lib/android/sdk/ndk/25.1.8937393 |
| 110 | + echo "ANDROID_NDK=/usr/local/lib/android/sdk/ndk-bundle" >> $GITHUB_ENV |
| 111 | + echo "ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk-bundle" >> $GITHUB_ENV |
| 112 | + if [ ! -f "$NDK_BIN/clang.real" ]; then |
| 113 | + mv $NDK_BIN/clang $NDK_BIN/clang.real |
| 114 | + mv $NDK_BIN/clang++ $NDK_BIN/clang++.real |
| 115 | + fi |
| 116 | + printf '#!/bin/bash\nexec /usr/bin/ccache %s/clang.real "$@"\n' "$NDK_BIN" > $NDK_BIN/clang |
| 117 | + printf '#!/bin/bash\nexec /usr/bin/ccache %s/clang++.real "$@"\n' "$NDK_BIN" > $NDK_BIN/clang++ |
| 118 | + chmod +x $NDK_BIN/clang $NDK_BIN/clang++ |
| 119 | +
|
| 120 | + - name: Generate secrets |
| 121 | + run: | |
| 122 | + cd src |
| 123 | + echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h |
| 124 | + echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h |
| 125 | + echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h |
| 126 | + echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h |
| 127 | + echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h |
| 128 | + echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h |
| 129 | + echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h |
| 130 | + echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js |
| 131 | +
|
| 132 | + - name: Prepare Android signing key |
| 133 | + env: |
| 134 | + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} |
| 135 | + run: | |
| 136 | + if [ -z "$SIGNING_KEY" ] || [ -z "${{ secrets.ALIAS }}" ] || [ -z "${{ secrets.KEY_STORE_PASSWORD }}" ] || [ -z "${{ secrets.KEY_PASSWORD }}" ]; then |
| 137 | + echo "Android signing secrets are required to publish a release build; aborting." |
| 138 | + exit 1 |
| 139 | + fi |
| 140 | + KEYSTORE="${{ github.workspace }}/src/android/ci-signing-key.jks" |
| 141 | + printf '%s' "$SIGNING_KEY" | base64 --decode > "$KEYSTORE" |
| 142 | + if [ ! -s "$KEYSTORE" ]; then |
| 143 | + echo "Failed to decode Android signing key." |
| 144 | + rm -f "$KEYSTORE" |
| 145 | + exit 1 |
| 146 | + fi |
| 147 | + echo "CI_ANDROID_SIGNING_KEYSTORE=$KEYSTORE" >> "$GITHUB_ENV" |
| 148 | +
|
| 149 | + - name: Compute next Play Store versionCode |
| 150 | + id: next_version |
| 151 | + env: |
| 152 | + PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} |
| 153 | + run: | |
| 154 | + echo "$PLAY_SERVICE_ACCOUNT_JSON" > /tmp/play-sa.json |
| 155 | + NEXT_VC=$(python3 scripts/play_next_version_code.py --sa-json /tmp/play-sa.json) |
| 156 | + rm -f /tmp/play-sa.json |
| 157 | + echo "version_code=$NEXT_VC" >> "$GITHUB_OUTPUT" |
| 158 | + echo "Computed next versionCode: $NEXT_VC" |
| 159 | +
|
| 160 | + - name: Bump Android versionCode/versionName for this release (ephemeral, never committed) |
| 161 | + run: | |
| 162 | + NEXT_VC="${{ steps.next_version.outputs.version_code }}" |
| 163 | + BASE_VERSION_NAME=$(grep -oE 'android:versionName="[^"]+"' src/android/AndroidManifest.xml | sed -E 's/.*"(.*)"/\1/') |
| 164 | + NEW_VERSION_NAME="${BASE_VERSION_NAME}-internal.${{ github.run_number }}" |
| 165 | + sed -i "s/android:versionCode=\"[0-9]*\"/android:versionCode=\"$NEXT_VC\"/" src/android/AndroidManifest.xml |
| 166 | + sed -i "s/android:versionName=\"[^\"]*\"/android:versionName=\"$NEW_VERSION_NAME\"/" src/android/AndroidManifest.xml |
| 167 | + echo "Using versionCode=$NEXT_VC versionName=$NEW_VERSION_NAME (this change is not committed back to the repo)" |
| 168 | +
|
| 169 | + - name: Build qthttpserver for Android |
| 170 | + run: | |
| 171 | + cd src/qthttpserver |
| 172 | + qmake |
| 173 | + make -j8 |
| 174 | + make install |
| 175 | +
|
| 176 | + - name: Build QZ for Android (4 ABIs) |
| 177 | + run: | |
| 178 | + lrelease src/qdomyos-zwift.pri |
| 179 | + # CONFIG+=debug matches the validated reference build: keeps .so unstripped and, |
| 180 | + # with this custom Qt, naturally leaves android-extra-libs unset - the bundled |
| 181 | + # android_openssl libs are not 16K-page-aligned, so we must NOT inject them. |
| 182 | + qmake -spec android-clang 'ANDROID_ABIS=armeabi-v7a arm64-v8a x86 x86_64' 'ANDROID_NDK_ROOT=/usr/local/lib/android/sdk/ndk/21.1.6352462' 'CONFIG+=debug' && make -j4 && make INSTALL_ROOT=${{ github.workspace }}/output/android/ install |
| 183 | + cat src/android-qdomyos-zwift-deployment-settings.json |
| 184 | +
|
| 185 | + - name: Cache Gradle |
| 186 | + uses: actions/cache@v4 |
| 187 | + with: |
| 188 | + path: | |
| 189 | + ${{ github.workspace }}/.gradle-android/caches |
| 190 | + ${{ github.workspace }}/.gradle-android/wrapper |
| 191 | + key: gradle-android-${{ runner.os }}-v2 |
| 192 | + restore-keys: | |
| 193 | + gradle-android-${{ runner.os }}- |
| 194 | +
|
| 195 | + - name: Build Android package (release) |
| 196 | + run: | |
| 197 | + cd src |
| 198 | + mkdir -p ${{ github.workspace }}/output/android |
| 199 | + rsync -a --exclude build --exclude .gradle android/ ${{ github.workspace }}/output/android/ |
| 200 | + androiddeployqt --input android-qdomyos-zwift-deployment-settings.json --output ${{ github.workspace }}/output/android/ --android-platform android-35 --gradle --aux-mode |
| 201 | + export GRADLE_USER_HOME="${{ github.workspace }}/.gradle-android" |
| 202 | + export GRADLE_OPTS="-Dgradle.user.home=$GRADLE_USER_HOME" |
| 203 | + export GRADLE_DIST_DIR="${{ github.workspace }}/gradle-8.13" |
| 204 | + export QT_ANDROID_DIR="${{ github.workspace }}/output/android/Qt/5.15.0/android" |
| 205 | + mkdir -p "$GRADLE_USER_HOME" |
| 206 | + rm -f "$GRADLE_USER_HOME/gradle.properties" |
| 207 | + rm -f "$HOME/.gradle/gradle.properties" |
| 208 | + find ${{ github.workspace }}/output/android \( -name '*.properties' -o -name '*.gradle' -o -name '*.gradle.kts' \) -exec sed -i '/android\.bundle\.enableUncompressedNativeLibs/d' {} + |
| 209 | + printf '\nqt5AndroidDir=%s/src/android/java\n' "$QT_ANDROID_DIR" >> ${{ github.workspace }}/output/android/gradle.properties |
| 210 | + if grep -RInE "android\\.bundle\\.enableUncompressedNativeLibs|enableUncompressedNativeLibs" ${{ github.workspace }}/output/android "$GRADLE_USER_HOME" "$HOME/.gradle" 2>/dev/null; then |
| 211 | + echo "Unexpected residual enableUncompressedNativeLibs matches found" |
| 212 | + exit 1 |
| 213 | + fi |
| 214 | + if [ ! -x "$GRADLE_DIST_DIR/bin/gradle" ]; then |
| 215 | + curl -fsSL https://services.gradle.org/distributions/gradle-8.13-bin.zip -o /tmp/gradle-8.13-bin.zip |
| 216 | + unzip -q /tmp/gradle-8.13-bin.zip -d ${{ github.workspace }} |
| 217 | + fi |
| 218 | + "$GRADLE_DIST_DIR/bin/gradle" -g "$GRADLE_USER_HOME" --no-daemon -p ${{ github.workspace }}/output/android assembleRelease bundleRelease |
| 219 | +
|
| 220 | + - name: Sign release APK and AAB |
| 221 | + run: | |
| 222 | + APK_PATH="${{ github.workspace }}/output/android/build/outputs/apk/release/android-release-unsigned.apk" |
| 223 | + AAB_PATH="${{ github.workspace }}/output/android/build/outputs/bundle/release/android-release.aab" |
| 224 | + APKSIGNER=$(find /usr/local/lib/android/sdk/build-tools -name apksigner | sort | tail -n 1) |
| 225 | + "$APKSIGNER" sign \ |
| 226 | + --ks "$CI_ANDROID_SIGNING_KEYSTORE" \ |
| 227 | + --ks-key-alias "${{ secrets.ALIAS }}" \ |
| 228 | + --ks-pass "pass:${{ secrets.KEY_STORE_PASSWORD }}" \ |
| 229 | + --key-pass "pass:${{ secrets.KEY_PASSWORD }}" \ |
| 230 | + --out "${{ github.workspace }}/output/android-release-signed.apk" \ |
| 231 | + "$APK_PATH" |
| 232 | + jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \ |
| 233 | + -keystore "$CI_ANDROID_SIGNING_KEYSTORE" \ |
| 234 | + -storepass "${{ secrets.KEY_STORE_PASSWORD }}" \ |
| 235 | + -keypass "${{ secrets.KEY_PASSWORD }}" \ |
| 236 | + -signedjar "${{ github.workspace }}/output/android-release-signed.aab" \ |
| 237 | + "$AAB_PATH" "${{ secrets.ALIAS }}" |
| 238 | + rm -f "$CI_ANDROID_SIGNING_KEYSTORE" |
| 239 | +
|
| 240 | + - name: Verify 16K page-size alignment |
| 241 | + run: | |
| 242 | + NDK_LIBDIR=/usr/local/lib/android/sdk/ndk/21.1.6352462/toolchains/llvm/prebuilt/linux-x86_64/bin |
| 243 | + FAIL=0 |
| 244 | + OK=0 |
| 245 | + WORKDIR=$(mktemp -d) |
| 246 | + unzip -q "${{ github.workspace }}/output/android-release-signed.apk" -d "$WORKDIR" |
| 247 | + while IFS= read -r -d '' so; do |
| 248 | + ALIGN=$("$NDK_LIBDIR/llvm-readelf" -lW "$so" 2>/dev/null | awk '/LOAD/ {print $NF}' | sort -u | tail -1) |
| 249 | + if [ "$ALIGN" = "0x4000" ]; then |
| 250 | + OK=$((OK+1)) |
| 251 | + else |
| 252 | + echo "FAIL (align=$ALIGN): $so" |
| 253 | + FAIL=$((FAIL+1)) |
| 254 | + fi |
| 255 | + done < <(find "$WORKDIR/lib" -name '*.so' -print0) |
| 256 | + rm -rf "$WORKDIR" |
| 257 | + echo "-- Summary: OK=$OK, FAIL=$FAIL --" |
| 258 | + if [ "$FAIL" -ne 0 ]; then |
| 259 | + echo "16K page-size check failed - aborting before publish." |
| 260 | + exit 1 |
| 261 | + fi |
| 262 | +
|
| 263 | + - name: Archive signed APK/AAB |
| 264 | + uses: actions/upload-artifact@v4 |
| 265 | + with: |
| 266 | + name: release-playstore-${{ steps.next_version.outputs.version_code }} |
| 267 | + path: | |
| 268 | + ${{ github.workspace }}/output/android-release-signed.apk |
| 269 | + ${{ github.workspace }}/output/android-release-signed.aab |
| 270 | +
|
| 271 | + - name: Publish to Play Store (internal testing track) |
| 272 | + if: ${{ inputs.publish }} |
| 273 | + uses: r0adkll/upload-google-play@v1 |
| 274 | + with: |
| 275 | + serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} |
| 276 | + packageName: org.cagnulen.qdomyoszwift |
| 277 | + releaseFiles: ${{ github.workspace }}/output/android-release-signed.aab |
| 278 | + track: internal |
| 279 | + status: completed |
0 commit comments