mirror of
https://github.com/kuaifan/dootask.git
synced 2026-10-30 06:20:44 +00:00
- ./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 签名脚本
301 lines
12 KiB
YAML
301 lines
12 KiB
YAML
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"
|