dootask/.github/workflows/app-internal.yml
kuaifan 945e5b10f2 feat(app): 新增 Expo 内测构建链路并更新 resources/app
- ./cmd appexpo [build] / appexpo npm:编译前端并同步到 resources/app/assets/web
- electron/build.js 新增 app-expo 目标,旧 eeui 流程不变
- 新增仅手动触发的 Expo App Internal workflow(APK / IPA artifact)
- resources/app 更新至 M3 会议与 M4 签名脚本
2026-10-17 23:08:24 +00:00

301 lines
12 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.

name: Expo App Internal
# 仅内测 artifact;与 eeui 正式发布并行,不创建 Release,也不上传 R2 / 商店。
on:
workflow_dispatch:
inputs:
platform:
description: 构建平台
type: choice
options: [all, android, ios]
default: all
ios_export_method:
description: iOS profiles 类型(必须包含测试设备 UDID)
type: choice
options: [ad-hoc, development]
default: ad-hoc
permissions:
contents: read
concurrency:
group: app-internal-${{ github.ref }}
cancel-in-progress: false
env:
CI: '1'
NODE_ENV: production
# 内测 Release 必须使用包内前端,禁止开发机 .env 覆盖 WebView 地址。
EXPO_NO_DOTENV: '1'
EXPO_PUBLIC_WEB_URL: ''
jobs:
prepare:
name: Prepare bundled web assets
runs-on: ubuntu-latest
timeout-minutes: 30
# 同名 iOS secrets 放在独立环境,避免覆盖 build 环境中的 App Store profiles。
environment: app-internal
outputs:
version: ${{ steps.version.outputs.version }}
ios_ready: ${{ steps.signing.outputs.ios_ready }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Check Expo checkout
run: test -f resources/app/app.config.ts
- uses: actions/setup-node@v4
with:
# Expo 58 / RN 0.88 需要 Node ^22.13 / ^24.3 或更新版本。
node-version: '24'
- name: Read version
id: version
run: node -e 'require("fs").appendFileSync(process.env.GITHUB_OUTPUT, `version=${require("./package.json").version}\n`)'
- name: Check iOS signing inputs
id: signing
env:
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 }}
BUILD_PLATFORM: ${{ inputs.platform }}
run: |
if [ "$BUILD_PLATFORM" = android ]; then
echo 'ios_ready=false' >> "$GITHUB_OUTPUT"
elif [ -n "$IOS_CERTIFICATE_BASE64" ] && [ -n "$IOS_PROVISION_PROFILE_BASE64" ] && [ -n "$IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64" ]; then
echo 'ios_ready=true' >> "$GITHUB_OUTPUT"
else
echo 'ios_ready=false' >> "$GITHUB_OUTPUT"
echo '::notice::跳过 iOS:缺少签名证书或主 App / 分享扩展 profile。'
echo 'iOS 已跳过:需要 IOS_CERTIFICATE_BASE64、IOS_PROVISION_PROFILE_BASE64、IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64;密码通过 IOS_CERTIFICATE_PASSWORD 提供。profiles 必须为包含设备 UDID 的内测类型,不能使用 App Store profiles。' >> "$GITHUB_STEP_SUMMARY"
fi
- name: Install frontend dependencies
# 主仓库没有追踪 npm lockfile;须显式包含 Vite 等 devDependencies。
run: npm install --include=dev --no-package-lock --no-audit --no-fund
- name: Install build script dependencies
working-directory: electron
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
run: npm install --include=dev --no-package-lock --no-audit --no-fund
- name: Build and copy web assets
run: ./cmd appexpo build
- name: Upload bundled web assets
uses: actions/upload-artifact@v4
with:
name: expo-web-assets
path: |
resources/app/assets/web/
resources/app/version.json
if-no-files-found: error
retention-days: 7
android:
name: Android internal APK
needs: prepare
if: inputs.platform != 'ios'
runs-on: ubuntu-latest
timeout-minutes: 90
environment: app-internal
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/download-artifact@v4
with:
name: expo-web-assets
path: resources/app
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: resources/app/package-lock.json
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v3
- name: Install Expo dependencies
working-directory: resources/app
run: npm ci --include=dev --no-audit --no-fund
- name: Prebuild Android
working-directory: resources/app
run: npx expo prebuild --platform android --clean --no-install
- name: Prepare optional Android keystore
id: signing
env:
# 旧 publish.yml 未定义 Android 签名 secrets;这四项仅供新链路使用。
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ] && [ -n "$ANDROID_KEYSTORE_PASSWORD" ] && [ -n "$ANDROID_KEY_ALIAS" ] && [ -n "$ANDROID_KEY_PASSWORD" ]; then
umask 077
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/dootask-internal.jks"
echo "ANDROID_KEYSTORE_PATH=$RUNNER_TEMP/dootask-internal.jks" >> "$GITHUB_ENV"
echo 'mode=keystore' >> "$GITHUB_OUTPUT"
echo 'Android Release 包使用提供的 keystore 签名。' >> "$GITHUB_STEP_SUMMARY"
else
echo 'mode=debug' >> "$GITHUB_OUTPUT"
echo '::notice::Android 签名 secrets 不完整,Release 包使用 debug 签名。'
echo 'Android Release 包使用 debug 签名(签名 secrets 缺失或不完整),用于全新安装内测;与正式包签名不同,不能直接覆盖正式安装。' >> "$GITHUB_STEP_SUMMARY"
fi
- name: Assemble Release
working-directory: resources/app/android
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: ./gradlew :app:assembleRelease --no-daemon --init-script ../scripts/android-signing.gradle
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: dootask-expo-android-${{ needs.prepare.outputs.version }}-${{ steps.signing.outputs.mode }}
path: resources/app/android/app/build/outputs/apk/release/*.apk
if-no-files-found: error
retention-days: 14
- name: Remove Android keystore
if: always()
run: rm -f "$RUNNER_TEMP/dootask-internal.jks"
ios:
name: iOS internal IPA
needs: prepare
if: needs.prepare.outputs.ios_ready == 'true'
runs-on: macos-26
timeout-minutes: 90
environment: app-internal
env:
IOS_EXPORT_METHOD: ${{ inputs.ios_export_method }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/download-artifact@v4
with:
name: expo-web-assets
path: resources/app
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: resources/app/package-lock.json
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Install Expo dependencies
working-directory: resources/app
run: npm ci --include=dev --no-audit --no-fund
- name: Prebuild iOS
working-directory: resources/app
run: npx expo prebuild --platform ios --clean --no-install
- name: Install CocoaPods
working-directory: resources/app/ios
run: pod install
- name: Import signing certificate
env:
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
umask 077
SIGNING_DIR="$RUNNER_TEMP/dootask-ios-signing"
mkdir -p "$SIGNING_DIR"
KEYCHAIN_PATH="$SIGNING_DIR/signing.keychain-db"
KEYCHAIN_PASSWORD=$(openssl rand -hex 20)
security list-keychains -d user > "$SIGNING_DIR/original-keychains.txt"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "$IOS_CERTIFICATE_BASE64" | base64 --decode > "$SIGNING_DIR/certificate.p12"
security import "$SIGNING_DIR/certificate.p12" -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
- name: Install and validate internal profiles
working-directory: resources/app
env:
IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 }}
run: python3 scripts/prepare-ios-signing.py "$RUNNER_TEMP/dootask-ios-signing"
- name: Configure product target signing via Expo mod
working-directory: resources/app
run: node scripts/configure-ios-signing.js "$RUNNER_TEMP/dootask-ios-signing/signing.json"
- name: Archive iOS Release
working-directory: resources/app
run: |
xcodebuild archive \
-workspace ios/DooTask.xcworkspace \
-scheme DooTask \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/DooTask.xcarchive" \
-derivedDataPath "$RUNNER_TEMP/dootask-derived-data"
- name: Export internal IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/DooTask.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/dootask-ios-signing/ExportOptions.plist" \
-exportPath "$RUNNER_TEMP/dootask-ipa"
- name: Upload IPA
uses: actions/upload-artifact@v4
with:
name: dootask-expo-ios-${{ needs.prepare.outputs.version }}-${{ inputs.ios_export_method }}
path: ${{ runner.temp }}/dootask-ipa/*.ipa
if-no-files-found: error
retention-days: 14
- name: Remove signing material
if: always()
run: |
SIGNING_DIR="$RUNNER_TEMP/dootask-ios-signing"
if [ -f "$SIGNING_DIR/signing.json" ]; then
python3 - "$SIGNING_DIR/signing.json" <<'PY'
import json
from pathlib import Path
import sys
for filename in json.loads(Path(sys.argv[1]).read_text())["installed_profiles"]:
Path(filename).unlink(missing_ok=True)
PY
fi
if [ -f "$SIGNING_DIR/original-keychains.txt" ]; then
python3 - "$SIGNING_DIR/original-keychains.txt" <<'PY'
from pathlib import Path
import shlex
import subprocess
import sys
subprocess.run(["security", "list-keychains", "-d", "user", "-s", *shlex.split(Path(sys.argv[1]).read_text())], check=True)
PY
fi
security delete-keychain "$SIGNING_DIR/signing.keychain-db" 2>/dev/null || true
rm -rf "$SIGNING_DIR"