Framework
Expo CI/CD without EAS
expo prebuild into ephemeral directories, then build natively on machines you own — and ship OTA updates as a distribution target like any other.
The pipeline, step by step
rn/bootstrapinstall, with the package manager read from your lockfilern/prebuildexpo prebuild, into directories that do not survive the jobrn/bundlethe bundle and its sourcemapios/buildthe native iOS build, from the prebuilt projectdistribute/otaa JavaScript-only update, as a target among targets
Every one of these is in the step library, with the credentials it can ask for and whether it may run on a fork’s pull request — generated from the file the server itself reads.
Prebuild output is ephemeral on purpose
ios/ and android/ are generated per job and thrown away. A prebuild output that persists between builds is a directory that drifts from the config that generated it, and the drift surfaces as a build that works on CI and not on the next machine.
OTA is a distribution target, not a different product
An Expo update goes through the same release, the same approval policy and the same audit trail as a store submission. The reason is that an OTA update is a release — it changes what your users are running — and treating it as a lighter-weight thing is how an unreviewed change reaches production.
Continuous Native Generation, without the lock-in
Nothing here requires an Expo account or a hosted build service. The pipeline runs on your Macs, the credentials stay in your infrastructure, and the config plugin story works exactly as it does locally because it is the same command.
What a build produces
IPA, AAB, dSYM, the bundle, the sourcemap, and OTA update manifests.
Symbol files are collected on every build rather than on the ones somebody remembers to configure. A crash report that arrives three weeks later resolving to a memory address is a crash report nobody can act on, and the difference is a build-time decision that cannot be made retroactively.