Skip to content

Merge pull request #695 from chaitin/fix/member-stats-exclude-admin #43

Merge pull request #695 from chaitin/fix/member-stats-exclude-admin

Merge pull request #695 from chaitin/fix/member-stats-exclude-admin #43

# 打 tag(移动端用日期序号 v<YYMMDD><NN>,如 v26060901)后自动构建 Windows / macOS 桌面与 Android/iOS 客户端,并发布到 GitHub Release。
# 桌面 electron-builder 需 semver,会把日期 tag 转成 20YY.MMDD.NN(如 2026.609.1);移动端 versionName/Code 直接用 26060901。
# 手动运行 workflow 仅上传 Actions Artifact,不创建 Release(便于试打)。
# Android 为 Expo 客户端(mobile/)的 release APK,需在仓库 Secrets 配置签名密钥:
# ANDROID_KEYSTORE_BASE64 / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD / ANDROID_STORE_PASSWORD
# iOS 为 Expo 客户端(mobile/)的 release IPA(xcodebuild archive+export),需在 Secrets 配置:
# IOS_CERTIFICATE_BASE64 / IOS_CERTIFICATE_PASSWORD / IOS_PROVISIONING_PROFILE_BASE64 / IOS_TEAM_ID
name: Client Release
on:
push:
tags:
- "v*"
workflow_dispatch:
# 仅写 contents 会把其余权限置为 none,会导致 download-artifact 找不到同 run 上传的产物
permissions:
contents: write
actions: write
concurrency:
group: electron-release-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
CSC_IDENTITY_AUTO_DISCOVERY: false
jobs:
electron-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: desktop/pnpm-lock.yaml
- name: Cache Electron binary
uses: actions/cache@v4
with:
path: ~\AppData\Local\electron\Cache
key: electron-win-${{ hashFiles('desktop/package.json') }}
- name: Cache electron-builder tools
uses: actions/cache@v4
with:
path: ~\AppData\Local\electron-builder\Cache
key: electron-builder-win-${{ hashFiles('desktop/package.json') }}
- name: Set package.json version
shell: bash
working-directory: desktop
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
RAW="${GITHUB_REF_NAME#v}"
# electron-builder 要求严格 semver;移动端日期 tag v<YYMMDD><NN> → 20YY.MMDD.NN
# (如 v26060901 → 2026.609.1;各段 ≤65535 兼容 Windows 版本号,且随 tag 单调递增)
if [[ "$RAW" =~ ^[0-9]{8}$ ]]; then
V="20${RAW:0:2}.$((10#${RAW:2:4})).$((10#${RAW:6:2}))"
elif [[ "$RAW" =~ ^[0-9]+\.[0-9]+$ ]]; then
V="${RAW}.0"
else
V="$RAW"
fi
else
V="0.0.0-ci.${{ github.run_number }}"
fi
node -e "const fs=require('fs');const p='package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
# windows-latest 默认 run 用 pwsh;pnpm/electron-builder 在 Git Bash 下 cwd 更可靠
- name: Install dependencies
working-directory: desktop
shell: bash
run: pnpm install --frozen-lockfile
# 用 bash + pnpm exec,避免 pwsh 下子进程 cwd/退出码异常;ci-win.json 显式 directories.output
- name: Build Windows (NSIS installer)
working-directory: desktop
shell: bash
run: |
set -euxo pipefail
pwd
test -f package.json
test -f electron/main.cjs
pnpm exec electron-builder --win nsis --x64 --publish never -c electron-builder.ci-win.json
if [[ ! -d release ]]; then
echo "electron-builder 已结束但 ./release 不存在,当前目录:"
ls -la
exit 1
fi
ls -laR release
- name: Rename Windows artifact
shell: bash
run: cp "$(ls desktop/release/*.exe | head -1)" desktop/release/MonkeyCode-windows.exe
- uses: actions/upload-artifact@v4
with:
name: electron-windows-x64
path: desktop/release/MonkeyCode-windows.exe
if-no-files-found: error
electron-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: desktop/pnpm-lock.yaml
- name: Cache Electron binary
uses: actions/cache@v4
with:
path: ~/Library/Caches/electron
key: electron-mac-${{ hashFiles('desktop/package.json') }}
- name: Cache electron-builder tools
uses: actions/cache@v4
with:
path: ~/Library/Caches/electron-builder
key: electron-builder-mac-${{ hashFiles('desktop/package.json') }}
- name: Set package.json version
working-directory: desktop
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
RAW="${GITHUB_REF_NAME#v}"
# electron-builder 要求严格 semver;移动端日期 tag v<YYMMDD><NN> → 20YY.MMDD.NN
# (如 v26060901 → 2026.609.1;各段 ≤65535 兼容 Windows 版本号,且随 tag 单调递增)
if [[ "$RAW" =~ ^[0-9]{8}$ ]]; then
V="20${RAW:0:2}.$((10#${RAW:2:4})).$((10#${RAW:6:2}))"
elif [[ "$RAW" =~ ^[0-9]+\.[0-9]+$ ]]; then
V="${RAW}.0"
else
V="$RAW"
fi
else
V="0.0.0-ci.${{ github.run_number }}"
fi
node -e "const fs=require('fs');const p='package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
- name: Install dependencies
working-directory: desktop
run: pnpm install --frozen-lockfile
# 目标格式(dmg / zip)取自 package.json 的 build.mac.target
- name: Build macOS (arm64 + x64)
working-directory: desktop
run: |
set -euo pipefail
pwd
pnpm run electron:ci:mac
test -d release || (echo "missing desktop/release"; ls -la; exit 1)
ls -laR release
- name: Rename macOS artifacts
run: |
ARM64=$(ls desktop/release/*arm64*.dmg 2>/dev/null | head -1)
X64=$(ls desktop/release/*.dmg | grep -v arm64 | head -1)
[ -n "$ARM64" ] && cp "$ARM64" desktop/release/MonkeyCode-macos-arm64.dmg
[ -n "$X64" ] && cp "$X64" desktop/release/MonkeyCode-macos-x86.dmg
- uses: actions/upload-artifact@v4
with:
name: electron-macos-universal
path: |
desktop/release/MonkeyCode-macos-arm64.dmg
desktop/release/MonkeyCode-macos-x86.dmg
if-no-files-found: error
mobile-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: mobile/package-lock.json
- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
# 缓存 Gradle 发行版 + 依赖 + 构建缓存(android/ 由 prebuild 生成、不在仓库里,
# 所以不用 setup-java 的 cache:gradle——它靠 hash 仓库里的 gradle 文件,这里 hash 不到)
- uses: gradle/actions/setup-gradle@v4
- name: Set versions (from tag, e.g. v26060901)
working-directory: mobile
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
V="${GITHUB_REF_NAME#v}" # 你打的 tag 形如 v<YYMMDD><2位序号>,自己保证唯一递增
else
V="${{ github.run_number }}" # 非 tag 的手动试打:用 run_number
fi
# versionName 与 versionCode 都取 V → 装机版本可直接对应到 tag / CI run。
# versionCode 必须是整数且 < 2,100,000,000;YYMMDDNN(如 26060901)恒满足,每天最多 99 次。
node -e "const fs=require('fs');const p='app.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.expo.version=String(process.argv[1]);j.expo.android=j.expo.android||{};j.expo.android.versionCode=Number(process.argv[1]);fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
- name: Install mobile deps
working-directory: mobile
run: npm ci
- name: Expo prebuild (generate android project)
working-directory: mobile
run: npx expo prebuild -p android --no-install
- name: Build Android APK (release, signed)
working-directory: mobile/android
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
KS="$RUNNER_TEMP/release.jks"
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$KS"
chmod +x gradlew
# 用 AGP 注入签名(无需改 build.gradle):release APK 直接用 keystore 签名。
# 只编真机 ABI(arm64 + armv7):x86/x86_64 仅模拟器需要,少编一半 C++(新架构下这是大头)
./gradlew assembleRelease --no-daemon --stacktrace -Dorg.gradle.jvmargs=-Xmx4096m \
-PreactNativeArchitectures=arm64-v8a,armeabi-v7a \
-Pandroid.injected.signing.store.file="$KS" \
-Pandroid.injected.signing.store.password="$ANDROID_STORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD"
- name: Stage APK for artifact / release
shell: bash
run: |
mkdir -p mobile/release-apk
cp mobile/android/app/build/outputs/apk/release/app-release.apk mobile/release-apk/MonkeyCode-android.apk
ls -lh mobile/release-apk/
- uses: actions/upload-artifact@v4
with:
name: mobile-android-apk
path: mobile/release-apk/*.apk
if-no-files-found: error
# iOS(Expo):用 xcodebuild archive+export,复用与原 CI 相同的签名 Secrets:
# IOS_CERTIFICATE_BASE64 / IOS_CERTIFICATE_PASSWORD / IOS_PROVISIONING_PROFILE_BASE64 / IOS_TEAM_ID
# 证书的 bundleId 必须是 com.chaitin.baizhi.monkeycode;导出方式默认 app-store(上 TestFlight/商店),
# 想直接装真机改成 ad-hoc。
# 注意:不要设 RCT_USE_PREBUILT_RNCORE=1。它强制用预编译 React.framework(依赖 ReactNativeDependencies.framework),
# 与 app.json 的 ios.buildReactNativeFromSource=true 冲突 → ReactNativeDependencies.framework 没被嵌入 .app,
# 真机一启动就 dyld 崩溃(Library not loaded: @rpath/ReactNativeDependencies.framework/...)。
# 保持从源码编译(app.json 已配 buildReactNativeFromSource=true),是 RN 官方对该实验特性的建议,能正常过 TestFlight。
mobile-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
# 预编译 RN / ExpoModulesJSI 的 SwiftPM 依赖要求 swift-tools 6.2(Xcode 26);runner 默认 Xcode 可能只有
# Swift 6.1,导致 "Build ExpoModulesJSI xcframework" 报 'using Swift tools version 6.2.0 but installed is 6.1.0'。
# 选最新 stable Xcode。若日志里 swift --version 仍是 6.1,说明该 runner 镜像没装 Xcode26,需换更新的 runs-on。
- name: Select latest stable Xcode (need Swift 6.2 / Xcode 26)
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Show Xcode / Swift version
run: xcodebuild -version && swift --version
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: mobile/package-lock.json
- name: Set versions (from tag, e.g. v26060901)
working-directory: mobile
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
V="${GITHUB_REF_NAME#v}"
else
V="${{ github.run_number }}"
fi
# CFBundleShortVersionString(version) 与 CFBundleVersion(build) 都取 V → 可对应 tag / CI run。
node -e "const fs=require('fs');const p='app.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.expo.version=String(process.argv[1]);j.expo.ios=j.expo.ios||{};j.expo.ios.buildNumber=String(process.argv[1]);fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
- name: Install mobile deps
working-directory: mobile
run: npm ci
- name: Expo prebuild (generate ios project)
working-directory: mobile
run: npx expo prebuild -p ios --no-install
- name: Pod install
working-directory: mobile/ios
run: pod install
- name: Install signing assets
shell: bash
env:
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
set -euo pipefail
: "${IOS_CERTIFICATE_BASE64:?Missing IOS_CERTIFICATE_BASE64}"
: "${IOS_CERTIFICATE_PASSWORD:?Missing IOS_CERTIFICATE_PASSWORD}"
: "${IOS_PROVISIONING_PROFILE_BASE64:?Missing IOS_PROVISIONING_PROFILE_BASE64}"
CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
PROFILE_PATH="$RUNNER_TEMP/build_profile.mobileprovision"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
echo "$IOS_CERTIFICATE_BASE64" | base64 -D > "$CERT_PATH"
echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 -D > "$PROFILE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
security cms -D -i "$PROFILE_PATH" > "$RUNNER_TEMP/profile.plist"
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" "$RUNNER_TEMP/profile.plist")
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print Name" "$RUNNER_TEMP/profile.plist")
cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.mobileprovision"
# 取出导入证书的签名标识名(如 "Apple Distribution: …"),用于强制手动签名,避免回退到 "iOS Development"
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | sed -n 's/.*"\(.*\)".*/\1/p' | head -1)
: "${SIGNING_IDENTITY:?No code-signing identity found in imported certificate}"
echo "IOS_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV"
echo "IOS_SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV"
- name: Archive & export IPA
working-directory: mobile/ios
env:
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
run: |
set -euo pipefail
BUNDLE_ID="com.chaitin.baizhi.monkeycode"
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>teamID</key><string>${IOS_TEAM_ID}</string>
<key>signingStyle</key><string>manual</string>
<key>provisioningProfiles</key>
<dict><key>${BUNDLE_ID}</key><string>${IOS_PROFILE_NAME}</string></dict>
<key>stripSwiftSymbols</key><true/>
<key>uploadBitcode</key><false/>
<key>compileBitcode</key><false/>
</dict>
</plist>
PLIST
xcodebuild -workspace MonkeyCode.xcworkspace -scheme MonkeyCode \
-configuration Release -sdk iphoneos -destination "generic/platform=iOS" \
-archivePath "$RUNNER_TEMP/MonkeyCode.xcarchive" \
DEVELOPMENT_TEAM="$IOS_TEAM_ID" CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="$IOS_SIGNING_IDENTITY" \
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" \
archive
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MonkeyCode.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-exportPath "$RUNNER_TEMP/export"
ls -lh "$RUNNER_TEMP/export"
- name: Stage IPA for artifact / release
shell: bash
run: |
set -euxo pipefail
mkdir -p mobile/release-ios
IPA="$(find "$RUNNER_TEMP/export" -name '*.ipa' | head -1)"
test -n "$IPA"
cp "$IPA" mobile/release-ios/MonkeyCode-ios.ipa
ls -lh mobile/release-ios/
- uses: actions/upload-artifact@v4
with:
name: mobile-ios-ipa
path: mobile/release-ios/*.ipa
if-no-files-found: error
- name: Cleanup signing keychain
if: always() && env.IOS_KEYCHAIN_PATH != ''
shell: bash
run: |
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
publish-release:
needs: [electron-windows, electron-macos, mobile-android, mobile-ios]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: electron-windows-x64
path: release-assets/windows
- uses: actions/download-artifact@v4
with:
name: electron-macos-universal
path: release-assets/macos
- uses: actions/download-artifact@v4
with:
name: mobile-android-apk
path: release-assets/android
- uses: actions/download-artifact@v4
with:
name: mobile-ios-ipa
path: release-assets/ios
- name: List release files
run: find release-assets -type f -exec ls -lh {} \;
- uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: MonkeyCode ${{ github.ref_name }}
generate_release_notes: true
fail_on_unmatched_files: false
files: |
release-assets/windows/*
release-assets/macos/*
release-assets/android/*
release-assets/ios/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# iOS 上传 TestFlight(App Store Connect)。仅 tag 发布时跑(workflow_dispatch 试打不传,避免污染 TestFlight)。
# 不自动提审——上传后 build 进入 App Store Connect,TestFlight 即可内测;要上架就在网页「App Store」里选这个
# 已上传的 build 提交审核,无需重传 IPA。需配 Secrets:
# APP_STORE_CONNECT_KEY_ID / APP_STORE_CONNECT_ISSUER_ID / APP_STORE_CONNECT_PRIVATE_KEY_BASE64(.p8 的 base64)
ios-testflight:
needs: [mobile-ios]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: mobile-ios-ipa
path: ipa
- name: Upload to TestFlight (App Store Connect)
shell: bash
env:
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
ASC_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
run: |
set -euo pipefail
: "${ASC_KEY_ID:?Missing APP_STORE_CONNECT_KEY_ID}"
: "${ASC_ISSUER_ID:?Missing APP_STORE_CONNECT_ISSUER_ID}"
: "${ASC_PRIVATE_KEY_BASE64:?Missing APP_STORE_CONNECT_PRIVATE_KEY_BASE64}"
# altool 约定:API key(.p8) 须放在 ~/.appstoreconnect/private_keys 且命名 AuthKey_<KEY_ID>.p8
KEY_DIR="$HOME/.appstoreconnect/private_keys"
mkdir -p "$KEY_DIR"
KEY_FILE="$KEY_DIR/AuthKey_${ASC_KEY_ID}.p8"
echo "$ASC_PRIVATE_KEY_BASE64" | base64 -D > "$KEY_FILE"
trap 'rm -f "$KEY_FILE"' EXIT
IPA="$(find ipa -name '*.ipa' | head -1)"
test -n "$IPA"
echo "Uploading $(basename "$IPA") to App Store Connect / TestFlight..."
# altool --upload-app 仍可用;若某天被移除可改用 xcrun iTMSTransporter / Transporter.app
xcrun altool --upload-app -t ios -f "$IPA" --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"