CI/CD of a EXPO managed-flow project based on tag

I was thought that expo managed flow CI/CD should be very easy than the vanilla React Native project.

But I found I was wrong, it encountered some cavets that spend much time to solve.

Anyway, it is true easier than vanilla React Native Project.

The build strategy

We have many environments(develop,staging,production) which have different api endpoint. So we will build different APP with corresponding APPID/package name:
production: com.kazge
staging: com.kazge.staging
develop: com.kazge.develop

For iOS, only production APP will go to release after tested. others will only available on testflight to test.

For Android, all is available as apk to download to test.

We will use the tag name as the command name, like <environemnt>-<platform>-<action>-<build-number>. So one build pipeline will handle different build action(build,upload,update-over-the-air) for different environment, the version will bump up by the tag, also the build number which have to be ascending number value.

What another tag name format I think may be better is like <environemnt>/<platform>/<action>/<build-number> which will have a good structure in a git desktop UI.

The app.config.js

expo support dynamic app config which is easy to build based on environment.

We use both app.json(put immutable config) and app.config.js(parse value from build environment).

app.json

{
  "expo": {
    "entryPoint": "./index.ts",
    "name": "Kazge",
    "slug": "kazge",
    "orientation": "portrait",
    "icon": "./src/assets/icons/icon-1024.png",
    "scheme": "kazge",
    "splash": {
      "image": "./src/assets/images/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "notification": {
      "icon": "./src/assets/icons/icon-1024.png"
    }
  }
}

app.config.js

const expoAppConfig = ({ config }) => {
  //

  const result = {
    ...config,
    version: process.env.KAZGE_VERSION,
    name: process.env.KAZGE_NAME,
    slug: process.env.KAZGE_SLUG,
    ios: {
      bundleIdentifier: process.env.KAZGE_IOS_BUNDLE_ID,
      usesIcloudStorage: true,
      supportsTablet: true,
      config: {
        usesNonExemptEncryption: false,
      },
      buildNumber: process.env.KAZGE_BUILD_NUMBER,
    },
    android: {
      package: process.env.KAZGE_ANDROID_PACKAGE,
      googleServicesFile: process.env.KAZGE_ANDROID_FCM_JSON_PATH,
      // eslint-disable-next-line radix
      versionCode: parseInt(process.env.KAZGE_BUILD_NUMBER),
      useNextNotificationsApi: true,
      permissions: [
        'CAMERA',
        'READ_EXTERNAL_STORAGE',
        'WRITE_EXTERNAL_STORAGE',
        'RECEIVE_BOOT_COMPLETED',
      ],
    },

    // All values in extra will be passed to your app.
    extra: {
      app: {
        apiBaseURL: process.env.KAZGE_API_BASEURL,
      },
      bugsnag: {
        apiKey: process.env.BUGSNAG_API_KEY,
      },
    },
    hooks: {
      postPublish: [
        {
          file: '@bugsnag/expo/hooks/post-publish.js',
          config: {},
        },
      ],
    },
  };

  return result;
};

export default expoAppConfig;

As ypu see, the app version, app name, build number, api path, bugsnag key, google FCM key are all build into the app as build environment.

Prepare for the credentials

The most complex part that is easy to get wrong is to prepare the credentials required to run expo command to build ipa and apk.

iOS - App Store

For iOS, what we need to do is mannually set up Certificates and identifiers etc. on apple developer console.

  • create a account(apple id) who have permission(developer resources) to access certificates, identifiers, this account will be used by CI/CD tool to login to apple to fetch these information.
  • Set up APP in appstoreconnect
  • Set up PUSH key in apple developer console
  • Set up Distribution Certificate, Identifiers(enable PUSH,associate domains, icloud etc.) in apple developer console

After set them up.

We use expo to handle the profile creation automatically.

yarn expo build:ios --non-interactive --no-wait -t archive \
    --release-channel "${RELEASE_CHANNEL}" \
    --apple-id "${IOS_APPLE_ID}" \
    --team-id "${IOS_TEAM_ID}" \

set the apple id and team id will help expo cli to identify which team and app you are try to build.

Android - Google Play Store

It is better to “Use app signing by Google Play”.

If you create the key by yourself, run command yarn expo build:android -t apk will prompt the choice to let you upload the key. this will only happen at the first time you run the build without build key not upload to expo yet. But of course you could run yarn expo build:android -c to clear the keystore and reupload again. BE SURE to make a backup of your keys(expo fetch:android:keystore) before clear them.

Build

Once you set up the cedentials correctly to expo, the build is pretty straight forward. expo will build based on the configuration and credentials on expo’s server

iOS Build

yarn expo build:ios --non-interactive --no-wait -t archive \
    --release-channel "${RELEASE_CHANNEL}" \
    --apple-id "${IOS_APPLE_ID}" \
    --team-id "${IOS_TEAM_ID}" \

Android Build

yarn expo build:android --release-channel ${RELEASE_CHANNEL} -t apk

Update Over the Air

In the command we all specified the release-channel, this is related to update over the air.

export RELEASE_CHANNEL="${STAGE_NAME}_${DEVICE_PLATFORM}_${VER_NAME}_${APP_BUILD_NUMBER}"

Our strategy is, OTA is based on build number, not the semver. This make sure the build is strict to each build.

In this way, it is safe to make ota build to each semiver that have been released. also for staging, the ota will not affect many build - it only affect the one sepecified with the build number value.

Upload to Testflight

To upload the ipa file to testflight, we first need get the download url of ipa file.

Get the build status and ipa download url

But expo have not a good command to let us know when the ipa is ready to donwload.

So we have to write a shell to do poll the status

IPA_URL=''
DCOUNT=0
while [[ $IPA_URL != http* ]] && [[ $DCOUNT -le 20 ]]
do
    sleep 20
    IPA_URL="$( yarn expo url:ipa --non-interactive | tail -2 )"
    echo "url:ipa reponse ..."
    echo $IPA_URL
    IPA_URL="$( echo $IPA_URL | head -n 1 )"
    echo "download url ..."
    echo $IPA_URL
    DCOUNT=`expr $DCOUNT + 1`
    echo "COUNT $DCOUNT"
done

if [[ $IPA_URL != http* ]];
then
    echo "exit as URL is not correct $IPA_URL"
    exit 1
fi

echo "got original url $IPA_URL"

aha! that’s it, an loop run expo url:ipa to get the url, and check if it is a correct download link, :joy:

Upload by fastlane

As we know, expo upload:ios should do this task, but… actually it does not work in most case, it will response a error Upload iOs: [iTMSTransporter] -Djava.ext.dirs=……/ext is not supported. Use -classpath instead.

So what we are doing is use fastlane, in bitrise, this is pretty simple as it is a build in widget.

Upload to Google Play Store

This have the same problem like ios, need get the download link by a loop

Get the build status and apk download url

getApkUrl(){
    IPA_URL=''
    DCOUNT=0
    while [[ $IPA_URL != http* ]] && [[ $DCOUNT -le 20 ]]
    do
        sleep 20
        IPA_URL="$( yarn expo url:apk --non-interactive | tail -2 )"
        echo "url:apk reponse ..."
        echo $IPA_URL
        IPA_URL="$( echo $IPA_URL | head -n 1 )"
        echo "download url ..."
        echo $IPA_URL
        DCOUNT=`expr $DCOUNT + 1`
        echo "COUNT $DCOUNT"
    done

    if [[ $IPA_URL != http* ]];
    then
        echo "exit as URL is not correct $IPA_URL"
        exit 1
    fi

    echo "got original url $IPA_URL"

    _arrCMDS=(${IPA_URL// / })
    IPA_URL="${_arrCMDS[0]}"
    eval "$1=$IPA_URL"
}

Upload apk to Google Play

Fortunately, expo is able to upload apk to google play without error. yarn expo upload:android --latest --key "$_GOOGLE_AUTH_JSON" _GOOGLE_AUTH_JSON is the key you get from your google play account.

Conclusion

expo CI/CD should to be much more eaiser if they solve the upload to ios problem. After we setup the credentials correctly once, the subsequent build steps is very straight forward.


Total views.

© 2013 - 2020. All rights reserved.

Powered by Hydejack v6.6.1