mirror of
https://github.com/kuaifan/dootask.git
synced 2026-10-30 06:20:44 +00:00
ci(app): App 改由 Expo 壳构建发布,移除旧 eeui 子模块 resources/mobile
- 新增可复用工作流 app-build.yml(web 资源 / Android 正式签名 APK / iOS IPA) - publish.yml 的 Android 改用 Expo 构建并上传 R2 与 Release - ios-publish.yml 改为构建 Expo App 并上传 App Store Connect(构建号自动递增) - app-internal.yml 改为调用 app-build.yml 出内测包 - 移除 ./cmd appbuild/eeui 与 electron/build.js 的 eeui 打包流程
This commit is contained in:
parent
a0e6d355f2
commit
bad7634f27
407
.github/workflows/app-build.yml
vendored
Normal file
407
.github/workflows/app-build.yml
vendored
Normal file
@ -0,0 +1,407 @@
|
||||
name: "App Build"
|
||||
|
||||
# Expo App(resources/app 子模块)构建,供 publish.yml(Android 发版)、ios-publish.yml(App Store)
|
||||
# 与 app-internal.yml(内测)调用;只产出 artifact(app-android / app-ios),上传发布由调用方负责。
|
||||
#
|
||||
# Secrets(仓库级,调用方 secrets: inherit):
|
||||
# ANDROID_KEYSTORE_BASE64 / ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD Android 正式签名
|
||||
# IOS_CERTIFICATE_BASE64 / IOS_CERTIFICATE_PASSWORD Apple Distribution 证书
|
||||
# IOS_PROVISION_PROFILE_BASE64 / IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 App Store profiles
|
||||
# IOS_INTERNAL_PROVISION_PROFILE_BASE64 / IOS_INTERNAL_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 内测 profiles(可选)
|
||||
# ASC_API_KEY_ID / ASC_ISSUER_ID / ASC_API_KEY_P8_BASE64 App Store 构建号查询
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
description: all / android / ios
|
||||
type: string
|
||||
default: all
|
||||
ios_export_method:
|
||||
description: app-store / ad-hoc / development
|
||||
type: string
|
||||
default: ad-hoc
|
||||
release:
|
||||
description: 正式发布:签名缺失或不符时失败,不退化为 debug 签名或跳过
|
||||
type: boolean
|
||||
default: false
|
||||
outputs:
|
||||
version:
|
||||
value: ${{ jobs.prepare.outputs.version }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: '1'
|
||||
NODE_ENV: production
|
||||
# 必须使用包内前端,禁止开发机 .env 覆盖 WebView 地址
|
||||
EXPO_NO_DOTENV: '1'
|
||||
EXPO_PUBLIC_WEB_URL: ''
|
||||
# 旧版正式包(eeui)使用的签名证书;新包必须一致才能覆盖升级
|
||||
ANDROID_CERT_SHA256: CE:30:13:6F:B9:28:56:13:C7:41:CB:CE:E3:6B:3C:DB:1F:01:D5:26:A7:3F:17:4E:1E:B2:B8:37:F1:E4:A2:B6
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare bundled web assets
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
ios_ready: ${{ steps.signing.outputs.ios_ready }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# 只取 Expo 子模块(drawio 体积大且与 App 无关),使用主仓库锁定的提交
|
||||
- name: Checkout Expo submodule
|
||||
run: git submodule update --init resources/app
|
||||
|
||||
- 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:
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
RELEASE: ${{ inputs.release }}
|
||||
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
|
||||
APP_PROFILE: ${{ inputs.ios_export_method == 'app-store' && secrets.IOS_PROVISION_PROFILE_BASE64 || secrets.IOS_INTERNAL_PROVISION_PROFILE_BASE64 }}
|
||||
SHARE_PROFILE: ${{ inputs.ios_export_method == 'app-store' && secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 || secrets.IOS_INTERNAL_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 }}
|
||||
run: |
|
||||
if [ "$PLATFORM" = android ]; then
|
||||
echo 'ios_ready=false' >> "$GITHUB_OUTPUT"
|
||||
elif [ -n "$IOS_CERTIFICATE_BASE64" ] && [ -n "$APP_PROFILE" ] && [ -n "$SHARE_PROFILE" ]; then
|
||||
echo 'ios_ready=true' >> "$GITHUB_OUTPUT"
|
||||
elif [ "$RELEASE" = true ]; then
|
||||
echo '::error::缺少 iOS 证书或 App Store profiles(IOS_CERTIFICATE_BASE64 / IOS_PROVISION_PROFILE_BASE64 / IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64)'
|
||||
exit 1
|
||||
else
|
||||
echo 'ios_ready=false' >> "$GITHUB_OUTPUT"
|
||||
echo '::notice::跳过 iOS:缺少证书或内测 profiles(IOS_INTERNAL_PROVISION_PROFILE_BASE64 / IOS_INTERNAL_SHARE_EXTENSION_PROVISION_PROFILE_BASE64)'
|
||||
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: app-web-assets
|
||||
path: |
|
||||
resources/app/assets/web/
|
||||
resources/app/version.json
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
android:
|
||||
name: Android APK
|
||||
needs: prepare
|
||||
if: inputs.platform != 'ios'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Free disk space
|
||||
run: sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/.ghcup /usr/local/share/boost /opt/hostedtoolcache/CodeQL
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Expo submodule
|
||||
run: git submodule update --init resources/app
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: app-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 Android keystore
|
||||
id: signing
|
||||
env:
|
||||
RELEASE: ${{ inputs.release }}
|
||||
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-release.jks"
|
||||
echo "ANDROID_KEYSTORE_PATH=$RUNNER_TEMP/dootask-release.jks" >> "$GITHUB_ENV"
|
||||
echo 'mode=release' >> "$GITHUB_OUTPUT"
|
||||
elif [ "$RELEASE" = true ]; then
|
||||
echo '::error::缺少 Android 签名 secrets(ANDROID_KEYSTORE_BASE64 / ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD)'
|
||||
exit 1
|
||||
else
|
||||
echo 'mode=debug' >> "$GITHUB_OUTPUT"
|
||||
echo '::notice::Android 签名 secrets 不完整,使用 debug 签名(不能覆盖安装正式包)'
|
||||
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 }}
|
||||
# R8 混淆元空间占用大,默认 512m 会 OOM
|
||||
run: ./gradlew :app:assembleRelease --no-daemon --init-script ../scripts/android-signing.gradle "-Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=2g"
|
||||
|
||||
- name: Rename and verify APK
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
MODE: ${{ steps.signing.outputs.mode }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/apk"
|
||||
# 文件名与旧版一致:官网下载索引与 App 检查更新按 android 平台取 .apk
|
||||
apk="$RUNNER_TEMP/apk/DooTask-v${VERSION}-android.apk"
|
||||
mv resources/app/android/app/build/outputs/apk/release/app-release.apk "$apk"
|
||||
apksigner=$(ls "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -1)
|
||||
"$apksigner" verify "$apk"
|
||||
if [ "$MODE" = release ]; then
|
||||
actual=$("$apksigner" verify --print-certs "$apk" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p' | head -1)
|
||||
expected=$(echo "$ANDROID_CERT_SHA256" | tr -d ':' | tr 'A-F' 'a-f')
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "::error::APK 签名证书与正式包不一致($actual),无法覆盖升级"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
ls -la "$RUNNER_TEMP/apk"
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-android
|
||||
path: ${{ runner.temp }}/apk/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Remove Android keystore
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/dootask-release.jks"
|
||||
|
||||
ios:
|
||||
name: iOS IPA
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.ios_ready == 'true'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
IOS_EXPORT_METHOD: ${{ inputs.ios_export_method }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Expo submodule
|
||||
run: git submodule update --init resources/app
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: app-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: Resolve App Store build number
|
||||
if: inputs.ios_export_method == 'app-store'
|
||||
env:
|
||||
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ruby <<'RUBY'
|
||||
require 'base64'
|
||||
require 'json'
|
||||
require 'net/http'
|
||||
require 'openssl'
|
||||
require 'uri'
|
||||
|
||||
BUNDLE_ID = 'com.dootask.task'
|
||||
|
||||
def base64url(value)
|
||||
Base64.urlsafe_encode64(value).delete('=')
|
||||
end
|
||||
|
||||
def asc_token
|
||||
private_key = OpenSSL::PKey.read(Base64.decode64(ENV.fetch('ASC_API_KEY_P8_BASE64')))
|
||||
now = Time.now.to_i
|
||||
header = { alg: 'ES256', kid: ENV.fetch('ASC_API_KEY_ID'), typ: 'JWT' }
|
||||
payload = { iss: ENV.fetch('ASC_ISSUER_ID'), iat: now, exp: now + 20 * 60, aud: 'appstoreconnect-v1' }
|
||||
unsigned = "#{base64url(header.to_json)}.#{base64url(payload.to_json)}"
|
||||
# ES256 签名须为 r||s 各 32 字节,而非 DER
|
||||
der = OpenSSL::ASN1.decode(private_key.sign('SHA256', unsigned))
|
||||
signature = der.value.map { |integer| integer.value.to_s(2).rjust(32, "\0")[-32, 32] }.join
|
||||
"#{unsigned}.#{base64url(signature)}"
|
||||
end
|
||||
|
||||
def asc_get(path, params, token)
|
||||
uri = URI::HTTPS.build(host: 'api.appstoreconnect.apple.com', path: path, query: URI.encode_www_form(params))
|
||||
loop do
|
||||
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
request['Authorization'] = "Bearer #{token}"
|
||||
http.request(request)
|
||||
end
|
||||
abort "App Store Connect API request failed: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
||||
parsed = JSON.parse(response.body)
|
||||
yield parsed
|
||||
next_link = parsed.dig('links', 'next')
|
||||
break unless next_link
|
||||
uri = URI(next_link)
|
||||
end
|
||||
end
|
||||
|
||||
token = asc_token
|
||||
app_id = nil
|
||||
asc_get('/v1/apps', { 'filter[bundleId]' => BUNDLE_ID, 'limit' => 1 }, token) do |page|
|
||||
app_id = page.fetch('data').first&.fetch('id')
|
||||
end
|
||||
abort "App Store Connect app not found for bundle id #{BUNDLE_ID}" unless app_id
|
||||
|
||||
versions = []
|
||||
asc_get('/v1/builds', { 'filter[app]' => app_id, 'fields[builds]' => 'version', 'limit' => 200 }, token) do |page|
|
||||
versions.concat(page.fetch('data').map { |build| build.dig('attributes', 'version').to_s })
|
||||
end
|
||||
max_build = versions.select { |v| v.match?(/\A\d+\z/) }.map(&:to_i).max || 0
|
||||
code = JSON.parse(File.read('resources/app/version.json')).fetch('codeVerson').to_i
|
||||
next_build = [max_build + 1, code].max
|
||||
File.open(ENV.fetch('GITHUB_ENV'), 'a') { |file| file.puts "IOS_BUILD_NUMBER=#{next_build}" }
|
||||
puts "App Store Connect 最大构建号 #{max_build},本次构建号 #{next_build}"
|
||||
RUBY
|
||||
|
||||
- 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 profiles
|
||||
working-directory: resources/app
|
||||
env:
|
||||
IOS_PROVISION_PROFILE_BASE64: ${{ inputs.ios_export_method == 'app-store' && secrets.IOS_PROVISION_PROFILE_BASE64 || secrets.IOS_INTERNAL_PROVISION_PROFILE_BASE64 }}
|
||||
IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64: ${{ inputs.ios_export_method == 'app-store' && secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 || secrets.IOS_INTERNAL_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 -quiet \
|
||||
-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 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: app-ios
|
||||
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"
|
||||
290
.github/workflows/app-internal.yml
vendored
290
.github/workflows/app-internal.yml
vendored
@ -1,6 +1,8 @@
|
||||
name: Expo App Internal
|
||||
name: "App Internal"
|
||||
|
||||
# 仅内测 artifact;与 eeui 正式发布并行,不创建 Release,也不上传 R2 / 商店。
|
||||
# 内测出包:只产出 artifact,不创建 Release,也不上传 R2 / 商店。
|
||||
# Android 使用正式签名(可覆盖安装正式包);iOS 需另配内测 profiles(含测试设备 UDID),未配置时跳过:
|
||||
# IOS_INTERNAL_PROVISION_PROFILE_BASE64 / IOS_INTERNAL_SHARE_EXTENSION_PROVISION_PROFILE_BASE64
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@ -10,7 +12,7 @@ on:
|
||||
options: [all, android, ios]
|
||||
default: all
|
||||
ios_export_method:
|
||||
description: iOS profiles 类型(必须包含测试设备 UDID)
|
||||
description: iOS 内测 profiles 类型(必须包含测试设备 UDID)
|
||||
type: choice
|
||||
options: [ad-hoc, development]
|
||||
default: ad-hoc
|
||||
@ -22,279 +24,11 @@ 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"
|
||||
build:
|
||||
name: Build
|
||||
uses: ./.github/workflows/app-build.yml
|
||||
with:
|
||||
platform: ${{ inputs.platform || 'all' }}
|
||||
ios_export_method: ${{ inputs.ios_export_method || 'ad-hoc' }}
|
||||
secrets: inherit
|
||||
|
||||
402
.github/workflows/ios-publish.yml
vendored
402
.github/workflows/ios-publish.yml
vendored
@ -1,5 +1,7 @@
|
||||
name: "iOS Publish"
|
||||
|
||||
# 构建 Expo App 并上传 App Store Connect(之后在 App Store Connect 提交审核)。
|
||||
#
|
||||
# Required GitHub Secrets:
|
||||
#
|
||||
# IOS_CERTIFICATE_BASE64 - Apple distribution certificate (.p12) encoded in base64
|
||||
@ -12,6 +14,11 @@ name: "iOS Publish"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upload:
|
||||
description: 上传到 App Store Connect(关闭则只构建 IPA artifact)
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -21,198 +28,34 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
prepare-assets:
|
||||
name: Prepare iOS Assets
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
version: ${{ steps.get-version.outputs.version }}
|
||||
build:
|
||||
name: Build
|
||||
uses: ./.github/workflows/app-build.yml
|
||||
with:
|
||||
platform: ios
|
||||
ios_export_method: app-store
|
||||
release: true
|
||||
secrets: inherit
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from package.json
|
||||
id: get-version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install electron dependencies
|
||||
run: |
|
||||
pushd electron
|
||||
npm install
|
||||
popd
|
||||
|
||||
- name: Init mobile submodule
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --remote "resources/mobile"
|
||||
|
||||
- name: Build app assets
|
||||
run: ./cmd appbuild publish
|
||||
|
||||
- name: Upload iOS platform artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-platform
|
||||
path: resources/mobile/platforms/ios/
|
||||
retention-days: 1
|
||||
|
||||
build-ios:
|
||||
name: Build & Submit iOS
|
||||
needs: prepare-assets
|
||||
submit:
|
||||
name: Submit to App Store Connect
|
||||
needs: build
|
||||
if: inputs.upload
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
environment: build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Init mobile submodule
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update --remote "resources/mobile"
|
||||
|
||||
- name: Download prepared assets
|
||||
uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-platform
|
||||
path: resources/mobile/platforms/ios/
|
||||
name: app-ios
|
||||
path: ipa-output
|
||||
|
||||
- name: Select Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Install CocoaPods
|
||||
run: |
|
||||
if [ -f "resources/mobile/platforms/ios/eeuiApp/Podfile" ]; then
|
||||
cd resources/mobile/platforms/ios/eeuiApp
|
||||
pod install
|
||||
fi
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
|
||||
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
# Create temporary keychain
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
KEYCHAIN_PASSWORD=$(openssl rand -hex 20)
|
||||
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
# Import certificate
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
|
||||
echo "$IOS_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
|
||||
security import "$CERTIFICATE_PATH" \
|
||||
-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-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
|
||||
- name: Import provisioning profile
|
||||
env:
|
||||
IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
|
||||
IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
APP_PROFILE_PATH=$RUNNER_TEMP/app.mobileprovision
|
||||
SHARE_PROFILE_PATH=$RUNNER_TEMP/share-extension.mobileprovision
|
||||
APP_PROFILE_PLIST=$RUNNER_TEMP/app-profile.plist
|
||||
SHARE_PROFILE_PLIST=$RUNNER_TEMP/share-extension-profile.plist
|
||||
|
||||
echo "$IOS_PROVISION_PROFILE_BASE64" | base64 --decode > "$APP_PROFILE_PATH"
|
||||
echo "$IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64" | base64 --decode > "$SHARE_PROFILE_PATH"
|
||||
|
||||
security cms -D -i "$APP_PROFILE_PATH" > "$APP_PROFILE_PLIST"
|
||||
security cms -D -i "$SHARE_PROFILE_PATH" > "$SHARE_PROFILE_PLIST"
|
||||
|
||||
APP_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$APP_PROFILE_PLIST")
|
||||
SHARE_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$SHARE_PROFILE_PLIST")
|
||||
IOS_TEAM_ID=$(/usr/libexec/PlistBuddy -c "Print :TeamIdentifier:0" "$APP_PROFILE_PLIST")
|
||||
APP_PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$APP_PROFILE_PLIST")
|
||||
SHARE_PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$SHARE_PROFILE_PLIST")
|
||||
|
||||
if [ "$APP_PROFILE_APP_ID" != "$IOS_TEAM_ID.com.dootask.task" ]; then
|
||||
echo "Expected app profile for $IOS_TEAM_ID.com.dootask.task, got $APP_PROFILE_APP_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SHARE_PROFILE_APP_ID" != "$IOS_TEAM_ID.com.dootask.task.shareExtension" ]; then
|
||||
echo "Expected share extension profile for $IOS_TEAM_ID.com.dootask.task.shareExtension, got $SHARE_PROFILE_APP_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:aps-environment" "$APP_PROFILE_PLIST" >/dev/null; then
|
||||
echo "The DooTask app profile must include Push Notifications."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.security.application-groups" "$APP_PROFILE_PLIST" | grep -q "group.im.dootask"; then
|
||||
echo "The DooTask app profile must include App Group group.im.dootask."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! /usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.security.application-groups" "$SHARE_PROFILE_PLIST" | grep -q "group.im.dootask"; then
|
||||
echo "The share extension profile must include App Group group.im.dootask."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp "$APP_PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||
cp "$SHARE_PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
|
||||
|
||||
echo "APP_PROFILE_NAME=$APP_PROFILE_NAME" >> $GITHUB_ENV
|
||||
echo "SHARE_PROFILE_NAME=$SHARE_PROFILE_NAME" >> $GITHUB_ENV
|
||||
echo "IOS_TEAM_ID=$IOS_TEAM_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure manual signing
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
ruby <<'RUBY'
|
||||
require 'xcodeproj'
|
||||
|
||||
project_path = 'resources/mobile/platforms/ios/eeuiApp/eeuiApp.xcodeproj'
|
||||
project = Xcodeproj::Project.open(project_path)
|
||||
|
||||
{
|
||||
'DooTask' => ENV.fetch('APP_PROFILE_NAME'),
|
||||
'ShareExtension' => ENV.fetch('SHARE_PROFILE_NAME')
|
||||
}.each do |target_name, profile_name|
|
||||
target = project.targets.find { |item| item.name == target_name }
|
||||
abort "Target #{target_name} not found in #{project_path}" unless target
|
||||
|
||||
target.build_configurations.each do |config|
|
||||
next unless config.name == 'Release'
|
||||
|
||||
config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
|
||||
config.build_settings['DEVELOPMENT_TEAM'] = ENV.fetch('IOS_TEAM_ID')
|
||||
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
|
||||
config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = profile_name
|
||||
end
|
||||
end
|
||||
|
||||
project.save
|
||||
RUBY
|
||||
|
||||
- name: Resolve iOS build number
|
||||
- name: Upload IPA
|
||||
env:
|
||||
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
@ -220,196 +63,12 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
ruby <<'RUBY'
|
||||
require 'base64'
|
||||
require 'json'
|
||||
require 'net/http'
|
||||
require 'openssl'
|
||||
require 'uri'
|
||||
|
||||
BUNDLE_ID = 'com.dootask.task'
|
||||
VERSION_CONFIG_PATH = 'resources/mobile/platforms/ios/eeuiApp/Config/Version.xcconfig'
|
||||
|
||||
def base64url(value)
|
||||
Base64.urlsafe_encode64(value).delete('=')
|
||||
end
|
||||
|
||||
def jwt_es256_signature(private_key, unsigned)
|
||||
der_signature = private_key.sign('SHA256', unsigned)
|
||||
sequence = OpenSSL::ASN1.decode(der_signature)
|
||||
|
||||
sequence.value.map { |integer|
|
||||
integer.value.to_s(2).rjust(32, "\0")[-32, 32]
|
||||
}.join
|
||||
end
|
||||
|
||||
def asc_token
|
||||
key_id = ENV.fetch('ASC_API_KEY_ID')
|
||||
issuer_id = ENV.fetch('ASC_ISSUER_ID')
|
||||
private_key = OpenSSL::PKey.read(Base64.decode64(ENV.fetch('ASC_API_KEY_P8_BASE64')))
|
||||
now = Time.now.to_i
|
||||
|
||||
header = { alg: 'ES256', kid: key_id, typ: 'JWT' }
|
||||
payload = {
|
||||
iss: issuer_id,
|
||||
iat: now,
|
||||
exp: now + 20 * 60,
|
||||
aud: 'appstoreconnect-v1'
|
||||
}
|
||||
|
||||
unsigned = "#{base64url(header.to_json)}.#{base64url(payload.to_json)}"
|
||||
signature = jwt_es256_signature(private_key, unsigned)
|
||||
"#{unsigned}.#{base64url(signature)}"
|
||||
end
|
||||
|
||||
def asc_get(path, params, token)
|
||||
uri = URI::HTTPS.build(
|
||||
host: 'api.appstoreconnect.apple.com',
|
||||
path: path,
|
||||
query: URI.encode_www_form(params)
|
||||
)
|
||||
|
||||
request_uri = uri
|
||||
loop do
|
||||
response = Net::HTTP.start(request_uri.host, request_uri.port, use_ssl: true) do |http|
|
||||
request = Net::HTTP::Get.new(request_uri)
|
||||
request['Authorization'] = "Bearer #{token}"
|
||||
http.request(request)
|
||||
end
|
||||
|
||||
unless response.is_a?(Net::HTTPSuccess)
|
||||
abort "App Store Connect API request failed: #{response.code} #{response.body}"
|
||||
end
|
||||
|
||||
parsed = JSON.parse(response.body)
|
||||
yield parsed
|
||||
|
||||
next_link = parsed.dig('links', 'next')
|
||||
break unless next_link
|
||||
|
||||
request_uri = URI(next_link)
|
||||
end
|
||||
end
|
||||
|
||||
token = asc_token
|
||||
app_id = nil
|
||||
|
||||
asc_get('/v1/apps', { 'filter[bundleId]' => BUNDLE_ID, 'limit' => 1 }, token) do |page|
|
||||
app_id = page.fetch('data').first&.fetch('id')
|
||||
end
|
||||
|
||||
abort "App Store Connect app not found for bundle id #{BUNDLE_ID}" unless app_id
|
||||
|
||||
existing_versions = []
|
||||
asc_get('/v1/builds', {
|
||||
'filter[app]' => app_id,
|
||||
'fields[builds]' => 'version',
|
||||
'limit' => 200
|
||||
}, token) do |page|
|
||||
existing_versions.concat(
|
||||
page.fetch('data').map { |build| build.dig('attributes', 'version').to_s }
|
||||
)
|
||||
end
|
||||
|
||||
max_build_number = existing_versions
|
||||
.select { |version| version.match?(/\A\d+\z/) }
|
||||
.map(&:to_i)
|
||||
.max || 0
|
||||
|
||||
next_build_number = max_build_number + 1
|
||||
config_content = File.exist?(VERSION_CONFIG_PATH) ? File.read(VERSION_CONFIG_PATH) : ''
|
||||
|
||||
if config_content.match?(/^VERSION_CODE\s*=/)
|
||||
config_content = config_content.gsub(/^VERSION_CODE\s*=.*$/, "VERSION_CODE = #{next_build_number}")
|
||||
else
|
||||
config_content = "#{config_content.rstrip}\nVERSION_CODE = #{next_build_number}\n"
|
||||
end
|
||||
|
||||
File.write(VERSION_CONFIG_PATH, config_content)
|
||||
File.open(ENV.fetch('GITHUB_ENV'), 'a') { |file| file.puts "IOS_BUILD_NUMBER=#{next_build_number}" }
|
||||
|
||||
puts "Latest App Store Connect build number: #{max_build_number}"
|
||||
puts "Resolved iOS build number: #{next_build_number}"
|
||||
RUBY
|
||||
|
||||
- name: Build archive
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cd resources/mobile/platforms/ios/eeuiApp
|
||||
xcodebuild archive \
|
||||
-workspace eeuiApp.xcworkspace \
|
||||
-scheme eeuiApp \
|
||||
-configuration Release \
|
||||
-destination "generic/platform=iOS" \
|
||||
-archivePath $RUNNER_TEMP/eeuiApp.xcarchive \
|
||||
-allowProvisioningUpdates \
|
||||
DEVELOPMENT_TEAM=$IOS_TEAM_ID \
|
||||
CODE_SIGN_IDENTITY="Apple Distribution" \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
| xcpretty
|
||||
|
||||
if [ ! -d "$RUNNER_TEMP/eeuiApp.xcarchive" ]; then
|
||||
echo "Archive was not created at $RUNNER_TEMP/eeuiApp.xcarchive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Export IPA
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cd resources/mobile/platforms/ios/eeuiApp
|
||||
|
||||
# Generate ExportOptions.plist
|
||||
cat > $RUNNER_TEMP/ExportOptions.plist << PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>teamID</key>
|
||||
<string>${IOS_TEAM_ID}</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>com.dootask.task</key>
|
||||
<string>${APP_PROFILE_NAME}</string>
|
||||
<key>com.dootask.task.shareExtension</key>
|
||||
<string>${SHARE_PROFILE_NAME}</string>
|
||||
</dict>
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath $RUNNER_TEMP/eeuiApp.xcarchive \
|
||||
-exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \
|
||||
-exportPath $RUNNER_TEMP/ipa-output \
|
||||
-allowProvisioningUpdates \
|
||||
| xcpretty
|
||||
|
||||
- name: Submit to App Store Connect
|
||||
env:
|
||||
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Prepare API key
|
||||
mkdir -p ~/private_keys
|
||||
echo "$ASC_API_KEY_P8_BASE64" | base64 --decode > ~/private_keys/AuthKey_${ASC_API_KEY_ID}.p8
|
||||
|
||||
# Find and upload IPA
|
||||
IPA_PATH=$(find $RUNNER_TEMP/ipa-output -name "*.ipa" | head -1)
|
||||
IPA_PATH=$(find ipa-output -name "*.ipa" | head -1)
|
||||
if [ -z "$IPA_PATH" ]; then
|
||||
echo "No IPA file found in $RUNNER_TEMP/ipa-output"
|
||||
echo "No IPA file found in ipa-output"
|
||||
exit 1
|
||||
fi
|
||||
echo "Uploading: $IPA_PATH"
|
||||
@ -422,11 +81,4 @@ jobs:
|
||||
|
||||
- name: Clean up
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true
|
||||
rm -f $RUNNER_TEMP/certificate.p12
|
||||
rm -f $RUNNER_TEMP/app.mobileprovision
|
||||
rm -f $RUNNER_TEMP/share-extension.mobileprovision
|
||||
rm -f $RUNNER_TEMP/app-profile.plist
|
||||
rm -f $RUNNER_TEMP/share-extension-profile.plist
|
||||
rm -rf ~/private_keys
|
||||
run: rm -rf ~/private_keys
|
||||
|
||||
138
.github/workflows/publish.yml
vendored
138
.github/workflows/publish.yml
vendored
@ -131,8 +131,6 @@ jobs:
|
||||
include:
|
||||
- platform: "macos-latest"
|
||||
build_type: "mac"
|
||||
- platform: "ubuntu-latest"
|
||||
build_type: "android"
|
||||
- platform: "windows-latest"
|
||||
build_type: "windows"
|
||||
|
||||
@ -147,73 +145,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
# Android 构建步骤
|
||||
- name: (Android) Build Js
|
||||
if: matrix.build_type == 'android'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: |
|
||||
git submodule init
|
||||
git submodule update --remote "resources/mobile"
|
||||
./cmd appbuild publish
|
||||
|
||||
- name: (Android) Setup JDK 11
|
||||
if: matrix.build_type == 'android'
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
|
||||
- name: (Android) Build App
|
||||
if: matrix.build_type == 'android'
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
max_attempts: 5
|
||||
command: |
|
||||
cd resources/mobile/platforms/android/eeuiApp
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease --quiet
|
||||
|
||||
- name: (Android) Upload File
|
||||
if: matrix.build_type == 'android'
|
||||
env:
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||
run: |
|
||||
node ./electron/build.js android-upload
|
||||
|
||||
- name: (Android) Upload Release
|
||||
if: matrix.build_type == 'android'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const globby = require('globby');
|
||||
|
||||
// 查找 APK 文件
|
||||
const files = await globby('resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release/*.apk');
|
||||
|
||||
for (const file of files) {
|
||||
const data = await fs.promises.readFile(file);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: path.basename(file),
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
# Mac 构建步骤
|
||||
- name: (Mac) Build Client
|
||||
if: matrix.build_type == 'mac'
|
||||
@ -247,8 +178,75 @@ jobs:
|
||||
run: |
|
||||
./cmd electron win
|
||||
|
||||
build-android:
|
||||
needs: [ check-version, create-release ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/app-build.yml
|
||||
with:
|
||||
platform: android
|
||||
release: true
|
||||
secrets: inherit
|
||||
|
||||
upload-android:
|
||||
needs: [ check-version, create-release, build-android ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: app-android
|
||||
path: dist-android
|
||||
|
||||
- name: Upload File
|
||||
env:
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
|
||||
run: |
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd || exit
|
||||
node ./electron/build.js android-upload dist-android
|
||||
|
||||
- name: Upload Release
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const files = fs.readdirSync('dist-android').filter(name => name.endsWith('.apk'));
|
||||
if (!files.length) {
|
||||
throw new Error('APK 未找到');
|
||||
}
|
||||
for (const name of files) {
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: name,
|
||||
data: await fs.promises.readFile(path.join('dist-android', name))
|
||||
});
|
||||
}
|
||||
|
||||
publish-release:
|
||||
needs: [ check-version, create-release, pack-vendor, build-client ]
|
||||
needs: [ check-version, create-release, pack-vendor, build-client, upload-android ]
|
||||
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,9 +1,6 @@
|
||||
[submodule "resources/drawio"]
|
||||
path = resources/drawio
|
||||
url = https://github.com/jgraph/drawio.git
|
||||
[submodule "resources/mobile"]
|
||||
path = resources/mobile
|
||||
url = https://github.com/kuaifan/dootask-app.git
|
||||
[submodule "resources/app"]
|
||||
path = resources/app
|
||||
url = https://github.com/dootask/app.git
|
||||
|
||||
@ -22,10 +22,17 @@ npm run build # 编译前端
|
||||
- 如果想手动生成客户端执行 `./cmd electron` 根据提示选择操作。
|
||||
|
||||
|
||||
## 编译 App
|
||||
## App(Expo,`resources/app` 子模块)
|
||||
|
||||
```shell
|
||||
./cmd appbuild publish # 编译生成App需要的资源
|
||||
```
|
||||
- **Android**:推送到 `pro` 时随 Publish 工作流自动构建(`.github/workflows/app-build.yml`),以正式签名出包,上传官网下载(R2)并附到 GitHub Release;App 内「检查更新」读取官网下载索引。
|
||||
- **iOS**:手动运行 **iOS Publish** 工作流(`gh workflow run ios-publish.yml --ref pro`),构建后上传 App Store Connect,再到 App Store Connect 提交审核。
|
||||
- **内测包**:手动运行 **App Internal** 工作流,只产出 artifact。
|
||||
- 本地出包见 `resources/app/scripts/mac-build.sh`(Android 正式签名读取构建机 iCloud 云盘 `DooTaskKeys/android/`)。
|
||||
|
||||
编译完后进入 `resources/mobile` EEUI框架目录内打包 Android 或 iOS 应用(Android 以实现 GitHub Actions 自动发布)
|
||||
| Secret | 用途 |
|
||||
|---|---|
|
||||
| `ANDROID_KEYSTORE_BASE64` / `ANDROID_KEYSTORE_PASSWORD` / `ANDROID_KEY_ALIAS` / `ANDROID_KEY_PASSWORD` | Android 正式签名(与旧版证书一致,CI 校验指纹) |
|
||||
| `IOS_CERTIFICATE_BASE64` / `IOS_CERTIFICATE_PASSWORD` | Apple Distribution 证书 |
|
||||
| `IOS_PROVISION_PROFILE_BASE64` / `IOS_SHARE_EXTENSION_PROVISION_PROFILE_BASE64` | App Store profiles(主 App 需含推送与 App Group `group.im.dootask`) |
|
||||
| `ASC_API_KEY_ID` / `ASC_ISSUER_ID` / `ASC_API_KEY_P8_BASE64` | App Store Connect API(构建号查询与上传) |
|
||||
| `IOS_INTERNAL_PROVISION_PROFILE_BASE64` / `IOS_INTERNAL_SHARE_EXTENSION_PROVISION_PROFILE_BASE64` | 可选,iOS 内测 profiles |
|
||||
|
||||
22
cmd
22
cmd
@ -429,7 +429,7 @@ electron_operate() {
|
||||
env BUILD_FRONTEND=$BUILD_FRONTEND node ./electron/build.js $argv
|
||||
}
|
||||
|
||||
# Expo 内测:同 electron 使用本机 Node,不依赖 Docker 或旧 eeui 容器。
|
||||
# Expo App 前端:同 electron 使用本机 Node,不依赖 Docker。
|
||||
appexpo_operate() {
|
||||
check_node
|
||||
case "${1:-build}" in
|
||||
@ -711,7 +711,7 @@ DooTask 管理脚本
|
||||
serve, dev 启动开发模式
|
||||
build, prod 生产环境构建
|
||||
electron 构建桌面应用
|
||||
appexpo [build] 编译并同步 Expo 内测前端
|
||||
appexpo [build] 编译并同步 Expo App 前端
|
||||
appexpo npm <参数...> 在 Expo 子模块执行本机 npm
|
||||
|
||||
🔧 服务管理:
|
||||
@ -764,7 +764,7 @@ Usage: ./cmd <command> [options]
|
||||
serve, dev Start dev mode
|
||||
build, prod Production build
|
||||
electron Build desktop app
|
||||
appexpo [build] Build and copy Expo internal web assets
|
||||
appexpo [build] Build and copy Expo App web assets
|
||||
appexpo npm <args...> Run local npm in the Expo submodule
|
||||
|
||||
🔧 Services:
|
||||
@ -1288,10 +1288,6 @@ case "$1" in
|
||||
shift 1
|
||||
web_build prod
|
||||
;;
|
||||
"appbuild"|"buildapp")
|
||||
shift 1
|
||||
electron_operate app "$@"
|
||||
;;
|
||||
"appexpo")
|
||||
shift 1
|
||||
appexpo_operate "$@"
|
||||
@ -1301,24 +1297,12 @@ case "$1" in
|
||||
shift 1
|
||||
electron_operate "$@"
|
||||
;;
|
||||
"eeui")
|
||||
shift 1
|
||||
cli="$@"
|
||||
por=""
|
||||
if [[ "$cli" == "build" ]]; then
|
||||
cli="build --simple"
|
||||
elif [[ "$cli" == "dev" ]]; then
|
||||
por="-p 8880:8880"
|
||||
fi
|
||||
docker run $TTY_FLAG --rm -v ${WORK_DIR}/resources/mobile:/work -w /work ${por} kuaifan/eeui-cli:0.0.1 eeui ${cli}
|
||||
;;
|
||||
"npm")
|
||||
shift 1
|
||||
npm "$@"
|
||||
pushd electron || exit
|
||||
npm "$@"
|
||||
popd || exit
|
||||
docker run $TTY_FLAG --rm -v ${WORK_DIR}/resources/mobile:/work -w /work --entrypoint=/bin/bash node:16 -c "npm $@"
|
||||
;;
|
||||
"doc")
|
||||
shift 1
|
||||
|
||||
72
electron/build.js
vendored
72
electron/build.js
vendored
@ -26,7 +26,6 @@ const architectures = ["arm64", "x64"];
|
||||
let buildChecked = false,
|
||||
updaterChecked = false;
|
||||
|
||||
const shellQuote = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
const elapsedSeconds = (startTime) => `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
|
||||
|
||||
/**
|
||||
@ -359,7 +358,7 @@ async function startBuild(data) {
|
||||
apiUrl: utils.formatUrl(data.url) + "api/",
|
||||
}
|
||||
// information
|
||||
if (['app', 'app-expo'].includes(data.id)) {
|
||||
if (data.id === 'app-expo') {
|
||||
console.log("\n=== 编译信息 ===");
|
||||
console.log("名称:", data.name);
|
||||
console.log("版本:", config.version + ` (${config.codeVerson})`);
|
||||
@ -422,51 +421,6 @@ async function startBuild(data) {
|
||||
console.log("Expo 前端资源已同步至 resources/app/assets/web/");
|
||||
return;
|
||||
}
|
||||
if (data.id === 'app') {
|
||||
const eeuiDir = path.resolve(__dirname, "../resources/mobile");
|
||||
const publicDir = path.resolve(__dirname, "../resources/mobile/src/public");
|
||||
const containerName = `dootask-eeui-${Date.now()}-${process.pid}`;
|
||||
fse.removeSync(publicDir)
|
||||
fse.copySync(electronDir, publicDir)
|
||||
if (argv[3] === "publish") {
|
||||
// Android config
|
||||
const gradleFile = path.resolve(eeuiDir, "platforms/android/eeuiApp/local.properties")
|
||||
let gradleResult = fs.existsSync(gradleFile) ? fs.readFileSync(gradleFile, 'utf8') : "";
|
||||
gradleResult = gradleResult.replace(/(versionCode|versionName)\s*=\s*(.+?)(\n|$)/g, '')
|
||||
gradleResult += `versionCode = ${config.codeVerson}\nversionName = ${config.version}\n`
|
||||
fs.writeFileSync(gradleFile, gradleResult, 'utf8')
|
||||
// iOS config
|
||||
const xcconfigFile = path.resolve(eeuiDir, "platforms/ios/eeuiApp/Config/Version.xcconfig")
|
||||
let xcconfigResult = fs.existsSync(xcconfigFile) ? fs.readFileSync(xcconfigFile, 'utf8') : "";
|
||||
xcconfigResult = xcconfigResult.replace(/(VERSION_CODE|VERSION_NAME)\s*=\s*(.+?)(\n|$)/g, '')
|
||||
xcconfigResult += `VERSION_CODE = ${config.codeVerson}\nVERSION_NAME = ${config.version}\n`
|
||||
fs.writeFileSync(xcconfigFile, xcconfigResult, 'utf8')
|
||||
}
|
||||
if (['build', 'publish'].includes(argv[3])) {
|
||||
child_process.execSync(
|
||||
`docker run -d --name ${containerName} -v ${shellQuote(eeuiDir)}:/work -w /work kuaifan/eeui-cli:0.0.1 sleep infinity`,
|
||||
{stdio: "ignore", cwd: "resources/mobile"}
|
||||
);
|
||||
try {
|
||||
if (!fs.existsSync(path.resolve(eeuiDir, "node_modules"))) {
|
||||
child_process.execSync(`docker exec ${containerName} npm install`, {stdio: "inherit", cwd: "resources/mobile"});
|
||||
}
|
||||
child_process.execSync(`docker exec ${containerName} node /work/scripts/patch-eeui-build.js`, {stdio: "inherit", cwd: "resources/mobile"});
|
||||
child_process.execSync(`docker exec ${containerName} eeui build --simple`, {stdio: "inherit", cwd: "resources/mobile"});
|
||||
} finally {
|
||||
child_process.execSync(`docker rm -f ${containerName}`, {stdio: "ignore", cwd: "resources/mobile"});
|
||||
}
|
||||
} else {
|
||||
[
|
||||
path.resolve(publicDir, "../../platforms/ios/eeuiApp/bundlejs/eeui/public"),
|
||||
path.resolve(publicDir, "../../platforms/android/eeuiApp/app/src/main/assets/eeui/public"),
|
||||
].some(dir => {
|
||||
fse.removeSync(dir)
|
||||
fse.copySync(electronDir, dir)
|
||||
})
|
||||
}
|
||||
return;
|
||||
}
|
||||
const output = `dist/${data.id.replace(/\./g, '-')}/${platform}`
|
||||
// package.json Backup
|
||||
fse.copySync(packageFile, packageBakFile)
|
||||
@ -554,26 +508,6 @@ if (["dev"].includes(argv[2])) {
|
||||
fs.writeFileSync(devloadCachePath, utils.formatUrl("localhost:" + env.parsed.APP_PORT), 'utf8');
|
||||
child_process.spawn("npx", ["vite", "--", "fromcmd", "electronDev"], {stdio: "inherit"});
|
||||
child_process.spawn("npm", ["run", "start-quiet"], {stdio: "inherit", cwd: "electron"});
|
||||
} else if (["app"].includes(argv[2])) {
|
||||
// 编译前端页面给 App
|
||||
let mobileSrcDir = path.resolve(__dirname, "../resources/mobile");
|
||||
if (!fs.existsSync(mobileSrcDir)) {
|
||||
console.error("resources/mobile 未找到");
|
||||
process.exit()
|
||||
}
|
||||
startBuild({
|
||||
name: 'App',
|
||||
id: 'app',
|
||||
platform: '',
|
||||
url: 'http://public/',
|
||||
configure: {
|
||||
platform: '',
|
||||
archs: [],
|
||||
publish: false,
|
||||
release: true,
|
||||
notarize: false,
|
||||
}
|
||||
})
|
||||
} else if (argv[2] === 'app-expo') {
|
||||
if ((argv[3] && argv[3] !== 'build') || argv.length > 4) {
|
||||
console.error("用法: node ./electron/build.js app-expo [build]");
|
||||
@ -601,14 +535,14 @@ if (["dev"].includes(argv[2])) {
|
||||
process.exitCode = 1;
|
||||
});
|
||||
} else if (["android-upload"].includes(argv[2])) {
|
||||
// 上传安卓文件到 R2 draft(GitHub Actions)
|
||||
// 上传安卓文件到 R2 draft(GitHub Actions):参数为 APK 所在目录(app-build.yml 产出的 app-android)
|
||||
(async () => {
|
||||
if (!r2.r2Configured()) {
|
||||
console.error("缺少 R2_* 环境变量(R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT/R2_BUCKET)")
|
||||
process.exit(1)
|
||||
}
|
||||
const client = r2.createR2Client()
|
||||
const releaseDir = path.resolve(__dirname, "../resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release");
|
||||
const releaseDir = path.resolve(argv[3] || path.resolve(__dirname, "../resources/app/android/app/build/outputs/apk/release"));
|
||||
if (!fs.existsSync(releaseDir)) {
|
||||
console.error("发布文件未找到")
|
||||
process.exit(1)
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit add42fcfdf199bfab86203cba01b7191aa2594e0
|
||||
Subproject commit d2ff9a54a0cfc1ea9942531193cc8265d80b7135
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 1a8467602004ac9336619fc6aff9dfbb7143527a
|
||||
@ -34,8 +34,6 @@ const ACTUAL_SOURCE_ROOTS = [
|
||||
"electron",
|
||||
"resources/assets/js",
|
||||
"resources/assets/sass",
|
||||
"resources/mobile/src",
|
||||
"resources/mobile/scripts",
|
||||
"resources/views",
|
||||
"routes",
|
||||
"bin",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user