|
| 1 | +# CI workflows |
| 2 | + |
| 3 | +Three GitHub Actions workflows run on every PR: |
| 4 | + |
| 5 | +| Workflow | Jobs | |
| 6 | +|---|---| |
| 7 | +| [`ci.yml`](ci.yml) | `lint` (eslint + `check-ci-filter-coverage.ts` + typecheck) and `build-library` (`yarn workspaces ... prepare`). Skips markdown / docs-only changes via `paths-ignore`. | |
| 8 | +| [`test-bare-rn.yml`](test-bare-rn.yml) | Jest smoke test for `apps/bare-rn` | |
| 9 | +| [`build-apps.yml`](build-apps.yml) | Per-app build matrix — bundles + native builds for all 5 demo apps on Android + iOS | |
| 10 | + |
| 11 | +All three workflows skip draft PRs via `if: github.event.pull_request.draft != true`. `build-apps.yml` and `test-bare-rn.yml` additionally short-circuit on forks (`github.repository == 'software-mansion/react-native-executorch'`) so the expensive native matrix doesn't run on contributor forks. |
| 12 | + |
| 13 | +The rest of this README explains `build-apps.yml`, which is the expensive one and has the most moving parts. |
| 14 | + |
| 15 | +## The per-app build matrix |
| 16 | + |
| 17 | +A naive matrix runs ~20 min × 10 native cells × every push. Two layers cut that cost: |
| 18 | + |
| 19 | +1. **Path filtering** narrows the matrix per push — only apps whose files actually changed enter the matrix. |
| 20 | +2. **Content-hash cache markers** let cells that already passed for the current code skip the build entirely. |
| 21 | + |
| 22 | +Both layers read the same filter file: [`scripts/build-app-filters.yml`](../../scripts/build-app-filters.yml). |
| 23 | + |
| 24 | +``` |
| 25 | +push → detect-changes job → matrix of (app, platform) cells |
| 26 | + │ │ |
| 27 | + │ └─ each cell: compute content hash → cache lookup → skip if marker exists |
| 28 | + │ |
| 29 | + └─ git diff $before $head | matched against build-app-filters.yml |
| 30 | +``` |
| 31 | + |
| 32 | +## Filter file: `scripts/build-app-filters.yml` |
| 33 | + |
| 34 | +Single source of truth for "which files belong to which app". Uses YAML anchors so each path is named once and composed up: |
| 35 | + |
| 36 | +```yaml |
| 37 | +core-shared: &core-shared # files every app depends on |
| 38 | + - packages/react-native-executorch/src/{common,constants,errors,native,types,utils}/** |
| 39 | + - "{package.json,yarn.lock}" |
| 40 | + # ... |
| 41 | +android-shared: &android-shared # Android-only shared |
| 42 | + - .github/actions/build-android-app/** |
| 43 | + - packages/react-native-executorch/android/** |
| 44 | +cv-pkg: &cv-pkg # CV-specific TS modules + C++ models |
| 45 | + - packages/react-native-executorch/common/rnexecutorch/models/VisionModel.{cpp,h} |
| 46 | + - packages/react-native-executorch/common/rnexecutorch/models/{classification,...,vertical_ocr}/** |
| 47 | + - packages/react-native-executorch/src/{modules,hooks}/computer_vision/** |
| 48 | + # ... |
| 49 | +computer-vision-app: &computer-vision-app |
| 50 | + - *core-shared |
| 51 | + - *expo-fetcher |
| 52 | + - *cv-pkg |
| 53 | + - apps/computer-vision/** |
| 54 | +computer-vision-android: [*computer-vision-app, *android-shared] |
| 55 | +computer-vision-ios: [*computer-vision-app, *ios-shared] |
| 56 | +``` |
| 57 | +
|
| 58 | +The final `<app>-<platform>` entries are what the matrix consumes. Everything else is composition. |
| 59 | + |
| 60 | +### When to update the filter file |
| 61 | + |
| 62 | +| Change | Update | |
| 63 | +|---|---| |
| 64 | +| New file under `apps/<existing-app>/**` | Nothing — already covered by `<app>-app` | |
| 65 | +| New model directory under `packages/react-native-executorch/common/rnexecutorch/models/<domain>/` | `<domain>-pkg` anchor (add the directory name into its brace-expansion list) | |
| 66 | +| New TS module under `src/modules/<domain>/` or `src/hooks/<domain>/` | `<domain>-pkg` anchor | |
| 67 | +| New file that affects every app (e.g. new utility under `src/common/`) | Usually already covered by `core-shared`'s `src/{common,constants,errors,native,types,utils}/**` | |
| 68 | +| New iOS-only / Android-only source under `packages/react-native-executorch/ios/` or `/android/` | Already covered by `ios-shared` / `android-shared` (broad `**` globs) | |
| 69 | +| New demo app under `apps/<new-app>/` | New `<new-app>-pkg` + `<new-app>-app` + `<new-app>-android` + `<new-app>-ios` anchors, plus the apps list in `build-apps.yml`'s `Compute matrices` step | |
| 70 | + |
| 71 | +You don't have to remember this list. `scripts/check-ci-filter-coverage.ts` runs in `ci.yml`'s `lint` job and fails the PR if any file under `packages/react-native-executorch/` isn't matched by a filter — you'll get a list of orphans and which anchor to add them to. |
| 72 | + |
| 73 | +If a file genuinely doesn't belong to any build (e.g. `tsconfig.doc.json`), add it to `ALLOWLIST` in that script instead. |
| 74 | + |
| 75 | +## Push-incremental diff (instead of `dorny/paths-filter`) |
| 76 | + |
| 77 | +`dorny/paths-filter@v3` silently ignores its `base:` input on `pull_request` events (you'll see `'base' input parameter is ignored when action is triggered by pull request event` in the log). It always uses GitHub's PR-files API, which returns the *cumulative* PR diff — so every push to a PR re-evaluates filters against the entire PR diff and pulls every app that has ever been touched into the matrix. |
| 78 | + |
| 79 | +We replaced it with [`scripts/detect-changed-filters.ts`](../../scripts/detect-changed-filters.ts), which runs `git diff --name-only $base $head` and matches the result against `build-app-filters.yml` using picomatch. For `pull_request synchronize` events, `base` is `github.event.before` (the previous push SHA), so the diff is just what *this push* changed. For `opened` / `reopened` / `ready_for_review`, `github.event.before` isn't set and the script falls back to `pull_request.base.sha` (full PR diff — the right thing for those events). |
| 80 | + |
| 81 | +## Cache markers |
| 82 | + |
| 83 | +Even when a cell enters the matrix, [`compute-app-hash.ts`](../../scripts/compute-app-hash.ts) hashes every tracked file matched by that cell's filter (excluding `HASH_EXCLUDE`). The hash becomes a cache key: `build-<app>-<platform>-<hash>` (or `bundle-<platform>-<app>-<hash>` for the bundle job). If a marker with that key exists in the GitHub Actions cache, the cell exits before doing any real work. |
| 84 | + |
| 85 | +Markers are tiny (~250 bytes each — they exist only as cache-presence proofs, no payload). They're saved at the end of every successful build and live under the per-PR ref (`refs/pull/N/merge`) plus `refs/heads/main` for main-branch builds. |
| 86 | + |
| 87 | +### `HASH_EXCLUDE` |
| 88 | + |
| 89 | +Editing a file in `core-shared` triggers every app's matrix cell, but most edits to those files don't actually change build behavior — workflow tweaks, filter-file edits, etc. The hash excludes a small allowlist of orchestration files so editing them re-runs the matrix but every cell still hits its existing marker: |
| 90 | + |
| 91 | +```ts |
| 92 | +// compute-app-hash.ts |
| 93 | +const HASH_EXCLUDE = new Set([ |
| 94 | + '.github/workflows/build-apps.yml', |
| 95 | + 'scripts/build-app-filters.yml', |
| 96 | + 'scripts/detect-changed-filters.ts', |
| 97 | +]); |
| 98 | +``` |
| 99 | + |
| 100 | +If you make a workflow edit that *does* change build behavior — pinning a new action version, changing a `with:` input on a composite, swapping `runs-on:`, editing `.github/actions/setup/` (Node version, install flags) — markers won't reflect it. Force-clear the relevant cache entries from **Actions → Caches** in the repo UI. |
| 101 | + |
| 102 | +Edits inside the per-platform composite actions (`.github/actions/build-android-app/`, `.github/actions/build-ios-app/`) *do* invalidate markers automatically, because those paths are part of `android-shared` / `ios-shared` and contribute to the hash. |
| 103 | + |
| 104 | +## Common scenarios |
| 105 | + |
| 106 | +### "I pushed a one-line tweak to one app and CI fired everything" |
| 107 | + |
| 108 | +It shouldn't — push-incremental diff narrows to just that app's filter. If it didn't, check: |
| 109 | + |
| 110 | +- Did the tweak also touch a file in `core-shared`? `package.json`, `yarn.lock`, and anything under `packages/react-native-executorch/src/{common,constants,errors,native,types,utils}/` fan out to every app. |
| 111 | +- Is this the first push to a fresh PR? `pull_request opened` uses the PR base for the diff, which is correct but means the *entire PR diff* matches filters on the first run. Subsequent `synchronize` pushes will narrow. |
| 112 | + |
| 113 | +### "I edited build-apps.yml and now everything is rebuilding from scratch" |
| 114 | + |
| 115 | +`build-apps.yml` is in `HASH_EXCLUDE`, so cells should fan out but hit cache. If they're rebuilding for real: |
| 116 | + |
| 117 | +- Did you also change something else under `core-shared` in the same push? That would invalidate every hash. |
| 118 | +- Did the markers get evicted? GitHub's cache quota is 10 GB per repo with LRU eviction. Markers themselves are tiny but the yarn `node_modules` cache (~550 MB × Linux + macOS) sits alongside them. Heavy churn on other branches can evict markers. |
| 119 | + |
| 120 | +### "I want to force a full rebuild" |
| 121 | + |
| 122 | +**Actions → Caches** in the repo UI. Filter by `build-` or `bundle-` prefix and delete the relevant entries. Next push will cache-miss and rebuild. |
| 123 | + |
| 124 | +### "I added a new model directory and the lint job fails saying it's an orphan" |
| 125 | + |
| 126 | +`scripts/check-ci-filter-coverage.ts` walks every tracked file under `packages/react-native-executorch/` and verifies it matches at least one filter. Fix by adding the new path to the right `<domain>-pkg` anchor in `scripts/build-app-filters.yml`. If the file genuinely doesn't belong to any build (it's a doc, a license, etc.), add it to `ALLOWLIST` in `check-ci-filter-coverage.ts`. |
| 127 | + |
| 128 | +### "I'm working on a long-running branch and don't want CI on every push" |
| 129 | + |
| 130 | +Open the PR in draft. All three workflows skip on `draft != true`. Marking the PR as ready_for_review fires the workflows fresh on the current HEAD. |
0 commit comments