feat(release): 打通 macOS、Windows 与 iOS 三平台发布 #146
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| checks: | |
| name: Test and build macOS app | |
| runs-on: macos-15 | |
| timeout-minutes: 40 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 2 | |
| - name: Detect changed paths | |
| id: changes | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| echo "rust_agent=true" >> "$GITHUB_OUTPUT" | |
| echo "voice_correction_benchmark=true" >> "$GITHUB_OUTPUT" | |
| echo "context_boost_benchmark=true" >> "$GITHUB_OUTPUT" | |
| echo "ios=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git diff --name-only --diff-filter=ACMRT HEAD^1 HEAD > "$RUNNER_TEMP/changed-files.txt" | |
| if grep -E '^(agent-cli/|\.github/workflows/ci\.yml$)' "$RUNNER_TEMP/changed-files.txt" >/dev/null; then | |
| echo "rust_agent=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "rust_agent=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if grep -E '^(Packages/VoxFlowVoiceCorrectionKit/|Package\.(swift|resolved)$|\.github/workflows/ci\.yml$)' "$RUNNER_TEMP/changed-files.txt" >/dev/null; then | |
| echo "voice_correction_benchmark=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "voice_correction_benchmark=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if grep -E '^(Packages/VoxFlowContextBoostKit/|Package\.(swift|resolved)$|\.github/workflows/ci\.yml$)' "$RUNNER_TEMP/changed-files.txt" >/dev/null; then | |
| echo "context_boost_benchmark=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "context_boost_benchmark=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if grep -E '^(Apps/VoxFlowiOS/|Makefile$|\.github/workflows/ci\.yml$)' "$RUNNER_TEMP/changed-files.txt" >/dev/null; then | |
| echo "ios=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "ios=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Check release metadata | |
| run: make release-check | |
| - name: Show Swift toolchain | |
| run: swift --version | |
| - name: Install lint and localization tools | |
| run: brew install swiftlint bartycrouch swiftgen xcodegen | |
| - name: Cache SwiftPM dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| .build/artifacts | |
| .build/checkouts | |
| .build/plugins | |
| .build/repositories | |
| key: ${{ runner.os }}-swiftpm-${{ hashFiles('Package.resolved') }} | |
| restore-keys: | | |
| ${{ runner.os }}-swiftpm- | |
| - name: Cache sherpa-onnx runtime | |
| uses: actions/cache@v4 | |
| with: | |
| path: Vendor/sherpa-onnx.xcframework | |
| key: ${{ runner.os }}-sherpa-onnx-${{ hashFiles('scripts/bootstrap-sherpa-onnx.sh') }} | |
| - name: Prepare sherpa-onnx runtime | |
| run: ./scripts/bootstrap-sherpa-onnx.sh | |
| - name: Cache iOS Rime Ice schemas | |
| if: steps.changes.outputs.ios == 'true' | |
| uses: actions/cache@v4 | |
| with: | |
| path: .build/ios-rime-ice | |
| key: ${{ runner.os }}-ios-rime-ice-${{ hashFiles('Apps/VoxFlowiOS/Scripts/bootstrap-rime-ice-schemas.sh') }} | |
| - name: Run tests | |
| run: | | |
| start=$(date +%s) | |
| swift_build_jobs="$(sysctl -n hw.logicalcpu)" | |
| swift_test_workers=8 | |
| VOXFLOW_TEST_INTERFACE_LANGUAGE=zh-Hans swift test --parallel --num-workers "$swift_test_workers" --jobs "$swift_build_jobs" | |
| duration=$(( $(date +%s) - start )) | |
| echo "### CI timing" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Step | Seconds |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| --- | ---: |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| swift test --parallel --num-workers $swift_test_workers --jobs $swift_build_jobs | $duration |" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Run voice correction benchmark | |
| if: steps.changes.outputs.voice_correction_benchmark == 'true' | |
| run: | | |
| swift run --package-path Packages/VoxFlowVoiceCorrectionKit VoxFlowVoiceCorrectionBench \ | |
| --fixtures Packages/VoxFlowVoiceCorrectionKit/Benchmarks/Fixtures \ | |
| --baseline Packages/VoxFlowVoiceCorrectionKit/Benchmarks/Baselines/phase1-baseline.json \ | |
| --output .build/voice-correction-benchmark | |
| - name: Run context boost benchmark | |
| if: steps.changes.outputs.context_boost_benchmark == 'true' | |
| run: swift run --package-path Packages/VoxFlowContextBoostKit VoxFlowContextBoostBench | |
| - name: Show Rust toolchain | |
| if: steps.changes.outputs.rust_agent == 'true' | |
| run: | | |
| rustc --version | |
| cargo --version | |
| rustup component add rustfmt clippy | |
| - name: Check Rust agent router | |
| if: steps.changes.outputs.rust_agent == 'true' | |
| working-directory: agent-cli | |
| run: | | |
| start=$(date +%s) | |
| cargo fmt --check | |
| cargo clippy --all-targets -- -D warnings | |
| cargo test | |
| duration=$(( $(date +%s) - start )) | |
| echo "| Rust fmt/clippy/test | $duration |" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Check helper script syntax | |
| run: | | |
| bash -n agent-cli/vox | |
| - name: Check architecture boundaries | |
| run: make architecture-check | |
| - name: Run SwiftLint | |
| run: make lint | |
| - name: Run iOS simulator tests | |
| if: steps.changes.outputs.ios == 'true' | |
| run: make ios-test-sim | |
| - name: Run i18n checks | |
| run: make i18n-check | |
| - name: Build with warnings as errors | |
| run: | | |
| start=$(date +%s) | |
| swift build -c debug -Xswiftc -warnings-as-errors | |
| duration=$(( $(date +%s) - start )) | |
| echo "| debug warnings-as-errors build | $duration |" >> "$GITHUB_STEP_SUMMARY" | |
| package-macos: | |
| name: Package signed macOS DMG | |
| if: github.event_name != 'pull_request' | |
| runs-on: macos-15 | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Cache SwiftPM dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| .build/artifacts | |
| .build/checkouts | |
| .build/plugins | |
| .build/repositories | |
| key: ${{ runner.os }}-swiftpm-${{ hashFiles('Package.resolved') }} | |
| restore-keys: | | |
| ${{ runner.os }}-swiftpm- | |
| - name: Prepare sherpa-onnx runtime | |
| run: ./scripts/bootstrap-sherpa-onnx.sh | |
| - name: Configure macOS distribution signing | |
| env: | |
| CERTIFICATE_BASE64: ${{ secrets.VOXFLOW_RELEASE_CERTIFICATE_P12_BASE64 }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.VOXFLOW_RELEASE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| test -n "$CERTIFICATE_BASE64" | |
| test -n "$CERTIFICATE_PASSWORD" | |
| certificate_path="$RUNNER_TEMP/voxflow-release.p12" | |
| keychain_path="$RUNNER_TEMP/voxflow-release.keychain-db" | |
| keychain_password="$(openssl rand -base64 24)" | |
| printf '%s' "$CERTIFICATE_BASE64" | base64 --decode > "$certificate_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 "$certificate_path" -k "$keychain_path" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" | |
| security list-keychains -d user -s "$keychain_path" | |
| certificate_sha1="$(security find-certificate -a -Z "$keychain_path" | awk '/SHA-1 hash:/ { print $3; exit }')" | |
| test -n "$certificate_sha1" | |
| echo "VOXFLOW_RELEASE_SIGNING_IDENTITY=$certificate_sha1" >> "$GITHUB_ENV" | |
| echo "VOXFLOW_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" | |
| - name: Build signed DMG | |
| env: | |
| VOXFLOW_SENTRY_DSN: ${{ secrets.VOXFLOW_SENTRY_DSN }} | |
| run: | | |
| make dmg \ | |
| RELEASE_CODE_SIGN_IDENTITY="$VOXFLOW_RELEASE_SIGNING_IDENTITY" \ | |
| RELEASE_KEYCHAIN_PATH="$VOXFLOW_RELEASE_KEYCHAIN_PATH" | |
| - name: Upload macOS package | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: VoxFlow-macOS | |
| path: | | |
| dist/VoxFlow-*-macOS.dmg | |
| dist/VoxFlow-*-macOS.dmg.sha256 | |
| if-no-files-found: error | |
| package-ios: | |
| name: Package installable iOS IPA | |
| if: github.event_name != 'pull_request' | |
| runs-on: macos-15 | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Install XcodeGen | |
| run: brew install xcodegen | |
| - name: Install iOS distribution certificate and profiles | |
| id: signing | |
| env: | |
| CERTIFICATE_BASE64: ${{ secrets.MASHANGXIE_IOS_DISTRIBUTION_P12_BASE64 }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.MASHANGXIE_IOS_DISTRIBUTION_P12_PASSWORD }} | |
| APP_PROFILE_BASE64: ${{ secrets.MASHANGXIE_IOS_APP_PROFILE_BASE64 }} | |
| KEYBOARD_PROFILE_BASE64: ${{ secrets.MASHANGXIE_IOS_KEYBOARD_PROFILE_BASE64 }} | |
| run: | | |
| require_secret() { | |
| name="$1" | |
| value="$2" | |
| test -n "$value" || { echo "::error::Missing Actions secret: $name"; exit 2; } | |
| } | |
| require_secret MASHANGXIE_IOS_DISTRIBUTION_P12_BASE64 "$CERTIFICATE_BASE64" | |
| require_secret MASHANGXIE_IOS_DISTRIBUTION_P12_PASSWORD "$CERTIFICATE_PASSWORD" | |
| require_secret MASHANGXIE_IOS_APP_PROFILE_BASE64 "$APP_PROFILE_BASE64" | |
| require_secret MASHANGXIE_IOS_KEYBOARD_PROFILE_BASE64 "$KEYBOARD_PROFILE_BASE64" | |
| require_secret MASHANGXIE_IOS_TEAM_ID "${{ secrets.MASHANGXIE_IOS_TEAM_ID }}" | |
| keychain_path="$RUNNER_TEMP/mashangxie-ios.keychain-db" | |
| keychain_password="$(openssl rand -base64 24)" | |
| certificate_path="$RUNNER_TEMP/mashangxie-ios.p12" | |
| printf '%s' "$CERTIFICATE_BASE64" | base64 --decode > "$certificate_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 "$certificate_path" -k "$keychain_path" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" | |
| security list-keychains -d user -s "$keychain_path" | |
| profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles" | |
| mkdir -p "$profiles_dir" | |
| install_profile() { | |
| encoded="$1" | |
| prefix="$2" | |
| source_path="$RUNNER_TEMP/$prefix.mobileprovision" | |
| plist_path="$RUNNER_TEMP/$prefix.plist" | |
| printf '%s' "$encoded" | base64 --decode > "$source_path" | |
| security cms -D -i "$source_path" > "$plist_path" | |
| uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$plist_path")" | |
| name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$plist_path")" | |
| cp "$source_path" "$profiles_dir/$uuid.mobileprovision" | |
| echo "${prefix}_profile=$name" >> "$GITHUB_OUTPUT" | |
| } | |
| install_profile "$APP_PROFILE_BASE64" app | |
| install_profile "$KEYBOARD_PROFILE_BASE64" keyboard | |
| - name: Build Ad Hoc IPA | |
| run: | | |
| make ios-ipa \ | |
| MASHANGXIE_DEVELOPMENT_TEAM="${{ secrets.MASHANGXIE_IOS_TEAM_ID }}" \ | |
| MASHANGXIE_APP_PROFILE_SPECIFIER="${{ steps.signing.outputs.app_profile }}" \ | |
| MASHANGXIE_KEYBOARD_PROFILE_SPECIFIER="${{ steps.signing.outputs.keyboard_profile }}" | |
| - name: Upload iOS package | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Mashangxie-iOS | |
| path: | | |
| dist/ios/Mashangxie-*-iOS.ipa | |
| dist/ios/Mashangxie-*-iOS.ipa.sha256 | |
| if-no-files-found: error |