IOS Bitbucket Pipelines CI/CD Pipeline with React Native

Here I will show you how to create a Continuous Integration and Continuous Deployment (CI/CD) pipeline with Bitbucket Pipelines for a React Native application, but it should work with anything with a few modifications.

tl;dr (too long, too lazy to read)

Here is the source code, go and figure it out!

https://github.com/AttilaBerczik/ios-bitbucket-cicd-pipeline

A list of things we will do

  1. Connect to a remote Mac server with an SSH connection
  2. Build the app on the remote Mac
  3. Sign the app
  4. Upload it to App Store Connect, making it available to Internal testers

List of environment variables

Environment variables are things that you don't want anyone to know, typically passwords. For example if someone gets access to your repository, they still don't know all your credentials and access everything.

NameDescription
macPasswordThe password of the Mac server you use to build your application
appStoreAccountPasswordThe password of the Apple account, from which you would like to upload your application
appStoreUsernameThe Apple ID of the Apple account, from which you would like to upload your application

To add an environment variable, in the Bitbucket repository go to Repository settings, then scroll down to Repository variables. Type in the name and the password, keep the Secured clicked.

[object Object]

Mac server

Because of Apple's policy you can't just simply build an IOS application anywhere, you must build it on a Mac machine.

How can you get access to a Mac server, that's always available, yet you only have to pay for it when you use it? I rented a server from MacinCloud.

We will connect to the remote server thorough an SSH connection.

I bought a Pay-As-You-Go Managed Server Plan on a Catalina machine. Be sure to buy the Enable Remote Build Port (SSH) from the Addons. This addon will be needed, when we try to connect from the Bitbucket Pipelines to the server.

[object Object]

Then you will get a zip, inside of that there will be a bunch of .rdp (Remote Desktop Connection) files. Click on one of them and start open the Mac with the credentials that you got from the email.

  1. Open the Apple menu in the upper left corner of the screen, and select "System Preferences...".
  2. Under "Internet & Wireless", select "Sharing".
[object Object]
  1. In the left column of services, enable "Remote Login".
  2. Under Remote Login On you will find a text "To login to this computer remotely, type ..." Copy the IP address and your username and save it somewhere, we will need it in the next section.
[object Object]

Create the SSH connection

If you haven't got Bitbucket Pipelines enabled, enable it now, create a file called bitbucket-pipelines.yml in your repository, and paste this code there.

We use a pipe from Atlassian called ssh-run, to make connecting easier. This code will connect to the SSH server that you specify with the IP address, try to login with the user, on a successful connection it will run the script that we specified in there and create 3 environment variables.

If you run this now, it will fail, because no authentication happens. SSH connections work with something called private and public keys. Basically here we generate a public-private key pair inside our Bitbucket Pipeline, and the private key will stay there. We put the public key on the remote server, this acts as a password field, which can be only authenticated with the private key. No one else will be able to login into our server, because just one private key can unlock this public key. So how do we do this?

  1. Go to the Bitbucket repository
  2. Click on Repository settings
  3. Scroll down to Pipelines section and open SSH keys
  4. Click Generate keys Now you generated a public and a private key, the private key will stay in here
  5. Copy the public key
[object Object]

Now we need to open the Mac server and paste this public key there, so our Pipeline will be able to authenticate with it.

  1. Open the remote Mac
  2. Open the Terminal
  3. Go to the .ssh folder cd .ssh
  4. Edit the authorized_keys file vim authorized_keys
[object Object]
  1. Go to insert mode with i
  2. Paste your public key
  3. Press ESC and enter :wq to save your file and close the vim editor
[object Object]

Now if we done everything correctly this should work, but let's do one more thing to make establishing the connection faster.

Pipelines provides a way for you to store, and inspect, the fingerprint of a remote host, along with the host address. This allows you to visually verify that the public key presented by a remote host actually matches the identity of that host, to help you detect spoofing and man-in-the-middle attacks. It also means that future communications with that host can be automatically verified.

  1. Let's go into SSH keys inside Repository settings again
  2. Inside Host address, under Known hosts enter the IP address of your remote server
  3. Click Fetch
  4. If all went well, you will get a fingerprint
  5. Click on Add host

Now our Bitbucket Pipeline will be able to connect to the remote server 🎉

Get certificates from Apple

First we need to create a Distribution Certificate inside Xcode on the remote Mac

Distribution Certificate

  1. Open Xcode on the remote Mac
  2. Click the Xcode sign
  3. Open Preferences...
  4. Go to Accounts
  5. Open Manage Certificates...
  6. With the + button create an Apple Distribution certificate
  7. Click on Done

Then we need to get two certificates, an App ID and a Provisioning Profile.

App ID

  1. Go to https://developer.apple.com/account
  2. Click on Certificates, Identifiers & Profiles
  3. Go to Identifiers
  4. Create a new identifier with the + button
  5. Then should be App IDs and App
  6. Type in a Description, select the Capabilities you need, and type in a Bundle ID
  7. Click Continue and Register
  8. Copy the App ID Prefix (Team ID) and the newly created Bundle ID, we will need it later

Change Bundle ID in the project.pbxproj file When you created your React Native application, you typed in a name. From that name came the default Bundle ID for your entire application, like this: com.your-name. For me this Bundle ID was reserved, so I had to choose another one. If you also had to change it, I will show you how to change the default Bundle ID in your React Native project.

  1. Go to your home repository, then into ios => dst.xcodeproj and open the project.pbxproj file.

  2. Search for PRODUCT_BUNDLE_IDENTIFIER inside of the file, you will find four hits

  3. Change the text from "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)" to "your Bundle ID"

    For example in my case: PRODUCT_BUNDLE_IDENTIFIER = "com.test-dst";

Provisioning Profile

  1. Go to https://developer.apple.com/account
  2. Click on Certificates, Identifiers & Profiles
  3. Go to Profiles
  4. Create a new profile with the + button
  5. Select the App Store option inside Distribution
  6. Select the App ID you created in the step above
  7. In the Select Certificates open the newest, that you created in the first step
  8. Type in a Provisioning Profile name, don't include spaces or any stupid things
  9. Click on Generate
  10. Copy the newly created Provisioning Profile name, we will need it later

Test build

I would encourage you to login to your remote Mac, clone your repository and try to build the application, archive it with Product -> Archive inside XCode and then distribute it to

  1. Login your remote Mac
  2. Clone your repository
  3. Install dependencies yarn/npm and pods
  4. Open the .workspace file inside ios
  5. Inside Product click Archive If it doesn't work then inside Product hover over Destination and select Any iOS Device (arm64), now Archive should be clickable This will take a while
  6. Then click on the Distribute App, App Store Connect, Upload, tick the two options, and select Manually manage signing
[object Object]
  1. Select the Distribution Certificate that we created in the section above
  2. Select Download Profile..., and select the Provisioning Profile that we created in the section above
  3. If everything was successful you will be asked whether to Upload it, confirm this
  4. Now when you sign in to App Store Connect, inside My Apps you should see your app, and when you click on it, inside TestFlight you will see a build https://appstoreconnect.apple.com/

We needed to do this, so we can figure out most of the errors here, when it's easy to deal with them. I would also recommend you to save the changes in your git repository with git push origin

Archive the application

Now we will understand the macBuildScript.sh file. This is a shell script file, which will be run in the Terminal after we made the SSH connection. This will install dependencies, build the application, sign it and upload it.

Here is all the code that you will need in the macBuildScript.sh file inside the ios-build folder, perhaps I wouldn't copy it all there, because it will cause bugs, and go one by one, but it's up to you.

1-8: First create a new build directory inside the home folder. Then paste the name of your git repository there, and clone it.

git clone AttilaBerczik/ios-bitbucket-cicd-pipeline

Now the remote Mac also has access to the files, let's go inside the folder.

10-17: We would like to download the dependencies, but first we need to create some environment variables. We need to help the script find the place of the node installation.

export PATH="/Users/user111111/.nvm/versions/node/v14.17.6/bin/:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/usr/local/munki:/Applications/Xamarin Workbooks.app/Contents/SharedSupport/path-bin"

If anyone knows a shorter code for this, let me know, I would love to keep this tutorial up-to-date and short.

Then create this language variable too, we need it for Cocoapods

export LANG=en_US.UTF-8

Then install dependencies with yarn/npm, then update and install pod dependencies.

We have to unlock Keychain, because we signed into Xcode and Keychain unlocks all the signed in applications.

security unlock-keychain -p $macPassword /Users/user111111/Library/Keychains/login.keychain-db

This command signs in, with the environment variable macPassword that we created in the Create the SSH connection section.

Now let's archive the application. There are a bunch of things you have fill in:

  • Inside the ios folder, the name of the .xcworkspace file

  • Usually the same as the name before If not the inside Xcode, go to Product, Scheme, and copy the name from there

  • Fill in the development team ID that you copied in Get certificates from Apple - App ID

    xcodebuild -workspace {your workspace}.xcworkspace -scheme {your scheme} -configuration Release clean archive -archivePath ../builds/{your workspace}.xcarchive -allowProvisioningUpdates DEVELOPMENT_TEAM={yourdevteamid}
    

We finished the archive phase of the process, now it would be a great time for you to test your application until this point and solve the bugs.

Export .ipa file

In this step we will sign the application, signing means that we prove that we created the application, and by the end we will have an .ipa file. Before we can dive into the xcodebuild command, we have to create a new file called exportOptions.plist. These are basically the export settings.

Copy this and create a file named exportOptions.plist in the ios-build folder. There are two ways to sign an application. Automatically or manually. Automatically means that you just sign in to Xcode, then Apple takes care of the signing. This would be better, but unfortunately I couldn't get this to work, so we sign manually, meaning that we created a Provisioning Profile, now we will download it on our machine and type it's name into this exportOptions file. Before we do that, you should replace the your id text under teamID with your team ID.

  1. Go to cd ~/Library/MobileDevice/Provisioning\ Profiles
  2. List all files ls
  3. If you have only one file with the .mobileprovision extension copy the name of this file without the extension It will look something like this: c2e97369-f701-4d8f-9d98-f278f65575b2
  4. Copy the name to the exportOptions.plist file under the provisioningProfiles
  5. If you have more than one file, delete all the files with the *rm ~/Library/MobileDevice/Provisioning\ Profiles/** command
  6. Open Xcode, Window and Organizer
  7. Then click on the Distribute App, App Store Connect, Upload, tick the two options, and select Manually manage signing
  8. Select the Distribution Certificate that we created in the Get certificates from Apple section
  9. Select Download Profile..., and select the Provisioning Profile that we created in the Get certificates from Apple section above
  10. Now do steps 3 and 4

Now let's have a look at this beautiful command. It creates a signed .ipa file from the .xcarchive we created in the section above.

xcodebuild -exportArchive -archivePath ../builds/{your workspace}.xcarchive PROVISIONING_PROFILE_SPECIFIER="your-profile" -exportOptionsPlist ../ios-build/exportOptions.plist -exportPath ../builds/ -UseModernBuildSystem=NO CODE_SIGN_STYLE="Manual" CODE_SIGN_IDENTITY="Apple Distribution: your code sign identity"

Things that you have to insert:

  • The name of your workspace file

  • The name of your Provisioning Profile

  • The CODE_SIGN_IDENTITY should be something like this:

    "Apple Distribution: BESK Kft (CBCPR8568U)"

    -   With the name of your organization
    -   Your Team ID
    

Now it would be a good time again to test the process up to this point, because if this succeeds, it's smooth sailing from now on. ⛵

Publish the app for testers

We have one command, that looks for the .ipa file, that we created above, then uploads the file with the environment variables that we created in the beginning.

xcrun altool --upload-app --file /Users/user111111/build/{the name of your git repo}/builds/{your workspace}.ipa --username $appStoreUsername --password $appStoreAccountPassword

You need to fill in the name of your repository and your workspace file as always.

Now when you run all of this, and go to App Store Connect, select your app, and go to TestFlight you should see your newly uploaded app.

At this point I still saw a Missing Compliance message.

[object Object]

I didn't want to click No after every upload, and I think you don't want that either, so let's solve this.

  1. Go your remote Mac
  2. Open your project in Xcode
  3. Open the Info.plist file inside your project folder
  4. Create new Boolean value with the name of App Uses Non-Exempt Encryption, in my case I selected the NO value, but it depends on your application
[object Object]

You can add new testers to the application in App Store Connect, go to TestFlight and create a new group of testers under Internal Testing.

With this we have finished a complete IOS CI/CD Pipeline, if have any questions or suggestions please write to me, the Contact page on this site can help find me. I would love to correct this tutorial if something is not easy to understand or can be done better.

Happy coding!

Cheers,

Attila Berczik