feat(reader): replace Readium with Skia-based rendering #402
Workflow file for this run
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: Build iOS IPA | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| branches: ["main"] | |
| paths: | |
| - "apps/mobile/**" | |
| - "packages/**" | |
| - "package.json" | |
| - "package-lock.json" | |
| - "tsconfig.base.json" | |
| - ".github/workflows/build_ios_ipa.yml" | |
| push: | |
| branches: ["main"] | |
| tags: ["v*"] | |
| paths: | |
| - "apps/mobile/**" | |
| - "packages/**" | |
| - "package.json" | |
| - "package-lock.json" | |
| - "tsconfig.base.json" | |
| - ".github/workflows/build_ios_ipa.yml" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: build-ios-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-ipa: | |
| name: Build Unsigned IPA | |
| runs-on: macos-26 | |
| timeout-minutes: 45 | |
| env: | |
| CCACHE_COMPILERCHECK: content | |
| CCACHE_BASEDIR: ${{ github.workspace }} | |
| CCACHE_NOHASHDIR: "true" | |
| CCACHE_MAXSIZE: 1G | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set Build Metadata | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin main:refs/remotes/origin/main | |
| RELEASE_VERSION="" | |
| if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then | |
| BUILD_NUMBER="$(git rev-list --count origin/main)" | |
| BUILD_CHANNEL="pr" | |
| BUILD_LABEL="PR #${{ github.event.pull_request.number }}" | |
| ARTIFACT_ID="pr-${{ github.event.pull_request.number }}-${GITHUB_SHA:0:7}" | |
| elif [ "$GITHUB_REF_TYPE" = "tag" ]; then | |
| if [[ ! "$GITHUB_REF_NAME" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then | |
| echo "::error::Release tag must use vX.Y.Z format: $GITHUB_REF_NAME" | |
| exit 1 | |
| fi | |
| RELEASE_VERSION="${BASH_REMATCH[1]}" | |
| BUILD_NUMBER="$(git rev-list --count HEAD)" | |
| BUILD_CHANNEL="tag" | |
| BUILD_LABEL="Build $BUILD_NUMBER" | |
| ARTIFACT_ID="$GITHUB_REF_NAME" | |
| elif [ "$GITHUB_REF_NAME" = "main" ]; then | |
| BUILD_NUMBER="$(git rev-list --count HEAD)" | |
| BUILD_CHANNEL="main" | |
| BUILD_LABEL="Build $BUILD_NUMBER" | |
| ARTIFACT_ID="main-${GITHUB_SHA:0:7}" | |
| else | |
| BUILD_NUMBER="$(git rev-list --count origin/main)" | |
| BUILD_CHANNEL="branch" | |
| BUILD_LABEL="$GITHUB_REF_NAME" | |
| SAFE_REF="${GITHUB_REF_NAME//\//-}" | |
| ARTIFACT_ID="branch-${SAFE_REF}-${GITHUB_SHA:0:7}" | |
| fi | |
| { | |
| echo "APP_VERSION=$RELEASE_VERSION" | |
| echo "APP_BUILD_NUMBER=$BUILD_NUMBER" | |
| echo "APP_BUILD_CHANNEL=$BUILD_CHANNEL" | |
| echo "APP_BUILD_LABEL=$BUILD_LABEL" | |
| echo "APP_ARTIFACT_ID=$ARTIFACT_ID" | |
| } >> "$GITHUB_ENV" | |
| - name: Verify release tag target | |
| # 与 Android 保持一致:tag 构建不应依赖仓库层 Ruleset 才能拦住坏 tag。 | |
| if: github.ref_type == 'tag' | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin main:refs/remotes/origin/main | |
| git merge-base --is-ancestor "$GITHUB_SHA" origin/main || { | |
| echo "::error::Release tag must point to a commit reachable from main." | |
| exit 1 | |
| } | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install JavaScript dependencies | |
| run: npm ci | |
| - name: Select Xcode 26.6 | |
| run: | | |
| set -euo pipefail | |
| XCODE_APP="$( | |
| find /Applications -maxdepth 1 -type d \ | |
| -name 'Xcode_26.6*.app' | | |
| sort -V | | |
| tail -1 | |
| )" | |
| if [ -z "$XCODE_APP" ]; then | |
| echo "::error::Xcode 26.6 is not installed on this runner." | |
| exit 1 | |
| fi | |
| # export 使当前步骤也能读到(GITHUB_ENV 只影响后续步骤)。 | |
| export DEVELOPER_DIR="$XCODE_APP/Contents/Developer" | |
| echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV" | |
| xcodebuild -version | |
| xcodebuild -showsdks | head -5 | |
| - name: Install ccache | |
| run: | | |
| if ! command -v ccache >/dev/null 2>&1; then | |
| if brew tap | grep -Fxq 'aws/tap'; then | |
| brew untap aws/tap | |
| fi | |
| brew install ccache | |
| fi | |
| # Xcode 26 omits user-defined build settings from CompileC environments | |
| # (facebook/react-native#55381), so pass the binary as a real process env var. | |
| echo "CCACHE_BINARY=$(command -v ccache)" >> "$GITHUB_ENV" | |
| ccache --version | head -1 | |
| - name: Restore iOS compiler cache | |
| id: ios-ccache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: ~/Library/Caches/ccache | |
| key: >- | |
| ios-ccache-${{ runner.os }}-xcode26.6-${{ hashFiles('package-lock.json') }}-${{ github.sha }} | |
| restore-keys: | | |
| ios-ccache-${{ runner.os }}-xcode26.6-${{ hashFiles('package-lock.json') }}- | |
| ios-ccache-${{ runner.os }}-xcode26.6- | |
| - name: Reset ccache statistics | |
| run: ccache --zero-stats | |
| - name: Show build environment | |
| run: | | |
| echo "BUILD_CHANNEL=${{ env.APP_BUILD_CHANNEL }}" | |
| echo "BUILD_LABEL=${{ env.APP_BUILD_LABEL }}" | |
| echo "BUILD_NUMBER=${{ env.APP_BUILD_NUMBER }}" | |
| echo "RELEASE_VERSION=${{ env.APP_VERSION }}" | |
| - name: Generate iOS project | |
| working-directory: apps/mobile | |
| env: | |
| APP_VERSION: ${{ env.APP_VERSION }} | |
| APP_BUILD_NUMBER: ${{ env.APP_BUILD_NUMBER }} | |
| APP_BUILD_CHANNEL: ${{ env.APP_BUILD_CHANNEL }} | |
| APP_BUILD_LABEL: ${{ env.APP_BUILD_LABEL }} | |
| run: npx expo prebuild --non-interactive --no-install --platform ios | |
| - name: Restore CocoaPods cache | |
| id: ios-pods | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| ~/Library/Caches/CocoaPods | |
| apps/mobile/ios/Pods | |
| apps/mobile/ios/Podfile.lock | |
| key: >- | |
| ios-pods-${{ runner.os }}-xcode26.6-${{ | |
| hashFiles( | |
| 'package-lock.json', | |
| 'apps/mobile/package.json', | |
| 'apps/mobile/app.config.ts', | |
| 'apps/mobile/plugins/**', | |
| 'apps/mobile/modules/**/ios/**', | |
| 'apps/mobile/modules/**/*.podspec', | |
| 'apps/mobile/modules/**/expo-module.config.json' | |
| ) | |
| }} | |
| - name: Install CocoaPods dependencies | |
| working-directory: apps/mobile | |
| run: npx pod-install ios | |
| - name: Save CocoaPods cache | |
| # pod install 之后才保存(save 立即执行,放前面会存空目录/旧内容)。 | |
| # 只由 main 写入:PR 缓存存 merge-ref scope,关闭后无复用价值。 | |
| if: success() && github.ref == 'refs/heads/main' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| ~/Library/Caches/CocoaPods | |
| apps/mobile/ios/Pods | |
| apps/mobile/ios/Podfile.lock | |
| key: ${{ steps.ios-pods.outputs.cache-primary-key }} | |
| - name: Build unsigned iOS app | |
| run: | | |
| xcodebuild \ | |
| -workspace apps/mobile/ios/Novella.xcworkspace \ | |
| -scheme Novella \ | |
| -configuration Release \ | |
| -destination 'generic/platform=iOS' \ | |
| -derivedDataPath apps/mobile/ios/build \ | |
| -showBuildTimingSummary \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| CODE_SIGN_IDENTITY="" \ | |
| COMPILER_INDEX_STORE_ENABLE=NO \ | |
| build | |
| - name: Show ccache statistics | |
| if: always() | |
| run: ccache --show-stats | |
| - name: Write iOS build summary | |
| if: always() | |
| run: | | |
| STATS="$(ccache --print-stats)" | |
| CACHE_MISSES="$(awk '$1 == "cache_miss" { print $2 }' <<< "$STATS")" | |
| CACHE_HITS="$( | |
| awk '$1 == "direct_cache_hit" || $1 == "preprocessed_cache_hit" { total += $2 } END { print total + 0 }' <<< "$STATS" | |
| )" | |
| CACHEABLE_CALLS="$((CACHE_HITS + CACHE_MISSES))" | |
| HIT_RATE="$( | |
| awk -v hits="$CACHE_HITS" -v calls="$CACHEABLE_CALLS" \ | |
| 'BEGIN { if (calls > 0) printf "%.1f%%", hits * 100 / calls; else printf "—" }' | |
| )" | |
| CACHE_SIZE_KIB="$(awk '$1 == "cache_size_kibibyte" { print $2 }' <<< "$STATS")" | |
| CACHE_SIZE="$( | |
| awk -v kib="$CACHE_SIZE_KIB" \ | |
| 'BEGIN { if (kib >= 1048576) printf "%.1f GiB", kib / 1048576; else printf "%.1f MiB", kib / 1024 }' | |
| )" | |
| XCODE_VERSION="$(xcodebuild -version | awk 'NR == 1 { print $2 }')" | |
| case "${{ job.status }}" in | |
| success) BUILD_OUTCOME="✅" ;; | |
| failure) BUILD_OUTCOME="❌" ;; | |
| cancelled) BUILD_OUTCOME="⏹️" ;; | |
| *) BUILD_OUTCOME="${{ job.status }}" ;; | |
| esac | |
| cat >> "$GITHUB_STEP_SUMMARY" <<EOF | |
| | Xcode Workspace | Scheme | Configuration | Xcode Version | Build Outcome | | |
| | --- | --- | --- | --- | :---: | | |
| | Novella.xcworkspace | Novella | Release | $XCODE_VERSION | $BUILD_OUTCOME | | |
| ### Compiler Caching - ⚡ ccache | |
| | Cacheable Calls | Hits | Misses | Hit Rate | Cache Size | | |
| | ---: | ---: | ---: | ---: | ---: | | |
| | $CACHEABLE_CALLS | $CACHE_HITS | $CACHE_MISSES | $HIT_RATE | $CACHE_SIZE | | |
| EOF | |
| - name: Save iOS compiler cache | |
| # 编译完成之后才保存(save 立即执行)。 | |
| # 只由 main 写入:PR 缓存存 merge-ref scope,关闭后无复用价值, | |
| # 且避免 1G 条目快速挤爆 10G 仓库缓存上限。 | |
| if: success() && github.ref == 'refs/heads/main' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: ~/Library/Caches/ccache | |
| key: ${{ steps.ios-ccache.outputs.cache-primary-key }} | |
| - name: Verify built version matches metadata | |
| run: | | |
| set -euo pipefail | |
| APP_PATH="apps/mobile/ios/build/Build/Products/Release-iphoneos/Novella.app" | |
| test -d "$APP_PATH" | |
| BUILT_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PATH/Info.plist")" | |
| BUILT_BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APP_PATH/Info.plist")" | |
| echo "Built version: $BUILT_VERSION ($BUILT_BUILD)" | |
| if [ -n "${{ env.APP_VERSION }}" ] && [ "$BUILT_VERSION" != "${{ env.APP_VERSION }}" ]; then | |
| echo "::error::Built version $BUILT_VERSION does not match tag version ${{ env.APP_VERSION }}" | |
| exit 1 | |
| fi | |
| if [ "$BUILT_BUILD" != "${{ env.APP_BUILD_NUMBER }}" ]; then | |
| echo "::error::Built build number $BUILT_BUILD does not match ${{ env.APP_BUILD_NUMBER }}" | |
| exit 1 | |
| fi | |
| - name: Package IPA | |
| run: | | |
| mkdir Payload | |
| cp -R apps/mobile/ios/build/Build/Products/Release-iphoneos/Novella.app Payload/ | |
| zip -qry "novella-${{ env.APP_ARTIFACT_ID }}-unsigned.ipa" Payload | |
| - name: Upload IPA | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: novella-unsigned-ipa-${{ env.APP_ARTIFACT_ID }} | |
| path: novella-*-unsigned.ipa | |
| if-no-files-found: error | |
| retention-days: 14 | |
| compression-level: 0 |