fix(android): keep signing secrets out of build logs

Run Expo prebuild and serialized Gradle directly instead of passing signing credentials through the local EAS payload. Verify the finished APK against the production signing fingerprint before upload.
This commit is contained in:
Mohamed Boudra
2026-07-16 17:53:20 +02:00
parent 0b820d729a
commit 5e8eec44c1
4 changed files with 56 additions and 16 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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: {

View File

@@ -23,7 +23,6 @@
},
"production-apk": {
"extends": "production",
"credentialsSource": "local",
"distribution": "internal",
"android": {
"buildType": "apk",