Inconsistent versioning and forgetting to increment the expo app version? Not great after spending 20 minutes for an app build. When I see this:
Version code X has already been used. Try another version code.
Here's what expo says about app versioning:
EAS Build can help manage developer-facing build versions automatically by incrementing these versions for you if you opt into using theremote
version source, which is the recommended behaviour. Optionally, you can choose to use alocal
app version source, which means you control versions manually in their respective config files.
If you're using EAS remote builds and have autoincrement enabled, you don’t need to worry about versioning. Eas handles it for you automatically.
But then your app builds depend either on eas, or on you to remember to increment the correct version number.
I have an alternative for you. You don't need to worry about autoincrementing, bumping the version, and you don't need to rely on remote eas builds to manage your versions.
My solution uses Release Please, GitHub Actions and Conventional Commits.
What's nice about this is that I just push to GitHub, and GitHub does all the heavy lifting.
It decides when to build and what version to build - Is it a fix, patch or a breaking change. I don't care. My release ends up in TestFlight or the Play test track.
About EAS Cloud Builds
EAS Build is free to start with. At the time of writing, the free plan allows up to 30 low-priority builds and a maximum of 15 iOS builds per month.
If you opt for usage-based pricing, it's faster but can also get costly if you release often.
I Prefer GitHub
I prefer GitHub for managing my versions. Not because it's cheaper, better, or whatever. I have an entire continuous integration and delivery setup over there. I use it because I like the process I've set up.
To make my process clearer, I’ve visualized it in the following diagram:
I develop a feature on a branch, then I make a PR to production branch. Before I can merge to production branch, my pal GitHub says:
- I have to do some checks before you can merge...Alright, carry on.
I merge.
Then my other buddy Release please steps in:
- Hey, I see a couple of fixes. I'd better increment your version from x.x.1 to x.x.2. I'll make a PR and you check if it's ok.
- Sure, merge it.
- Alright, I'll continue with rest of my tasks.
- Wait? What tasks?!
- Rest of the release tasks you've asked me to do. If there's nothing to do, I'll just chill until next time.
How much will GitHub cost?
At first, nothing. Every account has free 2000 CI minutes every month.
My build takes approximately 20 minutes for Android and around 30 minutes for iOS.
Building on a macOS machine consumes 10 times as many GitHub Actions free CI minutes. The multiplier for GitHub-hosted runners (e.g., the 10x multiplier for macOS) applies only to the free minutes included in your plan. After you exceed the free minutes allowance, GitHub charges you the standard per-minute rates for the type of runner you use.
Therefore, with 2000 free minutes, a macOS I can perform approximately 7 macOS builds within the free 2000-minute allowance. After the free allowance, GitHub will charge ~2.4$ per ios build and ~0.30$ per android build.
It's more expensive than EAS if you want to go with GitHub.
How to manage expo versions
If you look at your expo project, there are exactly 4 versions of your project.
- package.json has a version. It usually does not matter, but it needs to be there
- expo.version is user facing version. I guess it's a pretty representation of the version that user sees.
- expo.android.versionCode - is build version for android
- expo.ios.buildCode - is build version for iOS
Wouldn't it be great if there was just one version?
My way is to consolidate all different version properties and just worry about package.json version.
All the rest comes from package.json version. My app.config.ts then looks like:
This solves my issue of maintaining version consistency.
What about version incrementing
You can automate version bumps by enforcing the conventional commits style first. If you don't want to memorize commit format, use https://commitizen-tools.github.io/commitizen/.
I wrote about conventional commits in my guidelines for tidy commits. What's cool about these kinds of commits is the consistent structure. The key advantage is that this structure allows for automatic detection of version numbers based on the type of changes made.
You also need a process that understands conventional commit log and bumps the version accordingly. That's why the release please process.
What you get with release-please+commitizen:
- Automatic Versioning: If you use conventional commits, then there's a consensus how versioning should be done. fix => patch, feat => minor,
feat!
,fix!
,refactor!
=> major - Changelog magic: It creates a neat categorized changelog with links to everything you've done.
- Commit and Tag: Once the version and changelog are ready, it automatically commits them and tags your release in Git.
How to setup version auto increment
Set release-please-config.json. It tells what kind of projects need to be versioned, where they are and what type they are. Default is node. My app is in the subdirectory called app
:
{
"include-component-in-tag": true,
"include-v-in-tag": true,
"tag-separator": "@",
"release-type": "node",
"separate-pull-requests": true,
"release-search-depth": 20,
"commit-search-depth": 20,
"packages": {
"app": {
"path": "app"
}
},
}
Set .release-please-manifest.json. It's the source of truth for your app version. Unfortunately, I can't skip version repetition. It would be great if release-please would just read package.json version, but it is how it is:
{
"app": "1.0.0"
}
And example workflow looks like:
name: Release Please
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
app--tag_name: ${{ steps.release.outputs.app--tag_name}}
app--release_created: ${{ steps.release.outputs.app--release_created}}
steps:
- uses: actions/checkout@v4
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
release-play-internal-testing:
needs: release-please
runs-on: ubuntu-latest
if: ${{needs.release-please.outputs.app--release_created}}
steps:
# ...
release-testflight:
needs: release-please
runs-on: macos-latest
if: ${{needs.release-please.outputs.app--release_created}}
steps:
# ...
Next time Release Please detects a change, it will first read the manifest file, then bump the version to the next major, minor, or patch release. And then make a PR with modified package.json and updated CHANGELOG file.
What you need to do is review that PR and merge it. After merge goes through, other pipeline for publishing should be triggered. Specifics on how to setup publishing is for another post.