Skip to content

Commit 8c02351

Browse files
committed
docs(ci): add README explaining the per-app build matrix
1 parent a0bcf39 commit 8c02351

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

‎.github/workflows/README.md‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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

Comments
 (0)