CI/CD pipeline for React Native apps
Building React Native apps is a fraction of the whole app development SDLC. It comes with a whole bunch of tooling and processes around it. It’s very easy to get lost in the myriad of choices of tools and documentations. So, here I have collated the CI/CD workflows and pipelines in a flow diagram illustration and how we can implement it using Github actions.
Full resolution illustration Link
Github Link: https://github.com/paramsinghvc/Timely
Code Development
- Developers write the code in the IDE of their choice. The code formatting can differ for different devs. To ensure a universal formatting style, tools like Prettier, ESLint and EditorConfig can be used these days very easily.
- As soon as, you commit the code, static code analysis can be triggered on the local machine itself through automated pre-commit hook using husky which can be configured as follows in package.json
3. Once the checks pass, the code is committed and you can push your branch to Github.
Continuous Integration — I
In the first pass of CI pipeline, we perform like linting, testing, coverage reporting whenever a Pull Request is raised against a specified branch, commonly develop, preprod/staging and main/master to ensure code quality and nothing broken gets merged into these important branches.
The steps can be represented by the github actions yml file as follows:
Here, we’re defining a job to run on a trigger action which is whenever a PR is raised against master, develop or preprod branch. It can be summarised in the following steps:
- Checkout your repository
- Install NodeJS and configure version and package manager as yarn
- Install node modules using yarn ci which looks like this in package.json script
"preci": "rm -fr node_modules","ci": "yarn install --frozen-lockfile"
4. Run lint command to run eslint
"lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
5. Run Unit & Integration tests using jest
6. The last step is an optional decorator action for PR with coverage report as
Continuous Integration-II
After the install and test step, we want to make sure that the builds are passing while the PR is being reviewed.
Below install-and-test job in the pr.yml, we can have
Both of these point to other reusable workflows viz android-build.yml and ios-build.yml
Note how secrets: inherit
are being passed as it’s important to do, otherwise the secrets referred in those files won’t get populated. More on Github Secrets here
iOS Build
The ios-build looks like this
At the first glance, it seems like there’s a lot going on. Well, that’s how manual codesigning of an iOS app looks like. Unless you’re shipping automatically signed app on xcode, you’re supposed to do the codesigning manually by generating certificates and mobileprovision files. More on this coming soon, but lets keep the scope of discussion limited to CI/CD now.
Steps:
- After installing nodejs and yarn node modules on a macos machine this time, ruby and cocoapods are also installed and its cache strategy is setup based on the Podfile.lock contents on which a hash is generated and equated against.
- Then we build the IPA app bundle using ios-build-action which internally using xcodebuild and fastlane. It takes parameters like base64 encoded P12 and mobileprovision files which can be encoded as
$ base64 < MyApp.p12 | tr -d '\n' | tee p12.base64$ base64 < MyApp.mobileprovision | tr -d '\n' | tee mobileprovision.base64
and kept in Github Secrets along with others as
Team ID can be found in the Apple Developer portal
If everthing goes well, it’ll produce app named in the output-path with .ipa extension.
3. Now, you can upload the .ipa file as a github artifact which will appear on the Github action as
You can’t really do much with the ipa file other than distributing it via Apple testflight or App store, unlike Android APK which you can directly install on an Android device.
Android Build
Similarly we’re caching gradle dependencies here and for building the signed bundle we’re using ./gradlew bundleRelease
command. Code signing is also being done here but handled in the build.gradle file internally. Likewise, we’re uploading the aab artifact here as well which can be uploaded to the Google Play console to perform a release. Lastly, we get out build badges updated
Continuous Deployment
Up till now, we’ve only focused on the code quality being merged into the main branches through all these checks but once the code gets merged into these main branches, it should automatically get released and deployed to the users, either fully automatically or with a manual intervention.
After mulling over multiple possibilities of achieving it, I’ve up with the following way which makes sense.
- Whenever a branch is merged into dev, preprod or master, we would intend to publish a dev, preprod or a prod version of the app to the corresponding distribution store viz App store/Testflight for Apple or Google Playstore for Android
- A Github action would be triggered listening to the push event on a branch as
3. Next step is an interesting one as we need to increment/change the version of our Android and iOS apps or else the App store and Play Store won’t accept them during distribution. How can we do and maintain that in a Javascript environment? The best way one can think of package.json version and then keeping it as a source of truth.
The anatomy of a Semver (Semantic Version) looks like this
We need to automate type of version bump into the CD process. One great way of doing this is by using Conventional Commits specification and commitlint
Conventional commits in conjunction with commitlint forces you to write meaningful commit messages by validating against the conventional commits spec which looks something like this
type(scope?): subjectEg: fix(server): send cors headers
Based on the type value, we can determine the severity of the commit. For eg:
- If the type is chore or fix or refactor, it’s a simple patch.
- If it’s a feature which is backward compatible, it can come under minor.
- If it’s a breaking change which is backward compatible, it comes under major.
Now considering a git push having multiple commits, if any of the commits match major wording, the version bump is set as major and so on for minor and patch.
The pre-release id can also be set which is a suffix and can be useful while working on dev or preprod branches as you wouldn’t want to increment versions unnecessarily and frequently on these branches, so bumping the iteration or preid number makes sense here.
A github action with support for custom preid suffixes is available here
Above action will bump the package.json version intelligently based on commits and also commit it to the codebase.
4. Next step is to base our React native android and ios app versions on this. A simple npm utility as react-native-version can chime here as
It’ll make changes in the relevant Gradle and Info.plist files
5. Next, we have to commit these changed files to the source control as
6. We can now create a Github release using
Once this is done, we can go ahead with building our final bundle to test and publish it to the Appcenter.
We need to reuse our android-build and ios-build worflows here with an extra input boolean property of publish set to true based on which publishing artifact generated will be uploaded to Appcenter.
Once uploaded, it’ll appear under the releases section on Appcenter for your app. This is a little manual step. If you want to fully automate everything, you can omit Appcenter and upload to playstore, testflight and App store right from the github actions. There exist github actions for the same on the Github Marketplace
Clicking a release will open a detail view from where you can distribute your app bundle on Play store or App Store or Testflight for Apple.
Github Link: https://github.com/paramsinghvc/Timely
Summary
The importance of automation of code quality checks and deployment can’t be emphasised enough, be it for a small project or a large enterprise project, no body likes the hassle of doing all the steps manually. An ideal flow should do all the things for you right after you make a sweet commit and push to any version control system from your machine. It’s difficult to get your head around all the steps and finding the right flow that suits your needs. Luckily, there are great tools available like Github Actions, CircleCI in this date with a gigantic marketplace having plugins for most of your needs. React Native tooling itself can be quite overwhelming in itself. You spend more time doing the tooling and infra around it than coding sometimes. So, I tried to summarise my learnings and discoveries above which might help.
Happy Coding 🤓