ci: per-app build matrix with precise path triggers #17
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
| # Per-app build matrix gated by `dorny/paths-filter`. Adding a new model | |
| # directory, controller, or top-level module under packages/react-native-executorch/ | |
| # probably means updating the filter block below — `core-shared` if it affects every | |
| # app, or one of the per-app `<app>-pkg` anchors otherwise. CI runs | |
| # `scripts/check-ci-filter-coverage.ts` (in ci.yml's lint job) to flag uncovered | |
| # files so this stays accurate. | |
| name: Example apps build check | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| jobs: | |
| detect-changes: | |
| if: github.repository == 'software-mansion/react-native-executorch' && github.event.pull_request.draft != true | |
| runs-on: ubuntu-latest | |
| outputs: | |
| android_apps: ${{ steps.matrix.outputs.android_apps }} | |
| ios_apps: ${{ steps.matrix.outputs.ios_apps }} | |
| bundle_apps: ${{ steps.matrix.outputs.bundle_apps }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Detect changed apps | |
| id: filter | |
| if: github.event_name != 'workflow_dispatch' | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| # Diff against the previous commit (synchronize/push) or the PR base | |
| # (opened/reopened/ready_for_review). Without this, dorny defaults to | |
| # the PR base on every push, so each commit re-evaluates the | |
| # cumulative PR diff and pulls every app whose filter ever matched | |
| # into the matrix — even when the latest push only touched one app. | |
| base: ${{ github.event.before || github.event.pull_request.base.sha }} | |
| filters: | | |
| # Cross-platform shared infrastructure | |
| core-shared: &core-shared | |
| - .github/workflows/build-apps.yml | |
| - packages/react-native-executorch/{scripts,third-party}/** | |
| - packages/react-native-executorch/{package.json,tsconfig.json} | |
| - packages/react-native-executorch/common/{ada,runner}/** | |
| - packages/react-native-executorch/common/rnexecutorch/{Error,ErrorCodes,Log}.h | |
| - packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.{cpp,h} | |
| - packages/react-native-executorch/common/rnexecutorch/{data_processing,host_objects,jsi,metaprogramming,tests,threads,utils}/** | |
| - packages/react-native-executorch/common/rnexecutorch/models/BaseModel.{cpp,h} | |
| - packages/react-native-executorch/src/{common,constants,errors,native,types,utils}/** | |
| - packages/react-native-executorch/src/index.ts | |
| - packages/react-native-executorch/src/modules/{BaseModule.ts,general/**} | |
| - packages/react-native-executorch/src/hooks/{general/**,useModule.ts,useModuleFactory.ts} | |
| - "{package.json,yarn.lock}" | |
| # Platform-specific shared | |
| android-shared: &android-shared | |
| - .github/actions/build-android-app/** | |
| - packages/react-native-executorch/android/** | |
| ios-shared: &ios-shared | |
| - .github/actions/build-ios-app/** | |
| - packages/react-native-executorch/ios/** | |
| - packages/react-native-executorch/react-native-executorch.podspec | |
| # Resource fetchers | |
| expo-fetcher: &expo-fetcher | |
| - packages/expo-resource-fetcher/** | |
| bare-fetcher: &bare-fetcher | |
| - packages/bare-resource-fetcher/** | |
| # Per-app package paths (TS modules + C++ models for the app's domain) | |
| llm-pkg: &llm-pkg | |
| - packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h} | |
| - packages/react-native-executorch/common/rnexecutorch/models/llm/** | |
| - packages/react-native-executorch/common/rnexecutorch/models/privacy_filter/** | |
| - packages/react-native-executorch/src/modules/natural_language_processing/{LLMModule,PrivacyFilterModule,TokenizerModule}.ts | |
| - packages/react-native-executorch/src/hooks/natural_language_processing/{useLLM,usePrivacyFilter,useTokenizer}.ts | |
| - packages/react-native-executorch/src/controllers/LLMController.ts | |
| cv-pkg: &cv-pkg | |
| - packages/react-native-executorch/common/rnexecutorch/models/VisionModel.{cpp,h} | |
| - packages/react-native-executorch/common/rnexecutorch/models/{classification,instance_segmentation,object_detection,ocr,pose_estimation,semantic_segmentation,style_transfer,text_to_image,vertical_ocr}/** | |
| - packages/react-native-executorch/src/{modules,hooks}/computer_vision/** | |
| - packages/react-native-executorch/src/controllers/{BaseOCRController,OCRController,VerticalOCRController}.ts | |
| speech-pkg: &speech-pkg | |
| - packages/react-native-executorch/common/pfft/** | |
| - packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h} | |
| - packages/react-native-executorch/common/rnexecutorch/models/{speech_to_text,text_to_speech,voice_activity_detection}/** | |
| - packages/react-native-executorch/src/modules/natural_language_processing/{SpeechToTextModule,TextToSpeechModule,VADModule,TokenizerModule}.ts | |
| - packages/react-native-executorch/src/hooks/natural_language_processing/{useSpeechToText,useTextToSpeech,useVAD,useTokenizer}.ts | |
| text-embeddings-pkg: &text-embeddings-pkg | |
| - packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h} | |
| - packages/react-native-executorch/common/rnexecutorch/models/embeddings/** | |
| - packages/react-native-executorch/src/modules/natural_language_processing/{TextEmbeddingsModule,TokenizerModule}.ts | |
| - packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts | |
| - packages/react-native-executorch/src/hooks/natural_language_processing/{useTextEmbeddings,useTokenizer}.ts | |
| - packages/react-native-executorch/src/hooks/computer_vision/useImageEmbeddings.ts | |
| # Per-app bundle: core + fetcher + pkg + app dir | |
| llm-app: &llm-app | |
| - *core-shared | |
| - *expo-fetcher | |
| - *llm-pkg | |
| - apps/llm/** | |
| computer-vision-app: &computer-vision-app | |
| - *core-shared | |
| - *expo-fetcher | |
| - *cv-pkg | |
| - apps/computer-vision/** | |
| speech-app: &speech-app | |
| - *core-shared | |
| - *expo-fetcher | |
| - *speech-pkg | |
| - apps/speech/** | |
| text-embeddings-app: &text-embeddings-app | |
| - *core-shared | |
| - *expo-fetcher | |
| - *text-embeddings-pkg | |
| - apps/text-embeddings/** | |
| bare-rn-app: &bare-rn-app | |
| - *core-shared | |
| - *bare-fetcher | |
| - *llm-pkg | |
| - apps/bare-rn/** | |
| # Final per-platform per-app filters (the only ones the matrix consumes) | |
| llm-android: [*llm-app, *android-shared] | |
| llm-ios: [*llm-app, *ios-shared] | |
| computer-vision-android: [*computer-vision-app, *android-shared] | |
| computer-vision-ios: [*computer-vision-app, *ios-shared] | |
| speech-android: [*speech-app, *android-shared] | |
| speech-ios: [*speech-app, *ios-shared] | |
| text-embeddings-android: [*text-embeddings-app, *android-shared] | |
| text-embeddings-ios: [*text-embeddings-app, *ios-shared] | |
| bare-rn-android: [*bare-rn-app, *android-shared] | |
| bare-rn-ios: [*bare-rn-app, *ios-shared] | |
| - name: Compute matrices | |
| id: matrix | |
| run: | | |
| all='["llm","computer-vision","speech","text-embeddings","bare-rn"]' | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "android_apps=$all" >> "$GITHUB_OUTPUT" | |
| echo "ios_apps=$all" >> "$GITHUB_OUTPUT" | |
| echo "bundle_apps=$all" >> "$GITHUB_OUTPUT" | |
| else | |
| changes='${{ steps.filter.outputs.changes }}' | |
| android_apps=$(echo "$changes" | jq -c '[.[] | select(endswith("-android")) | sub("-android$"; "")]') | |
| ios_apps=$(echo "$changes" | jq -c '[.[] | select(endswith("-ios")) | sub("-ios$"; "")]') | |
| bundle_apps=$(echo "$changes" | jq -c '[.[] | select(endswith("-app")) | sub("-app$"; "")]') | |
| echo "android_apps=$android_apps" >> "$GITHUB_OUTPUT" | |
| echo "ios_apps=$ios_apps" >> "$GITHUB_OUTPUT" | |
| echo "bundle_apps=$bundle_apps" >> "$GITHUB_OUTPUT" | |
| fi | |
| bundle: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.bundle_apps != '[]' && needs.detect-changes.outputs.bundle_apps != '' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| app: ${{ fromJSON(needs.detect-changes.outputs.bundle_apps) }} | |
| platform: [android, ios] | |
| concurrency: | |
| group: bundle-${{ matrix.platform }}-${{ matrix.app }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| - name: Compute bundle hash | |
| id: hash | |
| run: | | |
| h=$(npx ts-node scripts/compute-app-hash.ts "${{ matrix.app }}-app") | |
| echo "key=bundle-${{ matrix.platform }}-${{ matrix.app }}-$h" >> "$GITHUB_OUTPUT" | |
| - name: Lookup pass marker | |
| id: cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ${{ runner.temp }}/ci-marker | |
| key: ${{ steps.hash.outputs.key }} | |
| lookup-only: true | |
| - name: Skip notice | |
| if: steps.cache.outputs.cache-hit == 'true' | |
| run: echo "Skipping bundle — bundle-${{ matrix.platform }}-${{ matrix.app }} already passed at this content hash." | |
| - name: Build workspace packages | |
| if: steps.cache.outputs.cache-hit != 'true' && matrix.app == 'bare-rn' | |
| run: yarn workspaces foreach --all --topological-dev run prepare | |
| - name: Bundle JS for ${{ matrix.platform }} | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| working-directory: apps/${{ matrix.app }} | |
| run: | | |
| if [ "${{ matrix.app }}" = "bare-rn" ]; then | |
| npx react-native bundle \ | |
| --platform ${{ matrix.platform }} \ | |
| --entry-file index.js \ | |
| --bundle-output /tmp/bundle.js \ | |
| --dev false | |
| else | |
| npx expo export \ | |
| --platform ${{ matrix.platform }} \ | |
| --output-dir /tmp/expo-export | |
| fi | |
| - name: Save pass marker | |
| if: steps.cache.outputs.cache-hit != 'true' && success() | |
| run: | | |
| mkdir -p "${{ runner.temp }}/ci-marker" | |
| touch "${{ runner.temp }}/ci-marker/passed" | |
| - name: Cache pass marker | |
| if: steps.cache.outputs.cache-hit != 'true' && success() | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ${{ runner.temp }}/ci-marker | |
| key: ${{ steps.hash.outputs.key }} | |
| build-android: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.android_apps != '[]' && needs.detect-changes.outputs.android_apps != '' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| app: ${{ fromJSON(needs.detect-changes.outputs.android_apps) }} | |
| concurrency: | |
| group: android-${{ matrix.app }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: recursive | |
| - name: Build Android app | |
| uses: ./.github/actions/build-android-app | |
| with: | |
| app-path: apps/${{ matrix.app }} | |
| expo-prebuild: ${{ matrix.app == 'bare-rn' && 'false' || 'true' }} | |
| filter-name: ${{ matrix.app }}-android | |
| build-ios: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.ios_apps != '[]' && needs.detect-changes.outputs.ios_apps != '' | |
| runs-on: macos-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| app: ${{ fromJSON(needs.detect-changes.outputs.ios_apps) }} | |
| concurrency: | |
| group: ios-${{ matrix.app }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: recursive | |
| - name: Build iOS app | |
| uses: ./.github/actions/build-ios-app | |
| with: | |
| app-path: apps/${{ matrix.app }} | |
| expo-prebuild: ${{ matrix.app == 'bare-rn' && 'false' || 'true' }} | |
| filter-name: ${{ matrix.app }}-ios |