Automatically build your app and send it to the app store using Bitbucket Pipelines or Github Actions
Steps
Build your app and let Expo manage your key
Generate a keystore
Sign the app and set it up in the Play Console
Setup Netlify to serve the app
Export the app to Netlify’s public URL
Build the app and mention that URL (will be used for OTA)
Send the builds (.aab
and .ipa
)to a distribution service (Firebase App Distribution or Microsoft AppCenter)
The reason i’m building with turtle
and not with expo
is because Expo builds are put in a build queue, which can take up to 45 minutes to build your app.. I can not waste this many minutes in a CI/CD pipeline. For reference, the Bitbucket free plan gives you only 50 build minutes for an entire month. I actually ran out of build minutes while putting the pipeline together..
Github gives your 2000 action minutes though. Go Github!
Needed
You need to build and upload your first apk
in order to enrol into App Signing by Google Play
Note that you’ll have to enable/create a release track in order to enable app signing.
How it works
The key with which you sign your app becomes your app’s upload key
If you want to use the same signing key across multiple stores, make sure to provide your own signing key when you opt in to app signing by Google Play, instead of having Google generate one for you.
So, if you want to use your own app signing key and are sure you can keep it secure and will never lose it, you need to provide Google with your own key. You do that when you’re opting in to App Signing by Google Play
Think of the app signing key as the master key and the upload key as an additional key on top of it. If Google generates the app signing key, they own it.
dist
My app’s URL is now: https://expo-devops-pipeline.netlify.app/
You need to build the app at least once for it to generate a keystore. If you have already done this then skip this step.
Before building, make sure your config file contains the required Android and iOS config. This includes version numbers and package names to identify your app builds.
1{
2 "expo": {
3 "name": "Your App Name",
4 "icon": "./path/to/your/app-icon.png",
5 "version": "1.0.0",
6 "slug": "your-app-slug",
7 "ios": {
8 "bundleIdentifier": "com.yourcompany.yourappname",
9 "buildNumber": "1.0.0"
10 },
11 "android": {
12 "package": "com.yourcompany.yourappname",
13 "versionCode": 1
14 }
15 }
16}
then login to your Expo account with expo login
and run
1expo build:android
and when prompted select the first option to Generate new keystore (previously it used to be Let Expo handle the process!)
✔ Choose the build type you would like: › app-bundle
Checking if there is a build in progress...
Configuring credentials for forcespenpals in project expo-devops-pipeline
✔ Would you like to upload a Keystore or have us generate one for you?
If you don't know what this means, let us generate it! :) › Generate new keystore
Keystore updated successfully
Unable to find an existing Expo CLI instance for this directory; starting a new one...
Starting Metro Bundler on port 19001.
Publishing to channel 'default'...
Building iOS bundle
Building Android bundle
Finished building JavaScript bundle in 25430ms.
Analyzing assets
Finished building JavaScript bundle in 21802ms.
Finished building JavaScript bundle in 969ms.
Finished building JavaScript bundle in 943ms.
Uploading assets
No assets changed, skipped.
Processing asset bundle patterns:
- /media/aamnah/Files/Projects/expo-devops-pipeline/**/*
Uploading JavaScript bundles
Publish complete
The manifest URL is: https://exp.host/@aamnah/expo-devops-pipeline. Learn more.
The project page is: https://expo.io/@aamnah/expo-devops-pipeline. Learn more.
› Closing Expo server
› Stopping Metro bundler
Checking if this build already exists...
Build started, it may take a few minutes to complete.
You can check the queue length at https://expo.io/turtle-status
You can make this faster. 🐢
Get priority builds at: https://expo.io/settings/billing
You can monitor the build at
https://expo.io/dashboard/aamnah/builds/47defda7-9a59-47fa-be54-b9f2c5d4ce31
Waiting for build to complete.
You can press Ctrl+C to exit. It won't cancel the build, you'll be able to monitor it at the printed URL.
⠏ Build queued...
...
After it has been successfully built, get the details for the keystore with
1expo fetch:android:keystore
Configuring credentials for aamnah in project expo-devops-pipelinezu
Saving Keystore to /media/aamnah/Files/Projects/expo-devops-pipeline/expo-devops-pipeline.jks
Keystore credentials
Keystore password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key alias: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Path to Keystore: /media/aamnah/Files/Projects/expo-devops-pipeline/expo-devops-pipeline.jks
You’ll need to provide these to turtle
when we build the standalone apps with it
Now we have a file called bitbucket-pipelines.yml
in our repo that will contain all the steps to run for building and deploying the app. We’ll come back to these settings later to setup repository variables once we have all the Expo and keystore values we need.
Now, setup repository variables. This will include all of the necessary information needed to sign your app, including the keystore location, keystore password, key name, and key password.
EXPO_ANDROID_KEYSTORE_BASE64
- base64-encoded Android keystoreEXPO_ANDROID_KEYSTORE_PASSWORD
- Android keystore passwordEXPO_ANDROID_KEYSTORE_ALIAS
- Android keystore aliasEXPO_ANDROID_KEY_PASSWORD
- Android key passwordEXPO_PUBLIC_URL
- URL where the app is publishedAlso, git pull
in your project to start editing the file locally
I am not going to commit the keystore file to git, but i need to use it in the pipeline..
The solution is to base64 encode the key, save it as a repository variable, and then decode and save the key in a file as part of the build step. Then i’ll save that key as an artifcat so that i am able to use it in further steps.
To encode the key
1# openssl base64 -in <infile> -out <outfile>
2openssl base64 -A -in keystore.jks
-in
is for an input file-out
can be provided. If not, it’ll output to terminal-A
will put it on a single line, which is useful when you’re saving it as a variableOn macOS, you can base64-encode the contents of a file and copy the string to the clipboard by running base64 some-file | pbcopy
in a terminal.
To decode the key (saved as a variable) and save it in a file
1echo $DEBUG_KEYSTORE_BASE64 | base64 --decode > keystore.jks
In the pipeline i have defined it as a step of it’s own
1- step: &decode-keystore
2 name: Decode the keystore
3 script:
4 - echo ${EXPO_ANDROID_KEYSTORE_BASE64} | base64 --decode > keystore.jks
5 artifacts:
6 - keystore.jks
Now we start editing the bitbucket-pipelines.yml
file. You can validate the file here
node_modules/
before installationpackage-lock.json
) existsnpx expo publish --clear
(as mentioned in Expo docs) doesn’t work. You have to install it inside the pipelineThe --unsafe-perm
flag avoids the following error when installing npm packages
ERR! sharp EACCES: permission denied, mkdir '/root/.npm'
set -e
vs. set +e
Pipelines achieves the default behaviour of exiting on first error by prepending your script with the “set -e” bash command. You can “undo” this at any point in your script using the command “set +e”. If you want all the commands in your script to execute regardless of errors then put “set +e” at the top of your script. If you just want to ignore the error for one particular command then put “set +e” before that command and “set -e” after it.
You can define and re-use steps with YAML anchors.
&
to define a chunk of configuration*
to refer to that chunk elsewhere 1image: node:latest
2
3definitions:
4 caches:
5 npm: '${HOME}/.npm'
6 jest: .jest
7 steps:
8 - step: &expo-publish
9 name: Publish to Expo
10 caches:
11 - npm
12 - node
13 script:
14 - npm ci
15 - npm i -g expo-cli
16 - expo login -u ${EXPO_USERNAME} -p ${EXPO_PASSWORD}
17 - expo publish --clear
18
19pipelines:
20 default:
21 - step: *expo-publish
1definitions:
2 scripts:
3 - script: &install-netlify
4 - npm install -g netlify-cli
5
6 - script: &commonScript1
7 - echo "common script 2"
8
9 steps:
10 - step: &build
11 name: Common Step 1
12
13 - step: &deploy
14 name: Common Step 2
15 deployment: test
16
17pipelines:
18 default:
19 - step: *build # use common step as is
20 - step:
21 <<: *deploy # update or override values with <<
22 deploymetr: staging
23 - step:
24 script:
25 - *install-netlify # use pre-defined scripts
26 - *commonScript2
27 - echo "extra script here.."
>
symbol is for multiline blocks which replace newlines with spaces (folded), and a single newline at the end (clip).>-
is same as above, but no newline at the end (strip)>+
will keep all newlines at the end (keep)|
symbol is for multiline blocks where it keeps newlines (literal).>-
is for multiline blocks with a line break at the endThe cleaner way of doing this is with Deployment variables defined in Settings. Based on the deployment, the variable RELEASE_CHANNEL
will have different values. Make sure you also specify deployment
in your step
for RELEASE_CHANNEL
to change accordingly.
1- expo publish --non-interactive --clear --release-channel ${RELEASE_CHANNEL} # make sure your have Deployment variables set
The hacky way of doing this is to run if
statements to check the branch and setting the --release-channel
values accordingly
1# Publish to Expo server (change release channels based on branches)
2- if [[ ${BITBUCKET_BRANCH} = develop ]]; then expo publish --clear --release-channel develop; fi
3- if [[ ${BITBUCKET_BRANCH} = staging ]]; then expo publish --clear --release-channel staging; fi
4- if [[ ${BITBUCKET_BRANCH} = master ]]; then expo publish --clear --release-channel production; fi
1# Build standalone app
2- expo export --public-url ${EXPO_PUBLIC_URL} # will publish the app to dist/ folder, which is being served by Netlify
3- expo build:ios --public-url ${EXPO_PUBLIC_URL}/index.json # build iOS app
4- expo build:android --public-url ${EXPO_PUBLIC_URL}/android-index.json # build Andorid app
1EXPO_ANDROID_KEYSTORE_PASSWORD="${EXPO_ANDROID_KEYSTORE_PASSWORD}" \
2EXPO_ANDROID_KEY_PASSWORD="${EXPO_ANDROID_KEY_PASSWORD}"
3
4turtle build:android \
5 --type "app-bundle" \
6 --build-dir "~/expo-apps" \
7 --mode "release" \
8 --release-channel "production"
9 --keystore-path /media/aamnah/Files/Sites/fppreactnative/fppreactnative.jks \
10 --keystore-alias ${EXPO_ANDROID_KEYSTORE_ALIAS} \
11 --public-url "${EXPO_PUBLIC_URL}/android-index.json" \
1turtle build:android --help
Usage: build:android|ba [options] [project-dir]
Build a standalone APK or App Bundle for your project, either signed and ready for submission to the Google Play Store or in debug mode.
Options:
-u --username <username> username (you can also set EXPO_USERNAME env variable)
-p --password <password> password (you can also set EXPO_PASSWORD env variable)
-d --build-dir <build-dir> directory for build artifact (default: `~/expo-apps`)
-o --output <output-file-path> output file path
--public-url <url> the URL of an externally hosted manifest (for self-hosted apps), only HTTPS URLs are supported!
--release-channel <channel-name> pull from specified release channel (default: default)
-c --config <config-file> specify a path to app.json
--keystore-path <app.jks> path to your Keystore (please provide Keystore password and Key password as EXPO_ANDROID_KEYSTORE_PASSWORD and EXPO_ANDROID_KEY_PASSWORD env variables)
--keystore-alias <alias> keystore Alias
-t --type <build> type of build: app-bundle|apk (default: "app-bundle")
-m --mode <build> type of build: debug|release (default: "release")
-h, --help output usage information
You can safely git ignore .turtle
You should stop auto-publishing to save on build minutes, which you get 300 of on the free plan. The deploys will be triggered from the pipeline
create a ‘user’ api token (as opposed to an ‘app’ api token)
1appcenter tokens create -d 'Bitbucket Pipelines'
1ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
2API Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3Description: Bitbucket Pipelines
4Created at: 2020-07-01T12:28:27.000Z
1- step:
2 name: Send the app to App Center
3 script:
4 - npm install appcenter-cli –g
5 - appcenter login --token ${APPCENTER_TOKEN}
6 - appcenter distribute release -f myapp.apk -r "My First Release"
ERROR: Failed to build standalone app
err: Error: Couldn't find app.json.
Turtle only works with app.json
and not app.config.js
or app.config.ts
. Changed back to app.json
and it worked.