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