Skip to content

Commit 2230582

Browse files
ci: build ExampleApp on EAS for native coverage on PRs (#262)
* ci: add EAS Workflow to build ExampleApp on PRs for native coverage Existing CI (CircleCI) only lints, type-checks, and runs Jest -- it never compiles native code, so Swift/Kotlin breakage in modules/* passes green. Add an EAS Workflow that builds the ExampleApp on both platforms for every PR to main; since the app depends on every module, a green build proves all native code compiles. Uses credential-free preview profiles (iOS simulator, Android APK) as a pure compile gate. * ci: trigger EAS workflow after linking Expo project * ci: enable EAS native compilation cache (EAS_USE_CACHE) for builds Set EAS_USE_CACHE=1 on the production profile (inherited by preview, which the PR builds use) to enable ccache-based native compilation caching -- the safest, highest-value cache for native-heavy Expo modules. ccache is content-addressed so it can't mask native breakage. Deliberately skip a custom Podfile.lock cache: local modules in modules/* can change generated native projects without touching yarn.lock, so a lockfile-keyed cache could go stale and hide the breakage this CI is meant to catch. Document the rationale in the workflow README. * ci: add cache-enabled ci:build:eas for EAS build hook eas-build-post-install previously ran the --no-cache ci:build, re-doing every module's tsc even though root postinstall already builds them with turbo cache earlier in the same EAS build. Add a cache-enabled ci:build:eas and point the EAS hook at it so the second pass is cache hits instead of a full rebuild. The release/publish path (ci:build, release) is intentionally left --no-cache and unchanged. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72f1d2e commit 2230582

5 files changed

Lines changed: 131 additions & 3 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# EAS Workflows — native build CI
2+
3+
This directory holds [EAS Workflows](https://docs.expo.dev/eas/workflows/get-started/)
4+
that give us **native build coverage** on pull requests.
5+
6+
## Why this exists
7+
8+
Our existing CI (CircleCI `test_and_build`) only does:
9+
10+
- `yarn lint`
11+
- `yarn ci:build` — which at the module level is just `tsc` (TypeScript only)
12+
- `yarn test` — Jest
13+
14+
None of that compiles native code. Every module in `modules/*` ships real
15+
native sources:
16+
17+
- iOS: `*.swift` + a `*.podspec`
18+
- Android: `build.gradle` + Kotlin/Java
19+
20+
So a change that breaks the Swift or Kotlin (or breaks the app build) currently
21+
passes CI green. Building `apps/ExampleApp` on EAS fixes this: the app depends on
22+
every module, so a green build proves the native code for all of them compiles
23+
on both platforms.
24+
25+
## What runs
26+
27+
`build-on-pr.yml` runs on every PR targeting `main` and builds the ExampleApp for
28+
both platforms using the `preview` profile from `eas.json`:
29+
30+
| Platform | Profile result | Credentials needed |
31+
| --- | --- | --- |
32+
| Android | APK | none (EAS-managed keystore) |
33+
| iOS | simulator build | none (simulator builds skip Apple distribution certs) |
34+
35+
These profiles are intentionally credential-free so the job is a pure
36+
"does it compile" gate.
37+
38+
## One-time setup (required for this to actually run)
39+
40+
EAS Workflows are triggered by Expo, not by GitHub Actions or CircleCI. To enable:
41+
42+
1. **Connect the repo to the Expo project.** In the Expo dashboard for
43+
`infinitered/react-native-mlkit` (projectId `4faa9328-e941-4395-879c-f558bf07e678`),
44+
go to **Project settings → GitHub** and link this GitHub repository.
45+
2. **Confirm billing / build credits.** Each PR triggers two builds (iOS + Android).
46+
Make sure the Expo account has capacity, or the jobs will queue/fail.
47+
3. (Optional) If you'd rather trigger from existing CI instead of Expo's GitHub
48+
integration, you can run `eas workflow:run .eas/workflows/build-on-pr.yml` from a
49+
job that has `EXPO_TOKEN` set, but the native EAS GitHub integration is simpler.
50+
51+
## Monorepo note
52+
53+
This is a Yarn 3 monorepo. EAS resolves project config (`app.json`/`eas.json`) from
54+
this directory (`apps/ExampleApp`), which is why the workflow lives here rather than
55+
at the repo root. EAS installs the full workspace and builds the local `modules/*`
56+
from source as part of the app build.
57+
58+
## Cost / tuning ideas
59+
60+
- To reduce spend, you could build **Android only** on PRs (Android breakage is the
61+
most common and Android needs no credentials) and keep iOS for `main` only.
62+
- Or gate builds behind a label / path filter so they only run when native files or
63+
`modules/*` change.
64+
65+
## Caching
66+
67+
EAS Build provides built-in caches (JS packages, CocoaPods, Maven/Gradle, and
68+
native compilation via `ccache`) with no configuration, so these builds are not
69+
fully cold.
70+
71+
On top of that we set `EAS_USE_CACHE=1` (in the `production` profile in
72+
`eas.json`, inherited by `preview`) to enable native-compilation caching. It's
73+
safe even for a build whose job is to catch native breakage, because `ccache` is
74+
content-addressed — changed source always recompiles, so it can't mask a failure.
75+
76+
We intentionally do **not** add a custom `cache` block (e.g. caching
77+
`Podfile.lock`). This repo's local Expo modules live in `modules/*`, and changing
78+
a module's podspec / `build.gradle` / `expo-module.config.json` alters the
79+
generated native project **without** touching `yarn.lock`. A lockfile-keyed cache
80+
would go stale and could hide exactly the native breakage this CI exists to catch.
81+
If build times become a real bottleneck, revisit this with a cache key that also
82+
invalidates on `modules/*` native config — and never cache the generated `ios/` or
83+
`android/` directories (CNG regenerates them every build).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Build ExampleApp on PRs
2+
3+
# Native build validation for pull requests.
4+
#
5+
# CircleCI already lints, type-checks (`yarn ci:build` == `tsc`) and runs Jest,
6+
# but it never compiles any native code. Every module under `modules/*` ships
7+
# real native sources (Swift podspecs + Android Gradle/Kotlin), and none of it
8+
# is compiled in CircleCI. This workflow closes that gap by building the
9+
# ExampleApp on EAS for both platforms: because the app depends on every module,
10+
# a successful build transitively proves the native code for all of them
11+
# compiles.
12+
#
13+
# Profiles come from eas.json:
14+
# - preview (iOS): simulator build -> no Apple distribution credentials needed
15+
# - preview (Android): APK build -> EAS-managed keystore, no Play creds needed
16+
#
17+
# Setup required (one-time) before this runs:
18+
# 1. Connect this GitHub repo to the Expo project (infinitered/react-native-mlkit)
19+
# in the Expo dashboard: Project settings -> GitHub.
20+
# 2. Ensure EAS build credits / billing are in place (each PR triggers 2 builds).
21+
# See ./README.md in this directory for details.
22+
23+
on:
24+
pull_request:
25+
branches:
26+
- main
27+
28+
jobs:
29+
build_android:
30+
name: Build Android (preview APK)
31+
type: build
32+
params:
33+
platform: android
34+
profile: preview
35+
36+
build_ios:
37+
name: Build iOS (preview simulator)
38+
type: build
39+
params:
40+
platform: ios
41+
profile: preview

apps/ExampleApp/eas.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
"ios": { "simulator": false }
3535
},
3636
"production": {
37-
"autoIncrement": true
37+
"autoIncrement": true,
38+
"env": {
39+
"EAS_USE_CACHE": "1"
40+
}
3841
}
3942
},
4043
"submit": {

apps/ExampleApp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"bundle:web": "npx expo export --platform web",
2828
"serve:web": "npx server dist",
2929
"prebuild:clean": "npx expo prebuild --clean",
30-
"eas-build-post-install": "yarn -T run ci:build"
30+
"eas-build-post-install": "yarn -T run ci:build:eas"
3131
},
3232
"dependencies": {
3333
"@expo-google-fonts/space-grotesk": "^0.2.2",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": true,
33
"//": [
4-
"CI scripts are all --no-cache because we aren't using Vercel's remote cache and the caches aren't working. It may be possible to cache dependencies in the future, but for now caching breaks the build."
4+
"The release/publish path (ci:build) stays --no-cache because we aren't using Vercel's remote cache. ci:build:eas is a cache-enabled variant used only by the EAS build hook (eas-build-post-install), mirroring the cached turbo run that postinstall already performs, so modules aren't tsc-built twice per EAS build."
55
],
66
"scripts": {
77
"build": "turbo run build",
@@ -13,6 +13,7 @@
1313
"setup": "./scripts/setup.sh",
1414
"version": "changeset version && yarn install --mode update-lockfile",
1515
"ci:build": "turbo run ci:build --filter=@infinitered/react-native-mlkit-core --no-cache && turbo run ci:build --filter=!@infinitered/react-native-mlkit-core --no-cache",
16+
"ci:build:eas": "turbo run ci:build --filter=@infinitered/react-native-mlkit-core && turbo run ci:build --filter=!@infinitered/react-native-mlkit-core",
1617
"ci:publish": "changeset publish",
1718
"ci:push-tags": "git push --follow-tags",
1819
"ci:trust": "./scripts/git-push-fork-to-upstream-repo.sh",

0 commit comments

Comments
 (0)