Skip to content

feat(release): 打通 macOS、Windows 与 iOS 三平台发布 #146

feat(release): 打通 macOS、Windows 与 iOS 三平台发布

feat(release): 打通 macOS、Windows 与 iOS 三平台发布 #146

Workflow file for this run

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