mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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.
276 lines
9.6 KiB
YAML
276 lines
9.6 KiB
YAML
name: Android APK Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
- "android-v*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: "Branch, tag, or commit to build. Leave blank to build the selected workflow ref."
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
publish:
|
|
description: "Upload the APK to an existing or new GitHub release."
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
tag:
|
|
description: "Release tag to rebuild and publish. Required when publish is enabled."
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
|
|
concurrency:
|
|
group: android-apk-release-${{ github.event_name == 'workflow_dispatch' && (inputs.ref || inputs.tag || github.ref) || github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
build-apk:
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
artifact-name: ${{ steps.mode.outputs.artifact-name }}
|
|
publish: ${{ steps.mode.outputs.publish }}
|
|
release-tag: ${{ steps.mode.outputs.release-tag }}
|
|
source-tag: ${{ steps.mode.outputs.source-tag }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && (inputs.publish && inputs.tag || inputs.ref || github.ref) || github.ref }}
|
|
|
|
- name: Resolve build mode
|
|
id: mode
|
|
env:
|
|
INPUT_PUBLISH: ${{ inputs.publish }}
|
|
INPUT_REF: ${{ inputs.ref }}
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
publish=false
|
|
release_tag=""
|
|
|
|
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
|
|
publish=true
|
|
source_tag="${GITHUB_REF_NAME}"
|
|
elif [[ "${INPUT_PUBLISH:-false}" == "true" ]]; then
|
|
if [[ -z "${INPUT_TAG:-}" ]]; then
|
|
echo "::error::tag is required when publish is enabled."
|
|
exit 1
|
|
fi
|
|
if [[ -n "${INPUT_REF:-}" ]]; then
|
|
echo "::error::ref cannot be combined with publish; the release tag is always the source."
|
|
exit 1
|
|
fi
|
|
publish=true
|
|
source_tag="${INPUT_TAG}"
|
|
fi
|
|
|
|
if [[ "${publish}" == "true" ]]; then
|
|
release_env="$(node scripts/emit-release-env.mjs --source-tag "${source_tag}")"
|
|
printf '%s\n' "${release_env}" >> "$GITHUB_ENV"
|
|
release_tag="$(printf '%s\n' "${release_env}" | sed -n 's/^RELEASE_TAG=//p')"
|
|
artifact_name="paseo-${release_tag}-android.apk"
|
|
else
|
|
package_version="$(node -p "require('./package.json').version")"
|
|
short_sha="$(git rev-parse --short=12 HEAD)"
|
|
artifact_name="paseo-v${package_version}-${short_sha}-android.apk"
|
|
fi
|
|
|
|
{
|
|
echo "artifact-name=${artifact_name}"
|
|
echo "publish=${publish}"
|
|
echo "release-tag=${release_tag}"
|
|
echo "source-tag=${source_tag:-}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Building ${artifact_name}; publish=${publish}"
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
registry-url: "https://npm.pkg.github.com"
|
|
scope: "@boudra"
|
|
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
cache: gradle
|
|
|
|
- name: Setup Android SDK
|
|
uses: android-actions/setup-android@v3
|
|
|
|
- name: Install JS dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Restore production Android credentials
|
|
env:
|
|
ANDROID_GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON_BASE64 }}
|
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
required=(
|
|
ANDROID_GOOGLE_SERVICES_JSON_BASE64
|
|
ANDROID_KEY_ALIAS
|
|
ANDROID_KEY_PASSWORD
|
|
ANDROID_KEYSTORE_BASE64
|
|
ANDROID_KEYSTORE_PASSWORD
|
|
)
|
|
for name in "${required[@]}"; do
|
|
if [[ -z "${!name:-}" ]]; then
|
|
echo "::error::Missing required repository secret: ${name}"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
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: $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_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
|
|
-Dorg.gradle.workers.max=1
|
|
-Dorg.gradle.daemon=false
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
npm run build:app-deps
|
|
npm --prefix packages/app run build:terminal-webview
|
|
|
|
cd packages/app
|
|
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
|
|
with:
|
|
name: ${{ steps.mode.outputs.artifact-name }}
|
|
path: ${{ runner.temp }}/${{ steps.mode.outputs.artifact-name }}
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
publish-apk:
|
|
needs: build-apk
|
|
if: needs.build-apk.outputs.publish == 'true'
|
|
permissions:
|
|
contents: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
sparse-checkout: scripts
|
|
ref: ${{ needs.build-apk.outputs.source-tag }}
|
|
|
|
- name: Resolve release metadata
|
|
shell: bash
|
|
run: node scripts/emit-release-env.mjs --source-tag "${{ needs.build-apk.outputs.source-tag }}" >> "$GITHUB_ENV"
|
|
|
|
- name: Ensure GitHub release exists
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
|
|
exit 0
|
|
fi
|
|
|
|
release_args=(
|
|
release create "$RELEASE_TAG"
|
|
--repo "${{ github.repository }}"
|
|
--title "Paseo $RELEASE_TAG"
|
|
--notes ""
|
|
)
|
|
|
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
|
release_args+=(--prerelease)
|
|
fi
|
|
|
|
if ! gh "${release_args[@]}"; then
|
|
echo "Release creation raced with another workflow; continuing."
|
|
fi
|
|
|
|
- name: Download APK workflow artifact
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: ${{ needs.build-apk.outputs.artifact-name }}
|
|
path: ${{ runner.temp }}/apk
|
|
|
|
- name: Upload APK to GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
gh release upload \
|
|
"$RELEASE_TAG" \
|
|
"$RUNNER_TEMP/apk/${{ needs.build-apk.outputs.artifact-name }}" \
|
|
--clobber \
|
|
--repo "${{ github.repository }}"
|