# App Expo:CI 内生成 Android 签名 APK(expo prebuild + Gradle assembleRelease) # 使用 app-expo/plugins/withAndroidReleaseSigning:在 android/app 放置 keystore.properties + jks 后打 release 包。 # # 产物说明: # - 以下为「真机向」release APK;Google Play 上架更推荐 AAB(按设备 ABI 下发)。 # - 构建步骤会将 reactNativeArchitectures 设为仅 arm64-v8a(更小;不含 x86/x86_64,多数模拟器无法安装)。 # - 若需兼容旧 32 位 ARM 设备,可将该属性改为 armeabi-v7a,arm64-v8a(体积会增大)。 # - 若需用 x86 模拟器验证此 APK,需改回含 x86 的 ABI 或另建 job。 # # 环境映射(与后端 api 一致:main → 预发 staging,tag → 生产 production): # push main → stage → node scripts/use-env.js staging → env/staging → .env # push v*.*.* → prod → node scripts/use-env.js production → env/production → .env # # 手动触发 workflow_dispatch: # - dev:内部测试包,使用 env/development # - stage:仅用于 main / master 补跑 Staging release,使用 env/staging # - prod:用于 vMAJOR.MINOR.PATCH tag,或在 main / master 上填写 version 后发正式 Release # # Repository secrets(与 android-release.yml 共用同一套即可): # ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD # # 版本注入(app.config.ts 读取,与 Gradle -PversionName/-PversionCode 对齐): # APP_VERSION — tag / 手动 version / staging 时 package.json # APP_BUILD_NUMBER — GITHUB_RUN_NUMBER(Android versionCode / iOS buildNumber) name: App Expo Deploy on: push: branches: [main] tags: ['v*.*.*'] paths: - "app-expo/**" - ".github/workflows/app-expo-deploy.yml" workflow_dispatch: inputs: environment: description: '部署环境(stage 请在 main 上补跑;prod 请使用 v tag 或在 main 上填写 version)' required: true type: choice options: - dev - stage - prod default: dev version: description: '版本号 (prod 手动发版时使用,如 1.0.0)' required: false type: string concurrency: group: app-expo-deploy-${{ github.ref }} cancel-in-progress: true env: APP_SLUG: life-echo APP_DISPLAY_NAME: Life Echo EXPO_NO_TELEMETRY: 1 FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: deploy: name: "Android APK" runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: ${{ startsWith(github.ref, 'refs/tags/') && '0' || '1' }} - name: Determine environment id: env run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then echo "env=${{ github.event.inputs.environment }}" elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then echo "env=prod" else # push 到 main(本 workflow 仅监听 main 与 tag) echo "env=stage" fi >> $GITHUB_OUTPUT - name: Validate manual release ref if: github.event_name == 'workflow_dispatch' run: | ENVIRONMENT="${{ steps.env.outputs.env }}" REF="${{ github.ref }}" REF_NAME="${{ github.ref_name }}" VERSION="${{ github.event.inputs.version }}" case "$ENVIRONMENT" in dev) echo "dev 构建允许使用当前 ref: $REF_NAME" ;; stage) if [ "$REF_NAME" != "main" ] && [ "$REF_NAME" != "master" ]; then echo "::error::Staging release 只允许在 main / master 上手动补跑,当前 ref 为 '$REF_NAME'。" exit 1 fi ;; prod) if [[ "$REF" == refs/tags/v* && "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "使用 tag $REF_NAME 发正式 Release。" elif { [ "$REF_NAME" = "main" ] || [ "$REF_NAME" = "master" ]; } && [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "使用 main / master 和显式版本 v$VERSION 发正式 Release。" else echo "::error::Production release 需要选择 vMAJOR.MINOR.PATCH tag,或在 main / master 上填写语义化 version。" exit 1 fi ;; *) echo "::error::未知部署环境 '$ENVIRONMENT'。" exit 1 ;; esac - name: Set up Node.js uses: actions/setup-node@v5 with: node-version: 22.22.1 cache: npm cache-dependency-path: app-expo/package-lock.json - name: Install dependencies working-directory: app-expo run: npm ci # TODO: Restore quality checks before staging/prod release once CI tests are stable. # - name: Quality checks # working-directory: app-expo # run: | # npm run format:check # npm run lint # npm run test:ci - name: Set API environment working-directory: app-expo run: | case "${{ steps.env.outputs.env }}" in prod) node scripts/use-env.js production ;; stage) node scripts/use-env.js staging ;; *) node scripts/use-env.js development ;; esac for legacy in .env.production .env.staging .env.development; do if [ -f "$legacy" ]; then echo "::error::Legacy $legacy must not exist at app-expo root (overrides .env on Release builds)." exit 1 fi done - name: Determine version id: version working-directory: app-expo run: | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/v}" TAG_NAME="${GITHUB_REF#refs/tags/}" elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ -n "${{ github.event.inputs.version }}" ]]; then VERSION="${{ github.event.inputs.version }}" TAG_NAME="v${VERSION}" else VERSION=$(node -p "require('./package.json').version") TAG_NAME="v${VERSION}" fi VERSION_CODE="${GITHUB_RUN_NUMBER}" echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT echo "version_code=${VERSION_CODE}" >> $GITHUB_OUTPUT echo "apk_name=${{ env.APP_SLUG }}-v${VERSION}-release.apk" >> $GITHUB_OUTPUT echo "版本名: ${VERSION}, versionCode: ${VERSION_CODE}, tag: ${TAG_NAME}" - name: Set up JDK uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Expo prebuild (Android) working-directory: app-expo env: APP_VERSION: ${{ steps.version.outputs.version }} APP_BUILD_NUMBER: ${{ steps.version.outputs.version_code }} run: npx expo prebuild --platform android --clean - name: Release APK — arm64 only (real devices; smaller) working-directory: app-expo/android run: | set -euo pipefail if grep -q '^reactNativeArchitectures=' gradle.properties; then sed -i 's/^reactNativeArchitectures=.*/reactNativeArchitectures=arm64-v8a/' gradle.properties else echo 'reactNativeArchitectures=arm64-v8a' >> gradle.properties fi grep -E '^reactNativeArchitectures=' gradle.properties - name: Decode keystore working-directory: app-expo/android/app run: | echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > release-keystore.jks cat > keystore.properties <> $GITHUB_OUTPUT du -h "$FINAL_PATH" - name: Upload APK artifact uses: actions/upload-artifact@v6 with: name: ${{ steps.version.outputs.apk_name }} path: app-expo/android/${{ steps.apk.outputs.apk_path }} retention-days: ${{ steps.env.outputs.env == 'prod' && '90' || (steps.env.outputs.env == 'dev' && '14' || '30') }} - name: Generate Release Notes (prod) if: steps.env.outputs.env == 'prod' && startsWith(github.ref, 'refs/tags/') id: release_notes run: | TAG_NAME="${{ steps.version.outputs.tag_name }}" PREV_TAG=$(git tag --sort=-creatordate | grep '^v' | sed -n '2p') if [ -n "$PREV_TAG" ]; then CHANGES=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (%h)" --no-merges) else CHANGES=$(git log --pretty=format:"- %s (%h)" --no-merges -20) fi { echo "notes<> $GITHUB_OUTPUT - name: Create GitHub Release (prod) if: steps.env.outputs.env == 'prod' uses: softprops/action-gh-release@v2.5.0 with: tag_name: ${{ steps.version.outputs.tag_name }} name: "${{ env.APP_DISPLAY_NAME }} ${{ steps.version.outputs.tag_name }}" body: ${{ steps.release_notes.outputs.notes || 'Release' }} draft: false prerelease: false files: app-expo/android/${{ steps.apk.outputs.apk_path }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}