# 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 # push v*.*.* → prod → node scripts/use-env.js production → .env.production # # 手动触发 workflow_dispatch:可选 dev / stage / prod(dev 用 .env.development,便于打内部测试包) # # Repository secrets(与 android-release.yml 共用同一套即可): # ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD name: App Expo Deploy on: push: branches: [main] tags: ['v*.*.*'] paths: - "app-expo/**" - ".github/workflows/app-expo-deploy.yml" workflow_dispatch: inputs: environment: description: '部署环境' 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.ref }}" == refs/tags/v* ]]; then echo "env=prod" elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then echo "env=${{ github.event.inputs.environment }}" else # push 到 main(本 workflow 仅监听 main 与 tag) echo "env=stage" fi >> $GITHUB_OUTPUT - 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 - name: Quality checks (non-prod) if: steps.env.outputs.env != 'prod' 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 - 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 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 }}