diff --git a/.github/workflows/app-ci.yml b/.github/workflows/app-ci.yml index bbfcdbe..bc6edc5 100644 --- a/.github/workflows/app-ci.yml +++ b/.github/workflows/app-ci.yml @@ -36,6 +36,25 @@ jobs: - name: Run Android quality checks run: ./gradlew ktlintCheck detekt testDebugUnitTest assembleDebug assembleRelease --stacktrace + - name: Verify debug APK signature + shell: bash + run: | + set -euo pipefail + apksigner_bin="$(find "${ANDROID_HOME}/build-tools" -maxdepth 2 -type f -name apksigner | sort -V | tail -n1)" + if [[ -z "${apksigner_bin}" ]]; then + echo "apksigner not found under ${ANDROID_HOME}/build-tools" >&2 + exit 1 + fi + shopt -s nullglob + apks=(app/build/outputs/apk/debug/*.apk) + if (( ${#apks[@]} == 0 )); then + echo "No debug APKs found" >&2 + exit 1 + fi + for apk in "${apks[@]}"; do + "${apksigner_bin}" verify --verbose --print-certs "$apk" + done + - name: Upload debug APK uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 1aefc2d..6e44953 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -26,9 +26,56 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 + - name: Decode release keystore + shell: bash + env: + AUTOFISH_RELEASE_KEYSTORE_BASE64: ${{ secrets.AUTOFISH_RELEASE_KEYSTORE_BASE64 }} + AUTOFISH_RELEASE_STORE_PASSWORD: ${{ secrets.AUTOFISH_RELEASE_STORE_PASSWORD }} + AUTOFISH_RELEASE_KEY_ALIAS: ${{ secrets.AUTOFISH_RELEASE_KEY_ALIAS }} + AUTOFISH_RELEASE_KEY_PASSWORD: ${{ secrets.AUTOFISH_RELEASE_KEY_PASSWORD }} + run: | + set -euo pipefail + for secret_name in \ + AUTOFISH_RELEASE_KEYSTORE_BASE64 \ + AUTOFISH_RELEASE_STORE_PASSWORD \ + AUTOFISH_RELEASE_KEY_ALIAS \ + AUTOFISH_RELEASE_KEY_PASSWORD + do + if [[ -z "${!secret_name}" ]]; then + echo "$secret_name is required for app releases" >&2 + exit 1 + fi + done + mkdir -p "$RUNNER_TEMP/autofish-signing" + printf '%s' "${AUTOFISH_RELEASE_KEYSTORE_BASE64}" | base64 --decode > "$RUNNER_TEMP/autofish-signing/release.keystore" + - name: Build release APK + env: + AUTOFISH_RELEASE_STORE_FILE: ${{ runner.temp }}/autofish-signing/release.keystore + AUTOFISH_RELEASE_STORE_PASSWORD: ${{ secrets.AUTOFISH_RELEASE_STORE_PASSWORD }} + AUTOFISH_RELEASE_KEY_ALIAS: ${{ secrets.AUTOFISH_RELEASE_KEY_ALIAS }} + AUTOFISH_RELEASE_KEY_PASSWORD: ${{ secrets.AUTOFISH_RELEASE_KEY_PASSWORD }} run: ./gradlew assembleRelease + - name: Verify release APK signature + shell: bash + run: | + set -euo pipefail + apksigner_bin="$(find "${ANDROID_HOME}/build-tools" -maxdepth 2 -type f -name apksigner | sort -V | tail -n1)" + if [[ -z "${apksigner_bin}" ]]; then + echo "apksigner not found under ${ANDROID_HOME}/build-tools" >&2 + exit 1 + fi + shopt -s nullglob + apks=(app/build/outputs/apk/release/*.apk) + if (( ${#apks[@]} == 0 )); then + echo "No release APKs found" >&2 + exit 1 + fi + for apk in "${apks[@]}"; do + "${apksigner_bin}" verify --verbose --print-certs "$apk" + done + - name: Collect APKs shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e9d40..9369fb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed +- Sign app release APKs in the GitHub release workflow and verify release artifacts with `apksigner` before publishing. +- Verify debug APK signatures in Android CI so installability regressions are caught earlier. + ## [0.5.0] - 2026-05-16 Range: `app-v0.4.0` / `cli-v0.4.0` .. `app-v0.5.0` / `cli-v0.5.0` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bb57c7f..5d61c11 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,16 @@ val versionCodeProp = rawVersionCodeProp!! } +val releaseStoreFile = providers.environmentVariable("AUTOFISH_RELEASE_STORE_FILE") +val releaseStorePassword = providers.environmentVariable("AUTOFISH_RELEASE_STORE_PASSWORD") +val releaseKeyAlias = providers.environmentVariable("AUTOFISH_RELEASE_KEY_ALIAS") +val releaseKeyPassword = providers.environmentVariable("AUTOFISH_RELEASE_KEY_PASSWORD") +val hasReleaseSigningConfig = + releaseStoreFile.isPresent && + releaseStorePassword.isPresent && + releaseKeyAlias.isPresent && + releaseKeyPassword.isPresent + android { namespace = "com.memohai.autofish" compileSdk = 36 @@ -60,6 +70,17 @@ android { versionName = versionNameProp } + signingConfigs { + if (hasReleaseSigningConfig) { + create("release") { + storeFile = file(releaseStoreFile.get()) + storePassword = releaseStorePassword.get() + keyAlias = releaseKeyAlias.get() + keyPassword = releaseKeyPassword.get() + } + } + } + buildTypes { debug { applicationIdSuffix = ".debug" @@ -73,6 +94,9 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) + if (hasReleaseSigningConfig) { + signingConfig = signingConfigs.getByName("release") + } } }