Automating Mobile Versioning (React-Native) with GitHub Actions

Keeping Android and iOS version numbers in sync and up to date is a small but crucial part of mobile app development. Whether you’re preparing for a production release or testing on internal distribution channels, incrementing your app’s version code (Android) or build number (iOS) is a step you can’t skip.

Manually updating these numbers is error-prone and tedious, especially when working in a team or releasing frequently. Fortunately, with GitHub Actions, we can automate this entire process directly from our CI pipeline.

In this post, I’ll walk you through how to automatically bump version numbers for Android and iOS projects using GitHub CI, ensuring consistent versioning, fewer manual mistakes, and a smoother release workflow.

In a React Native project, we use Changesets that handles semantic versioning based on commit-level changes, serving as the source of truth for internal version bumps and release automation.

When a release is ready and merged into main, our GitHub CI pipeline automatically handles native versioning for both Android and iOS.

  • Merging develop into main triggers Changesets to publish a new release, bumping the package.json version according to semantic versioning rules.
  • A GitHub Actions workflow, configured to run on every push to main, picks up this version bump.
  • For Android, the workflow reads the existing versionCode from build.gradle and increments it by 1.
  • For iOS, it reads the new version directly from package.json and sets it as the CFBundleShortVersionString in Info.plist.
  • Finally, the updated Android and iOS version files are committed and pushed back to the main branch.
The commits made by GitHub CI
name: Release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

env:
  ANDROID_PATH: android/app/build.gradle
  ANDROID_VERSION_CODE: 0
  VERSION_NAME: ''

jobs:
  release:
    name: Release
    timeout-minutes: 15
    runs-on: macos-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install Dependencies
        run: yarn

      - name: Install Bash 4 and GNU sed on Mac
        run: |
          brew install gnu-sed

          echo "/usr/local/bin" >> $GITHUB_PATH
          echo "$(brew --prefix)/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH

      - name: Extract existing version code for Android
        run: |
          # Extract version number from package.json
          version_name=$(grep "version" package.json | sed 's/^.*"version": "//g' | sed 's/",$//g')

          # Get existing version code from build.gradle
          android_version_code=$(grep "versionCode" ${{ env.ANDROID_PATH }} | awk '{print $2}' | tr -d '\n')

          # Increment existing version code by 1
          android_version_code=$((android_version_code + 1))

          # Set environment variable for later use
          echo "VERSION_NAME=$version_name" >> $GITHUB_ENV
          echo "ANDROID_VERSION_CODE=$android_version_code" >> $GITHUB_ENV

      - name: Increase version code and change version name for Android
        run: |
          # Update build.gradle with new version code and name
          echo "${{ env.ANDROID_VERSION_CODE }}"
          sed -i "s/versionCode [0-9]\+/versionCode ${{ env.ANDROID_VERSION_CODE }}/g" ${{ env.ANDROID_PATH }}
          sed -i "s/versionName \"[^\"]*\"/versionName \"${{ env.VERSION_NAME }}\"/g" ${{ env.ANDROID_PATH }}

      - name: Commit and push changes
        run: |
          git config user.email "github-actions@github.com"
          git config user.name "Github Actions"
          git commit -am "🤖: Bump Android version to ${{ env.ANDROID_VERSION_CODE }}"
          git push origin HEAD

      - uses: yanamura/ios-bump-version@v1
        with:
          version: ${{ env.VERSION_NAME }}
          project-path: ios

      - name: Commit and push changes
        run: |
          git config user.email "github-actions@github.com"
          git config user.name "Github Actions"
          git commit -am "🍏: Bump iOS version to ${{ env.VERSION_NAME }}"
          git push origin HEAD

      - name: Bump versions for each config environment file
        run: |
          find config -type f -name '*.env' | xargs gsed -i -e 's/APP_VERSION=\"[^\"]*\"/APP_VERSION=\"${{ env.VERSION_NAME }}\"/g'
          git config user.email "github-actions@github.com"
          git config user.name "Github Actions"
          git commit -am "Bump config .env versions to ${{ env.VERSION_NAME }}"
          git push origin HEAD

Global Variables in Twig with Silex

Recently I assigned my user object to a template in my UserController. The moment I did this i realised I should assign this globally instead of reassigning it to each view. In my set-up, all my Controllers extend my CoreController class. It’s a place where I just put up some proxies that do a lot of general stuff so that my UserController (or other controllers) won’t be cluttered.

I’ve added my silex-base git repository to github, feel free to give me feedback on how my project structure looks. This repository can be installed using Composer and gives you the basic stuff you’ll need in an application. It used the Symfony Security component for registering and logging users in. Plus bonus points for twitter bootstrap.

I’ve added the filters class from my previous post (Extending Twig template engine with Silex) and a mysql dump for users and logging.  The project structure looks like this and links to the Github repository:

Silex-base Structure

Now, back to the global variable issue|bug|feature:
Because of my CoreController which has methods like getTwig() and getUser() I could easily do something like this:

    /**
     * @return \Twig_Environment
     */
    protected function getTwig() {
        // add globals
        $this->app['twig']->addGlobal('user', $this->getUser());
        return $this->app['twig'];
    }

    /**
     * @return User|Null|string
     */
    protected function getUser() {
        if(is_null($this->getSecurity()->getToken())) {
            return null;
        }

        return $this->getSecurity()->getToken()->getUser();
    }

    /**
     * @return SecurityContext
     */
    protected function getSecurity() {
        return $this->app['security'];
    }

Great, I can now use my $user object in any template. Next up is getting phpunit working within Silex.
Any hints and tips on my project structure are greatly appreciated.