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

Ka
5 min readOct 24, 2020

--

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

But I found I am wrong, it still 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.

Originally published at https://blog.kazge.com on October 24, 2020.

--

--

Ka
Ka

Written by Ka

Ka is a veteran have more than 15 years of experience in software industry with a passion. He love reading and writing. He is looking for a remote position.

No responses yet