# The release APK, on every push to master. # # This is the one job that needs the Android SDK, so it is also where the `:app` # unit tests run — `:core`'s suite is in `tests.yml`, where no SDK is needed # because `settings.gradle.kts` leaves `:app` out when there is none. # # The Glyph Matrix AAR is not committed (its licence forbids redistribution), so # the job runs `android/fetch-glyph-sdk.sh` to download it from Nothing before # Gradle configures the app. That means the APK this produces really has the # rear-display implementation compiled in, rather than the no-op fallback. name: android on: push: branches: [master] tags: ["v*"] env: ANDROID_HOME: /opt/android-sdk JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64 DEBIAN_FRONTEND: noninteractive jobs: apk: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install JDK 21 and the tools the SDK installer needs run: | apt-get update -qq apt-get install -y --no-install-recommends \ openjdk-21-jdk-headless curl unzip >/dev/null # The AAR is git-ignored but required for a Glyph-enabled build. The # script exits non-zero if the download is not a real archive, so a vendor # URL that moves fails the job instead of quietly producing a no-op APK. - name: Fetch the Glyph Matrix SDK working-directory: android run: ./fetch-glyph-sdk.sh # Keyed on the pinned SDK versions, so this is a download once and a # restore every time after. - uses: actions/cache@v4 continue-on-error: true with: path: /opt/android-sdk key: android-sdk-${{ hashFiles('android/gradle/libs.versions.toml') }} - uses: actions/cache@v4 continue-on-error: true with: path: ~/.gradle/caches key: gradle-app-${{ hashFiles('android/gradle/libs.versions.toml', 'android/gradle/wrapper/gradle-wrapper.properties') }} - name: Install the Android SDK run: ./android/ci/install-sdk.sh - name: Unit tests (:app, on the JVM via Robolectric) working-directory: android run: ./gradlew --no-daemon :app:testDebugUnitTest - name: Unpack the signing key id: key env: KEYSTORE_BASE64: ${{ secrets.KADENCE_KEYSTORE_BASE64 }} run: | if [ -z "${KEYSTORE_BASE64:-}" ]; then echo "No KADENCE_KEYSTORE_BASE64 secret — nothing to sign with." >&2 echo "signed=false" >> "$GITHUB_OUTPUT" exit 0 fi mkdir -p "$RUNNER_TEMP" printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/kadence.keystore" echo "signed=true" >> "$GITHUB_OUTPUT" # A keystore without its password and alias cannot sign anything. Gradle # would eventually fail in `packageRelease`, after the build and R8 have # run; checking here says which secret is missing in a few seconds. - name: Check the signing secrets if: steps.key.outputs.signed == 'true' env: STORE_PASSWORD: ${{ secrets.KADENCE_KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KADENCE_KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KADENCE_KEY_PASSWORD }} run: | missing=() [ -n "${STORE_PASSWORD:-}" ] || missing+=(KADENCE_KEYSTORE_PASSWORD) [ -n "${KEY_ALIAS:-}" ] || missing+=(KADENCE_KEY_ALIAS) [ -n "${KEY_PASSWORD:-}" ] || missing+=(KADENCE_KEY_PASSWORD) if [ ${#missing[@]} -gt 0 ]; then echo "::error::Missing signing secret(s): ${missing[*]}" exit 1 fi if ! keytool -list -keystore "$RUNNER_TEMP/kadence.keystore" \ -storepass "$STORE_PASSWORD" -alias "$KEY_ALIAS" >/dev/null 2>&1; then echo "::error::KADENCE_KEYSTORE_PASSWORD or KADENCE_KEY_ALIAS does not open the keystore." exit 1 fi - name: Resolve the version name run: | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/v}" else VERSION="0.1.0-master.${{ gitea.run_number }}" fi echo "KADENCE_VERSION_NAME=$VERSION" >> "$GITHUB_ENV" echo "Resolved version $VERSION" - name: Build the release APK working-directory: android env: # The run number, so successive APKs install over each other. It was # pinned at 1, which made every build look like the same version. KADENCE_VERSION_CODE: ${{ gitea.run_number }} KADENCE_KEYSTORE: ${{ runner.temp }}/kadence.keystore KADENCE_KEYSTORE_PASSWORD: ${{ secrets.KADENCE_KEYSTORE_PASSWORD }} KADENCE_KEY_ALIAS: ${{ secrets.KADENCE_KEY_ALIAS }} KADENCE_KEY_PASSWORD: ${{ secrets.KADENCE_KEY_PASSWORD }} # With a keystore, a missing or misnamed secret must fail the build # rather than silently produce a debug-signed "release". KADENCE_REQUIRE_SIGNING: ${{ steps.key.outputs.signed == 'true' && '1' || '0' }} run: ./gradlew --no-daemon :app:assembleRelease - name: Say which key actually signed it run: | APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner | sort | tail -1)" APK=android/app/build/outputs/apk/release/app-release.apk "$APKSIGNER" verify --print-certs "$APK" if [ "${{ steps.key.outputs.signed }}" = "true" ]; then # The debug key is unmistakable, and shipping it would be a trap: the # APK installs, then cannot be upgraded by a properly signed one. if "$APKSIGNER" verify --print-certs "$APK" | grep -q "CN=Android Debug"; then echo "::error::signed with the debug key despite a keystore being provided" exit 1 fi fi # A small, separately uploaded description of the APK. The backend's OTA # endpoint reads this artifact on every check; keeping it out of the APK # artifact avoids downloading tens of megabytes just to ask "is there a # newer build?". - name: Write the update metadata run: | APK=android/app/build/outputs/apk/release/app-release.apk META_DIR=android/app/build/outputs/update mkdir -p "$META_DIR" APKSIGNER="$(find "$ANDROID_HOME/build-tools" -name apksigner | sort | tail -1)" SIGNER_SHA256="$("$APKSIGNER" verify --print-certs "$APK" \ | sed -n 's/.*certificate SHA-256 digest: //p' | head -1)" printf '{\n "version_code": %s,\n "version_name": "%s",\n "commit": "%s",\n "built_at": "%s",\n "size_bytes": %s,\n "sha256": "%s",\n "signer_sha256": "%s"\n}\n' \ "${{ gitea.run_number }}" \ "$KADENCE_VERSION_NAME" \ "$GITHUB_SHA" \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ "$(stat -c%s "$APK")" \ "$(sha256sum "$APK" | cut -d' ' -f1)" \ "$SIGNER_SHA256" > "$META_DIR/update.json" cat "$META_DIR/update.json" - uses: https://gitea.com/actions/gitea-upload-artifact@v4 with: name: kadence-release-apk path: android/app/build/outputs/apk/release/app-release.apk if-no-files-found: error - uses: https://gitea.com/actions/gitea-upload-artifact@v4 with: name: kadence-update-metadata path: android/app/build/outputs/update/update.json if-no-files-found: error - name: Attach it to the release, when this is a tag if: startsWith(github.ref, 'refs/tags/v') env: TOKEN: ${{ secrets.GITEA_TOKEN }} run: | VERSION="${GITHUB_REF#refs/tags/}" API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" ID="$(curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/$VERSION" \ | sed -n 's/.*"id":[ ]*\([0-9]*\).*/\1/p' | head -1)" curl -fsS -X POST -H "Authorization: token $TOKEN" \ -F "attachment=@android/app/build/outputs/apk/release/app-release.apk" \ "$API/releases/$ID/assets?name=kadence-$VERSION.apk"