Skip to content

Skip unaffected native CI jobs and cache iOS and Gradle builds - #29

Merged
janicduplessis merged 3 commits into
mainfrom
@janic/native-ci-cache
Sep 23, 2026
Merged

janicduplessis merged 3 commits into
mainfrom
@janic/native-ci-cache

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #28 (base @janic/example-drop-rn-patch). Retarget to main once #28 merges.

Description

With #28 in place, the ios job takes about 5 min and android about 4 min, and every PR runs both, even a docs-only one. This PR skips native jobs a PR doesn't affect and caches the slow parts of each build. When the caches hit, ios drops to about 2 min and android to about 2.5 min.

Path filter. A new changes job compares the PR merge commit with its first parent (git diff HEAD^1 HEAD) and outputs ios/android. ios and android run only when their output is true:

  • both: src/, example/src/, top-level example/* files (package.json, Gemfile*, app.json, …), root package.json, pnpm-lock.yaml, pnpm-workspace.yaml, .nvmrc, ci.yml, .github/actions/
  • iOS only: ios/, example/ios/, *.podspec
  • Android only: android/, example/android/

Any other event sets both outputs to true: pushes to main, merge_group, workflow_dispatch, and the Release workflow's workflow_call (its event is the tag push). The job needs only contents: read. dorny/paths-filter would need pull-requests: read, and release.yml doesn't grant that to its verify call.

Risk: skipped required checks pass. ios and android are required checks, and GitHub counts a job skipped by if: as passing. If the changes job fails, both native jobs are skipped. The run still turns red, but changes isn't a required check. Consider adding it to the ruleset.

iOS caching.

  • Xcode compilation caching: COMPILATION_CACHE_ENABLE_CACHING=YES on the xcodebuild command line, which covers the app and every pod target. The CAS lives in <derivedDataPath>/CompilationCache.noindex (example/build/...) and is kept after the build. actions/cache persists it under a key built from the Xcode build number, pnpm-lock.yaml, Podfile.lock and the native sources, with a restore key that falls back across source changes.
    • This is incompatible with React Native's ccache_enabled: Clang caching needs explicit modules and no C_COMPILER_LAUNCHER (swift-build).
    • A warm CI build reports 380 Cache hit and 6 Cache miss for the C/ObjC/C++/Swift compile tasks.
  • example/ios/Pods is cached by Podfile.lock. This saves about 75 s of prebuilt RN core, RN dependencies and Hermes tarball downloads.
  • ruby/setup-ruby has bundler-cache: true.
  • The job now runs on macos-26 instead of macos-latest, so a runner image switch can't silently invalidate the caches. macos-latest currently resolves to that image, with Xcode 26.6 (17F113).

Gradle caching. actions/setup-java in ci.yml and e2e-android.yml has cache: gradle. This uses the plain actions/cache provider. gradle/actions/setup-gradle would default to its proprietary "enhanced" provider.

Test plan

CI job wall times:

Job main before #28 #28 this PR, no cache this PR, caches hit
ios 16m08s (run) 5m05s 5m01s (run) 2m11s (run)
android 4m55s 4m16s 4m07s 2m32s (same run)

Warm ios step times: CAS restore 3 s (180 MB), Pods restore 12 s, pod install 24 s (1m35s–2m without the caches, including bundle install), xcodebuild 48 s. Without the CAS, xcodebuild took 1m50s–2m29s, and with a warm CAS 48 s–1m25s. Runner variance is large, so treat the CAS gain as roughly 30–60 s. Warm android step times: 1.4 GB Gradle cache restore in about 30 s, then gradlew 1m37s–1m52s instead of 3m24s–3m50s.

  • Path filter:
    • Simulated the changes script against local merge commits: this branch gives ios=true android=true, a README-only change gives false false, and a non-PR event gives true true. The regexes are also covered by a file-by-file table (ios/… → iOS only, example/android/… → Android only, docs/…/e2e/… → neither, lockfile → both).
    • A workflow_dispatch run on this branch ran both jobs (run).
  • Locally (M4, Xcode 26.6, CI xcodebuild command, clean DerivedData each time): no caching 24 s, caching with an empty CAS 35 s, caching with only the CAS kept 15.6 s.

Not verified:

  • A real docs-only PR skipping both jobs. This PR edits ci.yml, so both jobs run. The skip path is covered only by the local simulation above.
  • e2e-android with the Gradle cache (label-gated, not run).

Limitations

  • Caches written by PR runs are scoped to that PR. A new PR starts warm only from caches saved by main pushes, which always build both platforms. The warm numbers above come from re-runs of this PR.
  • The CAS cache grows as native sources change within one lockfile generation, because each new key saves the restored entries plus new ones. It starts over when pnpm-lock.yaml, Podfile.lock or the Xcode build changes.
  • The remaining Android time is mostly compiling the included React Native Gradle plugin (about 35 s), installing NDK 27.1 (about 20 s, not on the runner image) and CMake. Gradle's build cache (--build-cache) could cover the plugin compile, but setup-java saves only when its key changes, so it would need setup-gradle. That's not included here.

@janicduplessis
janicduplessis changed the base branch from @janic/example-drop-rn-patch to main September 23, 2026 18:23
@janicduplessis
janicduplessis marked this pull request as ready for review September 23, 2026 18:26
@janicduplessis
janicduplessis merged commit 69fd0eb into main Sep 23, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant