iTranslated by AI
Proposed Versioning Rules for Expo (React Native): Managing OTA Updates Effectively
Since I started using Expo, it has been absolutely fantastic for someone like me who had struggled with React Native several times before. However, I found the release management and releaseChannel aspects a bit tricky to handle. So, I decided to think about the best way to manage them.
Challenges
In Expo, when you perform a production build, it is published at the same time as the build. Because Expo has an Over-the-Air (OTA) update mechanism, the app gets updated immediately.
This means that if you use the same releaseChannel, creating a build for store review will trigger an OTA update for existing users at that point.
While it is possible to disable OTA through configuration, it is a very useful feature in emergencies, so completely turning it off feels like a waste.
I'm not exactly an expert in mobile app development, so I've been feeling my way through this, but I've come up with a versioning strategy that might offer the best of both worlds. If you know of a better way, please let me know.
My Personal Expo Versioning Rules
Unlike libraries, there isn't much point in strictly adhering to Semantic Versioning for consumer-facing apps. However, since it's a familiar set of rules, I decided to customize a versioning scheme based on it.
Following the x.y.z format, it looks like this:
- x (major) - Breaking changes. Used when API compatibility is lost, etc.
- y (minor) - Updated with every store release. Using dates might also work.
- z (patch) - Build updates that don't involve a store release. Used for re-builds for review or OTA updates.
Major versions are almost the same as in Semantic Versioning, used when there are breaking changes. Since this isn't a library, it won't happen often—perhaps when the server's API version becomes incompatible.
The minor version is where I differ significantly from Semantic Versioning. For mobile apps, the store release cycle is such a major event that I wanted to reflect it. So, I decided to use the minor version for this. if you release weekly, you might prefer using a date format.
Patch versions are kept open for OTA updates. They might also be updated when rebuilding during or before the review process.
Following this rule, the major version will likely be used most frequently, resulting in versions like 1.234.2. If you use dates for the minor version, it might look like 1.20200901.1.
Release Channel Management
Next, I'll set up a release-channel operation based on this rule, where OTA is applied only to patch updates.
I could have written my own, but the semver-extract library was just right with its small source size. It echoes the version like this:
$ semver-extract --pjson --minor -x
1.2.x
I'll set this up in package.json:
"scripts": {
"release-channel": "echo v$(semver-extract --pjson --minor -x)",
"build:ios": "expo build:ios --release-channel=production-$(npm run release-channel --silent)"
}
By following this Release Channel rule, updates will be made to channels like production-v1.2. If you are moving across store releases, such as from 1.2.3 to 1.3.0, OTA updates will not occur. On the other hand, if there is an issue with 1.2.3 and you want to update to 1.2.4, you can utilize OTA.
Appendix: Tips for Versioning
Appendix 1: How about Production and Staging?
I thought it would be easier to just separate them by slug and bundleIdentifier.
I try to store the parts I want to override for staging in a JSON file and customize app.config.js to overwrite them.
// app-staging.json
{
"name": "My App [Staging]",
"displayName": "My App [Staging]",
"expo": {
"name": "My App [Staging]",
"description": "My App [Staging]",
"slug": "staging-my-app",
"ios": {
"bundleIdentifier": "my.app.staging",
}
}
}
// app.config.js
import merge from "deepmerge"
import stagingConfig from "./app-staging.json"
const overrideEnv = (baseConfig) => {
if (process.env.BUILD_ENV === "staging") {
return merge.all([baseConfig, stagingConfig])
}
return baseConfig
}
export default ({ config }) => {
const conf = overrideEnv(config)
return conf
}
It would be nice if I could get the releaseChannel within app.config.js, but unfortunately, that doesn't seem possible, so let's pass BUILD_ENV in package.json.
// package.json
"scripts": {
"build:ios:staging": "BUILD_ENV=staging expo build:ios --release-channel=staging-$(npm run release-channel --silent)"
}
Appendix 2: It's convenient to set up react-native-version in postversion
I used to find it tedious to manually update the buildNumber and versionCode in app.json, but by using react-native-version, I hardly have to worry about it anymore.
"scripts": {
"postversion": "react-native-version"
}
Now, when you run yarn version or npm version, it will be updated automatically afterwards. Very useful.
Summary
Basically, these are versioning rules I came up with assuming the use of Expo, but they might be fairly applicable whenever OTA updates are involved.
Expo is great.
Discussion