Files
opencode-mobile/.github/workflows/build.yml
Den 5f9dd2a80c fix(release): enforce Android version parity (#141)
Adds a deterministic metadata guard before CI and F-Droid builds so generated release artifacts cannot silently inherit stale Gradle versions.\n\nPlan: https://github.com/dzianisv/opencode-mobile/issues/95#issuecomment-5047827673

Co-authored-by: engineer <engineer@gray-knight-m1.local>
2026-07-22 08:50:35 -07:00

296 lines
12 KiB
YAML

name: Build Android APK
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
# Node >= 23.6 runs the TypeScript test files natively (type-stripping),
# matching the local toolchain. No build step or extra deps required.
node-version: 24
cache: npm
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Check release version metadata
run: npm run check:versions
- name: Typecheck
run: npm run typecheck
- name: Unit tests
run: npm test
build:
runs-on: ubuntu-latest
env:
EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }}
EXPO_PUBLIC_POSTHOG_KEY: ${{ secrets.EXPO_PUBLIC_POSTHOG_KEY }}
EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER: ${{ secrets.EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v4
- name: Cache Gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
android/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Set Sentry release identifiers
# sentry.gradle (applied from android/app/build.gradle) defaults the
# upload's --release/--dist to `${applicationId}@${versionName}+${versionCode}`,
# which does NOT match the release/dist Sentry.init() reports at runtime
# (`opencode-mobile@${app.json version}`, see src/lib/sentry.ts). That
# mismatch made every uploaded source map land under a release Sentry
# never looks up, so symbolication silently failed. Pin the Gradle-side
# values to exactly what the app reports.
run: |
VERSION=$(node -p "require('./app.json').expo.version")
echo "SENTRY_RELEASE=opencode-mobile@${VERSION}" >> "$GITHUB_ENV"
echo "SENTRY_DIST=${VERSION}" >> "$GITHUB_ENV"
echo "Sentry release=opencode-mobile@${VERSION} dist=${VERSION}"
- name: Expo prebuild
run: npx expo prebuild --platform android --no-install
- name: Setup signing
run: |
if [[ "${{ github.ref }}" == refs/tags/v* && -n "${{ secrets.KEYSTORE_BASE64 }}" ]]; then
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
echo "RELEASE_STORE_FILE=release.keystore" >> "$GITHUB_ENV"
echo "RELEASE_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> "$GITHUB_ENV"
echo "RELEASE_KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> "$GITHUB_ENV"
echo "RELEASE_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> "$GITHUB_ENV"
echo "Signing: production keystore"
else
keytool -genkey -v -keystore android/app/debug.keystore -storepass android \
-alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
echo "Signing: debug keystore (non-release build)"
fi
- name: Build APK
working-directory: android
run: ./gradlew assembleRelease
- name: Upload APK artifact
uses: actions/upload-artifact@v7
with:
name: app-release
path: android/app/build/outputs/apk/release/app-release.apk
# F-Droid rebuilds this app from source using the recipe in
# distribution/fdroid-submission/metadata.yml (Builds:), which applies
# fdroid/expo-application-patches and fdroid/expo-notifications-patches to
# strip Firebase Cloud Messaging / Play Install Referrer / Sentry before
# compiling. If we only ever publish the full-featured `app-release.apk`
# (built above, unpatched) and metadata.yml's `Binaries:` points at it,
# F-Droid's reproducible-build check compares its from-source (patched)
# rebuild against that (unpatched) reference binary and can never match —
# see issue #95.
#
# Fix: build a SECOND, additional artifact here — app-release-fdroid.apk —
# by applying the exact same patch set F-Droid's own recipe applies, then
# publish it as an extra GitHub Release asset. metadata.yml's `Binaries:`
# points at THIS asset, not app-release.apk. The main `build` job above is
# completely untouched: Play/GitHub/IzzyOnDroid users keep the full-featured
# binary (push notifications, Sentry crash reporting) unchanged.
#
# This job intentionally does NOT run `npx expo prebuild` — F-Droid's own
# Builds: recipe doesn't either; it patches the already-committed `android/`
# tree directly. Running prebuild here would regenerate android/app/build.gradle
# and could diverge from what F-Droid's build server produces from the same
# tracked source, defeating the byte-for-byte parity this job exists for.
#
# This job also intentionally receives NONE of the EXPO_PUBLIC_SENTRY_DSN /
# EXPO_PUBLIC_POSTHOG_KEY / EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER / SENTRY_*
# secrets that the main `build` job sets — F-Droid's isolated build
# environment has no access to this repo's secrets either, so baking any of
# them into the JS bundle here would itself be a source of reproducible-build
# divergence.
build-fdroid:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
# F-Droid's metadata.yml prebuild patch bumps the Kotlin/Java toolchain
# target from 17 to 21 (see the "Apply F-Droid source patches" step
# below). Install both so Gradle's toolchain auto-detection can resolve
# either, matching F-Droid's own multi-JDK build environment.
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Android SDK
uses: android-actions/setup-android@v4
- name: Cache Gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
android/.gradle
key: ${{ runner.os }}-gradle-fdroid-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-fdroid-
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Apply F-Droid source patches
# Mirrors distribution/fdroid-submission/metadata.yml `Builds:.prebuild`
# verbatim so this CI build and F-Droid's from-source rebuild patch the
# exact same lines/files. Keep these two in sync on any future change.
run: |
set -euxo pipefail
# Drop the Sentry native gradle plugin apply (no SENTRY_* secrets
# are available in F-Droid's build environment either).
sed -i '/apply from.*sentry.gradle/d' android/app/build.gradle
# F-Droid's build environment toolchains on JDK 21; bump the
# Kotlin/Java compile target used by RN's gradle plugin + expo-modules-core.
sed -i '/jvmToolchain\|JavaVersion/s/17/21/' \
node_modules/@react-native/gradle-plugin/*/build.gradle.kts \
node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/JdkConfiguratorUtils.kt \
node_modules/expo-modules-core/android/ExpoModulesCorePlugin.gradle
printf '\nkotlin.jvm.target.validation.mode=warning\n' >> android/gradle.properties
# Strip Firebase Cloud Messaging from expo-notifications.
sed -i '/firebase/d' node_modules/expo-notifications/android/build.gradle
cp -a fdroid/expo-notifications-patches/. \
node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/
rm -f \
node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/RemoteMessageSerializer.java \
node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/triggers/FirebaseNotificationTrigger.kt
# Strip Google Play Install Referrer (GMS-only API) from expo-application.
sed -i '/installreferrer/d' node_modules/expo-application/android/build.gradle
cp -a fdroid/expo-application-patches/. \
node_modules/expo-application/android/src/main/java/expo/modules/application/
- name: Setup signing
run: |
if [[ -n "${{ secrets.KEYSTORE_BASE64 }}" ]]; then
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
echo "RELEASE_STORE_FILE=release.keystore" >> "$GITHUB_ENV"
echo "RELEASE_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> "$GITHUB_ENV"
echo "RELEASE_KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> "$GITHUB_ENV"
echo "RELEASE_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> "$GITHUB_ENV"
# Same production keystore as app-release.apk: F-Droid's
# AllowedAPKSigningKeys check requires this binary to carry the
# same signing certificate fingerprint.
echo "Signing: production keystore"
else
keytool -genkey -v -keystore android/app/debug.keystore -storepass android \
-alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
echo "Signing: debug keystore (non-release build)"
fi
- name: Build F-Droid-flavored APK
working-directory: android
run: ./gradlew assembleRelease
- name: Re-sign APK v1+v2 only (drop v3/v4 for fdroidserver compatibility)
if: ${{ env.RELEASE_STORE_FILE != '' }}
run: |
# Same rationale as publish-fdroid.yml: androguard (used by
# fdroidserver) crashes parsing a v2+v3 signature block pair. Force
# v1+v2-only here deterministically.
APK=android/app/build/outputs/apk/release/app-release.apk
APKSIGNER=$(ls "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -1)
echo "Using $APKSIGNER"
"$APKSIGNER" sign \
--ks android/app/release.keystore \
--ks-pass "pass:${RELEASE_STORE_PASSWORD}" \
--ks-key-alias "${RELEASE_KEY_ALIAS}" \
--key-pass "pass:${RELEASE_KEY_PASSWORD}" \
--v1-signing-enabled true \
--v2-signing-enabled true \
--v3-signing-enabled false \
--v4-signing-enabled false \
"$APK"
echo "=== signature schemes after re-sign ==="
"$APKSIGNER" verify -v "$APK" | grep -i "Verified using" || true
- name: Rename artifact
run: cp android/app/build/outputs/apk/release/app-release.apk app-release-fdroid.apk
- name: Upload F-Droid APK artifact
uses: actions/upload-artifact@v7
with:
name: app-release-fdroid
path: app-release-fdroid.apk
release:
needs: [build, build-fdroid]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: app-release
- uses: actions/download-artifact@v8
with:
name: app-release-fdroid
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
app-release.apk
app-release-fdroid.apk
generate_release_notes: true