Building a ci/cd pipeline for react-native ios app: How to run expo build in gitlab-ci?

Viewed 2951

I am helping set up a gitlab ci pipeline for a react-native application that was developed with expo. Here is my gitlab-ci.yml:

image: node/apline
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - ~/.npm

stages:
  - deploy
  - tag
before_script:
  - echo $CI_BUILD_REF
  - echo $CI_PROJECT_DIR
  - apk add --no-cache bash build-base gcc git python3 curl
  - PATCH=`git log --pretty=oneline | wc -l | sed -e 's/^[[:space:]]*//'`
  - VERSION=`cat VERSION`
  - VERSION=${VERSION%?}
  - TAG="${VERSION}${PATCH}"
  - echo "Build version = ${TAG}"

expo-build:
  stage: deploy
  artifacts:
    paths:
    - ipas/
  script:
    - sed -i "s/0.0.0/${TAG}/g" app.json
    - npm ci --production --cache .npm --prefer-offline
    - npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
    - EXPO_DEBUG=true npx expo build:ios --non-interactive
    - mkdir -p ipas
    - curl "$(npx expo url:ipa --non-interactive)" -o ipas/my-app-$TAG.ipa
  only:
    - master

What I am trying to do is have the app build a new .ipa every time there is a push to master. That way I can upload the .ipa to my mdm to distribute the application.

The problem is that if I every build a different app (I have multiple applications I am trying to do this with), it appears the build step needs to be run locally before it can be run in ci again. What I mean is that the command npx expo build:ios --non-interactive needs intervention in selecting the proper certificates every time I make a separate build utilizing my apple credentials.

Here is the output from a failed build (that had succeed in the pipeline before without code changes):

- Making sure project is set up correctly...
[17:26:33] Checking if there is a build in progress...
[17:26:34] Fetching available credentials
[17:26:38] Unable to validate distribution certificate due to insufficient Apple Credentials
[17:26:38] Unable to determine validity of Push Keys due to insufficient Apple Credentials
[17:26:38] CommandError: Input is required, but Expo CLI is in non-interactive mode.
Required input:
> Push Notifications Key (Key ID: XXX, Team ID: XXX)
>     not used by any apps
>     ✅ Currently valid on Apple's servers. 
>  Would you like to use this Push Key?
   at prompt (/expo-cli@3.17.17/src/prompt.ts:22:11)
   at CreateOrReusePushKey.open (/expo-cli@3.17.17/src/credentials/views/IosPushCredentials.ts:281:31)
   at processTicksAndRejections (internal/process/task_queues.js:97:5)
   at CredentialsManager.run (/expo-cli@3.17.17/src/credentials/route.ts:42:12)
   at runCredentialsManager (/expo-cli@3.17.17/src/credentials/route.ts:13:10)
   at IOSBuilder.produceCredentials (/expo-cli@3.17.17/src/commands/build/ios/IOSBuilder.ts:168:7)
   at IOSBuilder.prepareCredentials (/expo-cli@3.17.17/src/commands/build/ios/IOSBuilder.ts:124:7)
   at IOSBuilder.run (/expo-cli@3.17.17/src/commands/build/ios/IOSBuilder.ts:46:7)
   at IOSBuilder.command (/expo-cli@3.17.17/src/commands/build/BaseBuilder.ts:29:7)
   at Command.<anonymous> (/expo-cli@3.17.17/src/exp.ts:85:7) {
 code: 'NON_INTERACTIVE',
 isCommandError: true
}
[17:26:39] Failed to prepare all credentials. 
The next time you build, we will automatically use the following configuration:
[17:26:39]
[17:26:39] Project Credential Configuration:
[17:26:39]   Experience: @team/app, bundle identifier: com.app.profile
[17:26:39]     Provisioning profile (ID: XXX)
[17:26:39]     Apple Team ID: XXX,  Apple Team Name: ---------
[17:26:39]
[17:26:39]   Distribution Certificate - Certificate ID: XXX
[17:26:39]     Apple Team ID: XXX,  Apple Team Name: A Company, LLC (In-House)
[17:26:39]     used by
     @team/app, (com.app.profile)
[17:26:39] 
[17:26:39] Error
   at CredentialsManager.doQuit [as _quit] (/expo-cli@3.17.17/src/credentials/views/Select.ts:176:9)
   at CredentialsManager.run (/expo-cli@3.17.17/src/credentials/route.ts:49:42)
   at runCredentialsManager (/expo-cli@3.17.17/src/credentials/route.ts:13:10)
   at IOSBuilder.produceCredentials (/expo-cli@3.17.17/src/commands/build/ios/IOSBuilder.ts:168:7)
   at IOSBuilder.prepareCredentials (/expo-cli@3.17.17/src/commands/build/ios/IOSBuilder.ts:124:7)
   at IOSBuilder.run (/expo-cli@3.17.17/src/commands/build/ios/IOSBuilder.ts:46:7)
   at IOSBuilder.command (/expo-cli@3.17.17/src/commands/build/BaseBuilder.ts:29:7)
   at Command.<anonymous> (/expo-cli@3.17.17/src/exp.ts:85:7)

my question is: is this workflow supported by expo/gitlab-ci, and if so what am I doing wrong? I would like this to build reliably so I can develpo a solid ci/cd pipeline for this react-native application. If there is a better process for me to follow for this use case (building and deploying a react-native ios app) I am all ears as well. Anything helps.

2 Answers

After reading the documentation, and talking directly with some kind folks on the expo slack. Here is the conclusion that I finally arrived at to get reliable ci builds for my react-native ios app in gitlab-ci.

Here is the build stage from my gitlab-ci.yml file:

expo-build:
  stage: deploy
  artifacts:
    paths:
    - ipas/
  script:
    - sed -i "s/0.0.0/${TAG}/g" app.json
    - npm ci --prefer-offline
    - echo $P8_KEY > ${HOME}/${CI_PROJECT_NAME}.p8
    - echo $P12_DIST | base64 -d > ${HOME}/${CI_PROJECT_NAME}.p12
    - echo $PROVISIONING_PROFILE | base64 -d > ${HOME}/${CI_PROJECT_NAME}.mobileprovision
    - npx expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
    - npx expo build:ios --team-id $APPLE_TEAM_ID --push-id $PUSH_KEY_ID --dist-p12-path ${HOME}/${CI_PROJECT_NAME}.p12 --push-p8-path ${HOME}/${CI_PROJECT_NAME}.p8 --provisioning-profile-path ${HOME}/${CI_PROJECT_NAME}.mobileprovision --no-publish --non-interactive
    - mkdir -p ipas
    - curl "$(npx expo url:ipa --non-interactive)" -o ipas/${CI_PROJECT_NAME}-${TAG}.ipa
  only:
    - ci-build-test # Build api without pushing to master (protected branch)
    - master

Note that the build command contains all of the following flags:

--team-id 
--push-id
--dist-p12-path 
--push-p8-path
--provisioning-profile-path 
--no-publish 
--non-interactive

What is important to note is all of the environmental variables being used. Notice both my apple developer and expo account username and passwords are used:

  • APPLE_ID
    • my appleid
    • used for interaction with Apple Developer portal
    • needed to log into apple from command line
  • APPLE_TEAM_ID
    • The ID of my team from the Apple Developer portal
    • not explicitly used by ci command but must be in env with this key
  • EXPO_APPLE_PASSWORD
    • application specific password generated with my appleid
    • needed to log into apple from command line
    • not explicitly used by ci command but must be in env with this key
  • EXPO_USERNAME
    • my expo account user name
  • EXPO_PASSWORD
    • password to the above user account

The following values were created after building the application through the standard build prompt expo bi and then obtaining the created/generated credentials with expo fetch:ios:certs. The binary files were then encoded into base64 before being uploaded as strings. They are also exposed as environment variables.

  • P8_KEY
    • app-name_apns_key.p8
    • used directly in CI
  • P12_DIST
    • app-name_dist.p12
    • encoded in base64 before uploading as env variable
  • PROVISIONING_PROFILE
    • app-name.mobileprovision
    • encoded in base64 before uploading as env variable
  • PUSH_KEY_ID
    • output by expo fetch:ios:certs command.
    • originally I let expo generate this when I built the app the first time.
  • EXPO_IOS_DIST_P12_PASSWORD
    • From expo generated signing certificate
    • output by expo fetch:ios:certs command.

I hope this helps anybody having the same problems that I was!

You can pass in your credentials through build flags. For example, to pass in the distribution certificate, you will need to pass in the --dist-p12-path flag (see the docs here)

From your output, it looks like your push key is not associated with the app you are publishing under. Building with the non-interactive flag will only work if your credentials are already set for the app (ie) you've already chosen the push key to associate with your app.

Related