From 5fc074ef67bd21459ccc136410c4c4259549e6e5 Mon Sep 17 00:00:00 2001 From: BW Date: Mon, 5 Oct 2026 11:02:32 -0600 Subject: [PATCH] Add GitHub Actions workflows for CI and Google Play / F-Droid releases - Add .github/workflows/release.yml to build, sign, and publish releases on tag push (v*) or workflow_dispatch. - Verifies reproducible build signing key for F-Droid before attaching APKs to GitHub Releases. - Uploads AAB to Google Play with localized release notes via r0adkll/upload-google-play. - Add .github/workflows/ci.yml for pull request and branch validation. - Document GitHub secrets and release processes in android/README.md. --- .github/workflows/ci.yml | 44 +++++++++ .github/workflows/release.yml | 176 ++++++++++++++++++++++++++++++++++ android/README.md | 18 ++++ 3 files changed, 238 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..15dc21a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: + - main + paths: + - 'android/**' + - '.github/workflows/ci.yml' + pull_request: + branches: + - main + paths: + - 'android/**' + - '.github/workflows/ci.yml' + +jobs: + build: + name: Build Android (Debug) + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + yes | sdkmanager --licenses || true + sdkmanager "platforms;android-37.0" "build-tools;37.0.0" + + - name: Build Debug APKs + working-directory: android + run: | + ./gradlew :app:assembleGithubDebug :app:assembleFdroidDebug diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c1f48c9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,176 @@ +name: Release & Publish + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + play_track: + description: 'Google Play track' + required: true + default: 'internal' + type: choice + options: + - internal + - alpha + - beta + - production + publish_to_play: + description: 'Publish to Google Play (if service account configured)' + required: true + default: true + type: boolean + +permissions: + contents: write + +jobs: + release: + name: Build & Publish Release + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + yes | sdkmanager --licenses || true + sdkmanager "platforms;android-37.0" "build-tools;37.0.0" + + - name: Set up Release Signing Key + id: setup_signing + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: | + if [ -z "$KEYSTORE_BASE64" ]; then + echo "::error::KEYSTORE_BASE64 secret is not configured. Release builds require release signing." + exit 1 + fi + + KEYSTORE_PATH="${GITHUB_WORKSPACE}/android/app/release.jks" + echo "$KEYSTORE_BASE64" | base64 -d > "$KEYSTORE_PATH" + + cat < android/keystore.properties + storeFile=$KEYSTORE_PATH + storePassword=$KEYSTORE_PASSWORD + keyAlias=$KEY_ALIAS + keyPassword=$KEY_PASSWORD + EOF + + - name: Determine Version & Prepare Changelogs + id: version_info + run: | + VERSION=$(grep -E 'versionName\s*=\s*' android/app/build.gradle.kts | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') + VERSION_CODE=$(grep -E 'versionCode\s*=\s*' android/app/build.gradle.kts | head -n1 | sed -E 's/.*=\s*([0-9]+).*/\1/') + + TAG="${GITHUB_REF_NAME}" + if [ -z "$TAG" ] || [ "$TAG" = "main" ]; then + TAG="v${VERSION}" + fi + + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "version_code=${VERSION_CODE}" >> "$GITHUB_OUTPUT" + + CHANGELOG_SRC="fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt" + if [ -f "$CHANGELOG_SRC" ]; then + echo "changelog_file=${CHANGELOG_SRC}" >> "$GITHUB_OUTPUT" + else + echo "changelog_file=" >> "$GITHUB_OUTPUT" + fi + + # Prepare Google Play whatsnew directory format (whatsnew-) + mkdir -p distribution/whatsnew + if [ -f "$CHANGELOG_SRC" ]; then + cp "$CHANGELOG_SRC" distribution/whatsnew/whatsnew-en-US + fi + + - name: Build Android Release Artifacts + working-directory: android + run: | + ./gradlew :app:assembleRelease :app:bundleGithubRelease + + - name: Package Release Assets + run: | + TAG="${{ steps.version_info.outputs.tag }}" + mkdir -p dist + + # 1. GitHub flavor APK (includes bundled Pebble watchapp asset) + cp android/app/build/outputs/apk/github/release/app-github-release.apk "dist/PebbleRecorder-${TAG}.apk" + + # 2. F-Droid flavor APK (without bundled Pebble watchapp, reference binary for reproducible build) + cp android/app/build/outputs/apk/fdroid/release/app-fdroid-release.apk "dist/PebbleRecorder-${TAG}-fdroid.apk" + + # 3. Android App Bundle (AAB) for Google Play distribution + cp android/app/build/outputs/bundle/githubRelease/app-github-release.aab "dist/PebbleRecorder-${TAG}.aab" + + # 4. Watchapp PBW asset + if [ -f watch/build/watch.pbw ]; then + cp watch/build/watch.pbw dist/watch.pbw + elif [ -f android/app/src/github/assets/watch.pbw ]; then + cp android/app/src/github/assets/watch.pbw dist/watch.pbw + fi + + ls -lh dist/ + + - name: Verify F-Droid APK Signature + run: | + TAG="${{ steps.version_info.outputs.tag }}" + EXPECTED_FPRINT="3c7b738a4be737b5ed376eb686f9d507d4779d715e6d73a0306e2d7b7203852c" + + FDROID_APK="dist/PebbleRecorder-${TAG}-fdroid.apk" + CERT_FPRINT=$(apksigner verify --print-certs "$FDROID_APK" | grep "SHA-256 digest:" | head -n1 | awk '{print $NF}') + + echo "Expected certificate digest: $EXPECTED_FPRINT" + echo "Actual APK certificate digest: $CERT_FPRINT" + + if [ "$CERT_FPRINT" != "$EXPECTED_FPRINT" ]; then + echo "::error::F-Droid APK signing certificate does not match packaging/fdroid/com.pebblerecorder.app.yml AllowedAPKSigningKeys!" + exit 1 + fi + + - name: Publish GitHub Release (Powers F-Droid Updates) + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version_info.outputs.tag }} + name: ${{ steps.version_info.outputs.tag }} + body_path: ${{ steps.version_info.outputs.changelog_file }} + files: | + dist/PebbleRecorder-${{ steps.version_info.outputs.tag }}.apk + dist/PebbleRecorder-${{ steps.version_info.outputs.tag }}-fdroid.apk + dist/PebbleRecorder-${{ steps.version_info.outputs.tag }}.aab + dist/watch.pbw + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to Google Play Store + if: ${{ (github.event_name == 'push' || inputs.publish_to_play) && secrets.PLAY_STORE_JSON_KEY != '' }} + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_JSON_KEY }} + packageName: com.pebblerecorder.app + releaseFiles: dist/PebbleRecorder-${{ steps.version_info.outputs.tag }}.aab + track: ${{ inputs.play_track || vars.PLAY_STORE_TRACK || 'internal' }} + whatsNewDirectory: distribution/whatsnew + + - name: Notice on Google Play Upload + if: ${{ (github.event_name == 'push' || inputs.publish_to_play) && secrets.PLAY_STORE_JSON_KEY == '' }} + run: | + echo "::notice::Google Play upload was skipped because PLAY_STORE_JSON_KEY secret is not configured." diff --git a/android/README.md b/android/README.md index f133138..f33fd61 100644 --- a/android/README.md +++ b/android/README.md @@ -78,3 +78,21 @@ rejects any extra signing block. Kotlin DSL. - **`buildToolsVersion` is pinned** to avoid AGP's default pick triggering a download-and-license-accept flow. + +## Automated Releases (GitHub Actions) + +Releases can be built and published via `.github/workflows/release.yml` when a tag matching `v*` is pushed (or triggered manually via `workflow_dispatch`). + +### Required GitHub Secrets + +- `KEYSTORE_BASE64` — base64-encoded release `.jks` file (e.g. `base64 -w 0 ~/.android/pebblerecorder-release.jks`). +- `KEYSTORE_PASSWORD` — keystore password. +- `KEY_ALIAS` — key alias in the keystore. +- `KEY_PASSWORD` — key password. +- `PLAY_STORE_JSON_KEY` *(optional)* — Google Play Console service account credentials JSON. If omitted, Google Play upload is safely skipped with a notice. + +### F-Droid & Google Play Distribution + +- **F-Droid**: F-Droid does not have a direct upload API; it builds from source on its own servers. For reproducible builds, F-Droid's update bot (`UpdateCheckMode: Tags` in `packaging/fdroid/com.pebblerecorder.app.yml`) monitors GitHub Releases for `PebbleRecorder-vX.Y.Z-fdroid.apk`, verifies the build hash matches, and publishes it. The GitHub Release published by this workflow provides this required binary asset automatically. +- **Google Play**: The workflow builds the signed Android App Bundle (`PebbleRecorder-vX.Y.Z.aab`) and uploads it to the configured track (`internal`, `alpha`, `beta`, or `production`, defaulting to `internal` or repository variable `PLAY_STORE_TRACK`) along with release notes from `fastlane/metadata/android/en-US/changelogs/.txt`. +