* add staging ios app build script * feat(api): add OpenTelemetry LGTM stack for local observability Wire OTel traces, metrics, and logs through a collector to Tempo, Prometheus, and Loki, with custom LLM instrumentation, dev compose overlay, Grafana provisioning, env templates, and development.sh auto-start. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: expand observability, harden dev tooling, and fix expo staging UX Add business and LLM Prometheus metrics with Grafana dashboards, alerting, and a metrics verification script. Wire telemetry through adapters and core LLM paths, and document the local LGTM workflow. Fix development.sh for macOS bash 3.2, open Grafana and eval-web in Chrome, and repair eval-web auto-open (unbound EVAL_WEB_BROWSER_SCHEDULED). Merge internal-eval into the main dev script with improved compose handling. Require EXPO_PUBLIC_* at build time, improve iOS HTTP ATS for staging IPs, show memoir empty state instead of load errors when no chapters exist, and add jest env setup plus chapter list response normalization. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: enable Grafana Assistant Cursor plugin Co-authored-by: Cursor <cursoragent@cursor.com> * fix: memoir empty state and repair withdrawn 0020_chapters_book_id stamp Show empty memoir UI when the chapter list succeeds with no items; treat auth/404 as non-fatal. Extend alembic revision repair so local dev DBs stamped with the removed 0020_chapters_book_id migration can roll back and upgrade to 0019. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Kevin <kevin@brighteng.org> Co-authored-by: Cursor <cursoragent@cursor.com>
275 lines
10 KiB
YAML
275 lines
10 KiB
YAML
# 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
|
||
|
||
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
|
||
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 }}
|