Files
life-echo/.github/workflows/app-expo-deploy.yml
Sully cb84c00eca 添加staging release workflow (#22)
* update variable name

* update docker port

* fix alembic migration files

* 给远端 SSH 调用加了 keepalive

* fix app-expo code file format

* comment out quality test threshold

---------

Co-authored-by: Kevin <kevin@brighteng.org>
2026-05-11 11:33:07 +08:00

269 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# App ExpoCI 内生成 Android 签名 APKexpo prebuild + Gradle assembleRelease
# 使用 app-expo/plugins/withAndroidReleaseSigning在 android/app 放置 keystore.properties + jks 后打 release 包。
#
# 产物说明:
# - 以下为「真机向」release APKGoogle Play 上架更推荐 AAB按设备 ABI 下发)。
# - 构建步骤会将 reactNativeArchitectures 设为仅 arm64-v8a更小不含 x86/x86_64多数模拟器无法安装
# - 若需兼容旧 32 位 ARM 设备,可将该属性改为 armeabi-v7a,arm64-v8a体积会增大
# - 若需用 x86 模拟器验证此 APK需改回含 x86 的 ABI 或另建 job。
#
# 环境映射(与后端 api 一致main → 预发 stagingtag → 生产 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内部测试包使用 .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
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
- 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 <<EOF
storeFile=release-keystore.jks
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
EOF
sed -i 's/^[[:space:]]*//' keystore.properties
- name: Build release APK
working-directory: app-expo/android
env:
GRADLE_OPTS: '-Xmx4g -Dorg.gradle.daemon=false'
run: |
chmod +x gradlew
./gradlew assembleRelease --no-daemon \
-PversionName="${{ steps.version.outputs.version }}" \
-PversionCode="${{ steps.version.outputs.version_code }}"
- name: Locate APK
id: apk
working-directory: app-expo/android
run: |
APK_PATH=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
if [ -z "$APK_PATH" ]; then
echo "未找到 Release APK"
exit 1
fi
FINAL_NAME="${{ steps.version.outputs.apk_name }}"
FINAL_PATH="app/build/outputs/apk/release/${FINAL_NAME}"
mv "$APK_PATH" "$FINAL_PATH"
echo "apk_path=${FINAL_PATH}" >> $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<<EOF"
echo "## ${{ env.APP_DISPLAY_NAME }} ${TAG_NAME}"
echo ""
echo "### 更新内容"
echo ""
echo "${CHANGES}"
echo ""
echo "---"
echo "构建编号: #${GITHUB_RUN_NUMBER}"
echo "EOF"
} >> $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 }}