diff --git a/.github/workflows/android-apk-release.yml b/.github/workflows/android-apk-release.yml index 1ca583d74..e1f7a38a7 100644 --- a/.github/workflows/android-apk-release.yml +++ b/.github/workflows/android-apk-release.yml @@ -141,22 +141,26 @@ jobs: fi done - mkdir -p packages/app/.secrets packages/app/credentials/android - printf '%s' "$ANDROID_GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > packages/app/.secrets/google-services.prod.json - printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > packages/app/credentials/android/keystore.jks + google_services_file="$RUNNER_TEMP/google-services.prod.json" + keystore_file="$RUNNER_TEMP/paseo-upload.jks" + + printf '%s' "$ANDROID_GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > "$google_services_file" + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$keystore_file" jq -n \ + --arg keystorePath "$keystore_file" \ --arg keystorePassword "$ANDROID_KEYSTORE_PASSWORD" \ --arg keyAlias "$ANDROID_KEY_ALIAS" \ --arg keyPassword "$ANDROID_KEY_PASSWORD" \ - '{android: {keystore: {keystorePath: "credentials/android/keystore.jks", keystorePassword: $keystorePassword, keyAlias: $keyAlias, keyPassword: $keyPassword}}}' \ + '{android: {keystore: {keystorePath: $keystorePath, keystorePassword: $keystorePassword, keyAlias: $keyAlias, keyPassword: $keyPassword}}}' \ > packages/app/credentials.json + echo "GOOGLE_SERVICES_FILE_PROD=$google_services_file" >> "$GITHUB_ENV" + - name: Build signed production APK on GitHub env: APP_VARIANT: production - EAS_LOCAL_BUILD_ARTIFACTS_DIR: ${{ runner.temp }}/eas-artifacts - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - GOOGLE_SERVICES_FILE_PROD: ./.secrets/google-services.prod.json + EAS_BUILD: "1" + EXPO_UPDATES_CHANNEL: production GRADLE_OPTS: >- -Dorg.gradle.jvmargs="-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" -Dorg.gradle.parallel=false @@ -165,13 +169,43 @@ jobs: shell: bash run: | set -euo pipefail + + npm run build:app-deps + npm --prefix packages/app run build:terminal-webview + cd packages/app - npx eas build \ - --platform android \ - --profile production-apk \ - --local \ - --non-interactive \ - --output "$RUNNER_TEMP/${{ steps.mode.outputs.artifact-name }}" + npx expo prebuild --platform android --clean --non-interactive + node -e '(async () => require("expo/node_modules/@expo/config-plugins").AndroidConfig.EasBuild.configureEasBuildAsync(process.cwd()))().catch((error) => { console.error(error); process.exitCode = 1; })' + + cd android + ./gradlew \ + :app:assembleRelease \ + --no-daemon \ + --max-workers=1 \ + -Dorg.gradle.parallel=false \ + -x lint \ + -x lintVitalAnalyzeRelease \ + -x lintVitalRelease \ + -x generateReleaseLintModel \ + -x generateReleaseLintVitalModel + + artifact="$RUNNER_TEMP/${{ steps.mode.outputs.artifact-name }}" + cp app/build/outputs/apk/release/app-release.apk "$artifact" + + apksigner="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | sort -V | tail -n 1)" + if [[ -z "$apksigner" ]]; then + echo "::error::Unable to locate apksigner." + exit 1 + fi + + expected_sha256="421698bdca5bb9168c970e24539781509b5aa23fab8ab406a15b3f9c5f04c647" + actual_sha256="$($apksigner verify --print-certs "$artifact" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p' | tr -d ':[:space:]' | tr '[:upper:]' '[:lower:]')" + if [[ "$actual_sha256" != "$expected_sha256" ]]; then + echo "::error::APK signing certificate does not match the production key." + exit 1 + fi + + echo "Verified production signing certificate: $actual_sha256" - name: Upload APK to workflow artifacts uses: actions/upload-artifact@v4 diff --git a/docs/android.md b/docs/android.md index 488559bbe..9eb7f98e9 100644 --- a/docs/android.md +++ b/docs/android.md @@ -137,7 +137,7 @@ adb exec-out screencap -p > screenshot.png Stable tag pushes like `v0.1.0` trigger: - The EAS GitHub app on Expo servers (iOS + Android production builds + store submit). There is no workflow file in this repo for it. -- `.github/workflows/android-apk-release.yml` on GitHub Actions (locally built, production-signed APK asset on GitHub Release). The APK build uses EAS local tooling for build parity but runs entirely on the GitHub runner, so it does not consume an EAS cloud build. +- `.github/workflows/android-apk-release.yml` on GitHub Actions (locally built, production-signed APK asset on GitHub Release). The workflow runs Expo prebuild and Gradle directly on the GitHub runner, so it does not consume an EAS cloud build. iOS auto-submits to App Store review via a Fastlane lane after EAS uploads to TestFlight. Android auto-submits to the Play Store via EAS-managed credentials. @@ -150,7 +150,7 @@ The GitHub APK workflow also supports `workflow_dispatch`: - Leave `publish` disabled and optionally provide `ref` to build any branch, tag, or commit as a workflow artifact without changing a GitHub release. - Enable `publish` and provide an existing `tag` to rebuild that tag and upload the APK to its GitHub release. Manual publishes always build the tag itself; they cannot publish an arbitrary ref under a release tag. -The workflow restores the production keystore and Firebase configuration from protected GitHub repository secrets. Keep those values synchronized with the production Android credentials in EAS. The APK profile uses `credentialsSource: local`; the AAB profile continues using EAS-managed credentials and store submission. +The workflow restores the production keystore and Firebase configuration from protected GitHub repository secrets, configures the production EAS Update channel, and verifies the finished APK's signing certificate before upload. Keep the protected values synchronized with the production Android credentials in EAS. The AAB profile continues using EAS-managed credentials and store submission. ### Useful commands diff --git a/packages/app/app.config.js b/packages/app/app.config.js index 6041247ec..fd16e2924 100644 --- a/packages/app/app.config.js +++ b/packages/app/app.config.js @@ -130,6 +130,13 @@ export default { }, updates: { url: "https://u.expo.dev/0e7f65ce-0367-46c8-a238-2b65963d235a", + ...(process.env.EXPO_UPDATES_CHANNEL + ? { + requestHeaders: { + "expo-channel-name": process.env.EXPO_UPDATES_CHANNEL, + }, + } + : {}), ...buildProfile.updates, }, ios: { diff --git a/packages/app/eas.json b/packages/app/eas.json index e5ae97038..805217b89 100644 --- a/packages/app/eas.json +++ b/packages/app/eas.json @@ -23,7 +23,6 @@ }, "production-apk": { "extends": "production", - "credentialsSource": "local", "distribution": "internal", "android": { "buildType": "apk",