Software Engineering | 23 Dec 2022 | 24 min
Welcome back to the CI/CD realm! I’m not exaggerating when I say that GitHub Actions makes setting up React Native CI/CD a cakewalk. I hope you had a chance to read Part 1 of this blog series! If not, do check it out here; it will help you to understand the initial setup.
In this blog, I mainly focus on steps related to iOS like Ruby setup, pod install, build, and upload. So, let’s get started!
First, let’s delve into setting up Ruby.
- name: ruby setup uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 bundler-cache: true env: ImageOS: macos1015
If the virtual environment is extremely similar to the ones used by GitHub runners, this action will work with self-hosted runners. Notably:
Now, allow me to explain how you can restore Pods cache using actions/cache@v3.
- name: Restore Pods cache uses: actions/cache@v3 with: path: | ios/Pods ~/Library/Caches/CocoaPods ~/.cocoapods key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }} restore-keys: | ${{ runner.os }}-pods-
Now it’s time to install the Pods!
- name: Install Pods run: pod install --repo-update && cd ..
Here’s how you can bump the iOS version.
- name: bump version uses: abhishek219tiwari/[email protected] with: marketingVersion: ${{ github.event.inputs.version }} project-path: ios/Nitor/Config/Info-plist/Nitor-Dev-info.plist
It’s not necessary to use this action, this is under development if agvTool is working fine for you. You can go with other options as well, such as the iOS bump version.
Let’s look at how you can build an iOS app.
- name: Build IOS App NitorPro / staging if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: NitorPro - Staging
Now that you have the app in place, read on to know how you can configure your iOS project on Xcode.
We utilize yukiarrr/[email protected] to create the app and export the IPA for TestFlight submission.
The following variables must be added as GitHub Secrets for the build action:
Use the following script to build base64 encoded format after downloading it from your Apple developer portal.
openssl base64 < YOUR_Profile.mobileprovision | tr -d ‘\n’ | tee my-profile.base64.txt
Once the certificate has been installed on your Mac, launch the Keychain Access app, choose ‘My Certificates’ from the menu, and then find the downloaded certificate.
To view the matching private key, expand the certificate. Next, choose ‘Export 2 items…’ from the context menu when you right-click on the certificate and private key.
Choose a location on your computer to save the file as a.p12 and give it a secure password (IOS CERTIFICATE PASSWORD).
For the .p12 file, create a base64 using
openssl base64 < cert.p12 | tr -d ‘\n’ | tee cert.base64.txt
The final step is to use upload-TestFlight-build to upload the created IPA to TestFlight.
Note: As we are using a personal Mac system as self-hosted, we need to download Apple Worldwide Developer Relations Certificate Authority.
Now it’s time to see how you can upload your app to TestFlight.
#Upload app to TestFlight For NitorPro / staging - name: Upload app to TestFlight NitorPro / staging if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}
If you get stuck in any of the steps, you can get some help from here, I have put my .yml.
name: iOS Build Deployment on: workflow_dispatch: inputs: target: description: "Target" required: true default: "Nitor" type: choice options: - Nitor - NitorPro flavor: description: "Environment" required: true default: "staging" type: choice options: - staging - development - production version: description: new app version x.x.x. required: true jobs: ios-build: name: IOS Production Build runs-on: self-hosted defaults: run: working-directory: ios steps: - name: Check out Git repository uses: actions/checkout@v2 with: persist-credentials: false # make it false if you are going to clone private repositories in yarn - name: Set up our node configuration uses: actions/setup-node@v1 with: node-version: 16.x - name: Decode .env.Nitor.staging # decode .env.Nitor.staging and generate .env.Nitor.staging file if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging'}} id: decode_Nitor_staging uses: timheuer/base64-to-file@v1 with: fileName: ".env" encodedString: ${{ secrets.NITOR_STAGING}} - name: Decode .env.Nitor.development # decode .env.Nitor.development and generate .env.Nitor.development file if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development'}} id: decode_Nitor_development uses: timheuer/base64-to-file@v1 with: fileName: ".env" encodedString: ${{ secrets.NITOR_DEVELOPMENT}} - name: Decode .env.Nitor.production # decode .env.Nitor.production and generate .env.Nitor.production file if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production'}} id: decode_Nitor_production uses: timheuer/base64-to-file@v1 with: fileName: ".env" encodedString: ${{ secrets.NITOR_PRODUCTION}} - name: Decode .env.NitorPro.staging # decode .env.NitorPro.stagingand generate .env.NitorPro.staging file if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging'}} id: decode_NitorPro_staging uses: timheuer/base64-to-file@v1 with: fileName: ".env" encodedString: ${{ secrets.NITORPRO_STAGING}} - name: Decode .env.NitorPro.development # decode .env.NitorPro.development and generate .env.NitorPro.development file if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development'}} id: decode_NitorPro_development uses: timheuer/base64-to-file@v1 with: fileName: ".env" encodedString: ${{ secrets.NITORPRO_DEVELOPMENT}} - name: Decode .env.NitorPro.production # decode .env.NitorPro.production and generate .env.NitorPro.production file if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production'}} id: decode_NitorPro_production uses: timheuer/base64-to-file@v1 with: fileName: ".env" encodedString: ${{ secrets.NITORPRO_PRODUCTION}} - name: "move Nitor_staging" if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging'}} run: | mv ${{steps.decode_Nitor_staging.outputs.filePath}} ./ pwd cat .env - name: "move Nitor_development" if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development'}} run: | mv ${{steps.decode_Nitor_development.outputs.filePath}} ./ pwd cat .env - name: "move Nitor_production" if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production'}} run: | mv ${{steps.decode_Nitor_production.outputs.filePath}} ./ pwd cat .env - name: "move NitorPro_staging" if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging'}} run: | mv ${{steps.decode_NitorPro_staging.outputs.filePath}} ./ pwd cat .env - name: "move NitorPro_development" if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development'}} run: | mv ${{steps.decode_NitorPro_development.outputs.filePath}} ./ pwd cat .env - name: "move NitorPro_production" if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production'}} run: | mv ${{steps.decode_NitorPro_production.outputs.filePath}} ./ pwd cat .env - name: git config with PAT_GITHUB run: git config --global url.https://${{ secrets.PAT_GITHUB }}@github.com/.insteadOf https://github.com - run: | echo "github.event.inputs.version: ${{ github.event.inputs.version }}" - name : ruby setup uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 bundler-cache: true env: ImageOS: macos1015 LANG: en_US.UTF-8 - name: bump version uses: abhishek219tiwari/[email protected] with: marketingVersion: ${{ github.event.inputs.version }} project-path: ios/Nitor/Config/Info-plist/Nitor-Dev-info.plist - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - name: Restore node_modules from cache uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: | steps.cache-yarn-cache.outputs.cache-hit != 'true' || steps.cache-node-modules.outputs.cache-hit != 'true' run: | yarn install --network-concurrency 1 - name: Restore Pods cache uses: actions/cache@v3 with: path: | ios/Pods ~/Library/Caches/CocoaPods ~/.cocoapods key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }} restore-keys: | ${{ runner.os }}-pods- - name: Install Pods run: pod install --repo-update && cd .. - name: check .env run: cat .env # Build IOS App Nitor / staging - name: Build IOS App Nitor / staging if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: Nitor - Staging # Build IOS App Nitor / development - name: Build IOS App Nitor / development if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: Nitor - Dev # Build IOS App Nitor / production - name: Build IOS App Nitor / production if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: Nitor # Build IOS App NitorPro / staging - name: Build IOS App NitorPro / staging if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: NitorPro - Staging # Build IOS App NitorPro / development - name: Build IOS App NitorPro / development if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: NitorPro - Dev # Build IOS App NitorPro / production - name: Build IOS App NitorPro / production if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production' }} uses: yukiarrr/[email protected] with: project-path: ios/Nitor.xcodeproj p12-base64: ${{ secrets.IOS_P12_BASE64 }} mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }} code-signing-identity: "iPhone Distribution" team-id: ${{ secrets.IOS_TEAM_ID }} certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} workspace-path: ios/Nitor.xcworkspace scheme: NitorPro #Upload app to TestFlight For Nitor / staging - name: Upload app to TestFlight Nitor / staging if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} #Upload app to TestFlight For Nitor / development - name: Upload app to TestFlight Nitor / development if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} #Upload app to TestFlight For Nitor / production - name: Upload app to TestFlight Nitor / production if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} #Upload app to TestFlight For NitorPro / staging - name: Upload app to TestFlight NitorPro / staging if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} #Upload app to TestFlight For NitorPro / development - name: Upload app to TestFlight NitorPro / development if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} #Upload app to TestFlight For NitorPro / production - name: Upload app to TestFlight NitorPro / production if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production' }} uses: apple-actions/upload-testflight-build@v1 with: app-path: "output.ipa" issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}
That’s all for today’s blog! You can save this blog as a pocket guide to the iOS setup of CI/CD with GitHub Actions! Stay tuned for Part 3 in this series which will walk you through setting up CI/CD in the world of Android! Meanwhile, write to us with your thoughts about Part 2 and visit us at Nitor Infotech to learn about what we do.
we'll keep you in the loop with everything that's trending in the tech world.