iOS and macOS applications
You can build and test iOS and macOS applications using a macOS platform on Harness Cloud, a self-hosted macOS VM, or a local runner build infrastructure.
The examples in this guide use Xcode. You can also use Fastlane to build and test your iOS and macOS apps.
This guide assumes you've created a Harness CI pipeline. For more information about creating pipelines, go to:
If you don't have a Harness account yet, you can create one for free at app.harness.io.
Specify architecture
- Harness Cloud
- Self-hosted
To use M1 machines with Harness Cloud, use the Arm64
architecture.
stages:
- stage:
name: build
identifier: build
type: CI
spec:
cloneCodebase: true
platform:
os: MacOS ## selects macOS operating system
arch: Arm64 ## selects M1 architecture
If you need to use Intel-based architecture, Rosetta is pre-installed on Harness Cloud's M1 machines. If you need to use it, add the prefix arch -x86_64
to commands in your scripts. Keep in mind that running apps through Rosetta can impact performance. Use native Apple Silicon apps whenever possible to ensure optimal performance.
To configure a self-hosted macOS build infrastructure, go to Set up a macOS VM build infrastructure with Anka Registry or Set up a local runner build infrastructure.
This example uses a VM build infrastructure:
stages:
- stage:
name: build
identifier: build
description: ""
type: CI
spec:
cloneCodebase: true
infrastructure:
type: VM
spec:
type: Pool
spec:
poolName: YOUR_VM_POOL_NAME
os: MacOS
If you need to use Intel-based architecture and Rosetta is not already installed on your build infrastructure machines, you can use a Run step to install this dependency. Keep in mind that running apps through Rosetta can impact performance. Use native Apple Silicon apps whenever possible to ensure optimal performance.
Install dependencies
Use Run steps to install dependencies in the build environment.
- Harness Cloud
- Self-hosted
Homebrew and Xcode are already installed on Harness Cloud macOS machines. For more information about preinstalled tools and libraries, go to the Harness Cloud image specifications.
- step:
type: Run
identifier: dependencies_ruby_gems
name: dependencies-ruby-gems
spec:
shell: Sh
command: |-
brew install fastlane
You can add package dependencies in your Xcode project and then run Xcode commands in Run steps to interact with your project's dependencies.
- step:
type: Run
identifier: dependencies
name: Dependencies
spec:
shell: Sh
command: |-
xcodebuild -resolvePackageDependencies
Due to the long install time, make sure Xcode is pre-installed on your build infrastructure machines. If Homebrew is not already installed, use Run steps to install it and any other dependencies.
- step:
type: Run
identifier: dependencies_install_brew
name: dependencies-install-brew
spec:
shell: Sh
command: |-
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- step:
type: Run
identifier: dependencies_ruby_gems
name: dependencies-ruby-gems
spec:
shell: Sh
command: |-
brew install fastlane
You can add package dependencies in your Xcode project and then run Xcode commands in Run steps to interact with your project's dependencies.
- step:
type: Run
identifier: dependencies
name: Dependencies
spec:
shell: Sh
command: |-
xcodebuild -resolvePackageDependencies
Cache dependencies
Add caching to your stage.
- Harness Cloud
- Self-hosted
Use Cache Intelligence by adding caching
to your stage.spec
.
- stage:
spec:
caching:
enabled: true
paths:
- /Users/anka/Library/Developer/Xcode/DerivedData
sharedPaths:
- /Users/anka/Library/Developer/Xcode/DerivedData
With self-hosted build infrastructures, you can:
Here's an example of a pipeline with Save Cache to S3 and Restore Cache from S3 steps. It also includes a Run step that creates the .ipa
archive with xcodebuild archive
and xcodebuild --exportArchive
.
steps:
- step:
type: RestoreCacheS3
name: Restore Cache From S3
identifier: Restore_Cache_From_S3
spec:
connectorRef: AWS_Connector
region: us-east-1
bucket: your-s3-bucket
key: cache-{{ checksum "cache.ipa" }}
archiveFormat: Tar
- step:
type: Run
...
- step:
type: Run
...
- step:
type: Run
identifier: create_cache
name: create cache
spec:
shell: Sh
command: |-
xcodebuild archive
xcodebuild -exportArchive
- step:
type: SaveCacheS3
name: Save Cache to S3
identifier: Save_Cache_to_S3
spec:
connectorRef: AWS_Connector
region: us-east-1
bucket: your-s3-bucket
key: cache-{{ checksum "cache.ipa" }}
sourcePaths:
- "/Users/anka/Library/Developer/Xcode/DerivedData"
archiveFormat: Tar
Build and run tests
Add Run steps to run tests in Harness CI.
- Harness Cloud
- Self-hosted
- step:
type: Run
name: Test
identifier: test
spec:
shell: Sh
command: |-
xcodebuild
xcodebuild test -scheme SampleApp
If you want to view test results in Harness, make sure your test commands produce reports in JUnit XML format and that your steps include the reports
specification. The following example uses xcpretty to produce reports in JUnit XML format.
- step:
type: Run
name: Test
identifier: test
spec:
shell: Sh
command: |-
brew install xcpretty
- step:
type: Run
name: Test
identifier: test
spec:
shell: Sh
command: |-
xcodebuild
xcodebuild test -scheme SampleApp | xcpretty -r junit
reports:
type: JUnit
spec:
paths:
- "build/reports/junit.xml"
- step:
type: Run
name: Test
identifier: test
spec:
shell: Sh
command: |-
xcodebuild
xcodebuild test -scheme SampleApp
If you want to view test results in Harness, make sure your test commands produce reports in JUnit XML format and that your steps include the reports
specification. The following example uses xcpretty to produce reports in JUnit XML format.
- step:
type: Run
name: Test
identifier: test
spec:
shell: Sh
command: |-
brew install xcpretty
- step:
type: Run
name: Test
identifier: test
spec:
shell: Sh
command: |-
xcodebuild
xcodebuild test -scheme SampleApp | xcpretty -r junit
reports:
type: JUnit
spec:
paths:
- "build/reports/junit.xml"
Specify version
- Harness Cloud
- Self-hosted
Xcode is pre-installed on Harness Cloud machines. For details about all available tools and versions, go to Platforms and image specifications.
Use xcode-select
in a Run step to switch between pre-installed versions of Xcode.
- step:
type: Run
name: set_xcode_version
identifier: set_xcode_version
spec:
shell: Sh
command: |-
sudo xcode-select -switch /Applications/Xcode_13.4.1.app
xcodebuild -version
If your build infrastructure machines have multiple versions of Xcode installed, you can use xcode-select
in a Run step to switch versions.
- step:
type: Run
name: set_xcode_version
identifier: set_xcode_version
spec:
shell: Sh
command: |-
sudo xcode-select -switch /Applications/Xcode_13.4.1.app
xcodebuild -version
Deploy to the App Store
The following examples use Fastlane in a Continuous Integration setup to deploy an app to the Apple App Store. The environment variables in these examples use secrets and expressions to store and recall sensitive values, such as FASTLANE_PASSWORD=<+secrets.getValue('fastlanepassword')>
.
To learn more about app distribution, go to the Apple Developer documentation on Distribution.
- Harness Cloud
- Self-hosted
- step:
type: Run
name: Fastlane Build
identifier: Fastlane_Build
spec:
shell: Sh
command: |-
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export APP_ID="osx.hello-harness"
export APP_STORE_CONNECT_KEY_ID="FW...CV3"
export APP_STORE_CONNECT_ISSUER_ID="80...e54"
export APP_STORE_CONNECT_KEY_FILEPATH="/tmp/build_certificate.p12"
export FASTLANE_USER=sample@mail.com
export FASTLANE_PASSWORD=<+secrets.getValue('fastlanepassword')>
export BUILD_CERTIFICATE_BASE64=<+secrets.getValue('BUILD_CERTIFICATE_BASE64')>
export BUILD_PROVISION_PROFILE_BASE64=<+secrets.getValue('BUILD_PROVISION_PROFILE_BASE64')>
export P12_PASSWORD=<+secrets.getValue('certpassword')>
export KEYCHAIN_PASSWORD=admin
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=<+secrets.getValue('fastlaneapppassword')>
export FASTLANE_SESSION='-..._at: *1\n'
export APP_STORE_CONNECT_KEY_BASE64=<+secrets.getValue('appstoreapikey')>
sudo xcode-select -switch /Applications/Xcode_14.1.0.app
cd hello-harness
CERTIFICATE_PATH=/tmp/build_certificate.p12
PP_PATH=/tmp/profile.mobileprovision
KEYCHAIN_PATH=/tmp/app-signing.keychain-db
KEY_FILE_PATH="/tmp/app_store_connect_key.p8"
echo "$BUILD_CERTIFICATE_BASE64" >> ce
base64 -i ce --decode > $CERTIFICATE_PATH
echo "$BUILD_PROVISION_PROFILE_BASE64" >> prof
base64 -i prof --decode > $PP_PATH
echo "$APP_STORE_CONNECT_KEY_BASE64" >> key_base64
base64 -i key_base64 --decode > $KEY_FILE_PATH
export APP_STORE_CONNECT_KEY_FILEPATH="$KEY_FILE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
gem install bundler
bundle install
bundle exec fastlane beta
echo $ABC
envVariables:
ABC: samples
- step:
type: Run
name: Run_2
identifier: Run_2
spec:
shell: Sh
command: echo $ABC
- step:
type: Run
name: Fastlane Build
identifier: Fastlane_Build
spec:
shell: Sh
command: |-
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export APP_ID="osx.hello-harness"
export APP_STORE_CONNECT_KEY_ID="FW...CV3"
export APP_STORE_CONNECT_ISSUER_ID="801...e54"
export APP_STORE_CONNECT_KEY_FILEPATH="/tmp/build_certificate.p12"
export FASTLANE_USER=sample@mail.com
export FASTLANE_PASSWORD=<+secrets.getValue('fastlanepassword')>
export BUILD_CERTIFICATE_BASE64=<+secrets.getValue('BUILD_CERTIFICATE_BASE64')>
export BUILD_PROVISION_PROFILE_BASE64=<+secrets.getValue('BUILD_PROVISION_PROFILE_BASE64')>
export P12_PASSWORD=<+secrets.getValue('certpassword')>
export KEYCHAIN_PASSWORD=admin
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=<+secrets.getValue('fastlaneapppassword')>
export FASTLANE_SESSION='-...*1\n'
export APP_STORE_CONNECT_KEY_BASE64=<+secrets.getValue('appstoreapikey')>
sudo xcode-select -switch /Applications/Xcode_14.1.0.app
cd hello-harness
CERTIFICATE_PATH=/tmp/build_certificate.p12
PP_PATH=/tmp/profile.mobileprovision
KEYCHAIN_PATH=/tmp/app-signing.keychain-db
KEY_FILE_PATH="/tmp/app_store_connect_key.p8"
echo "$BUILD_CERTIFICATE_BASE64" >> ce
base64 -i ce --decode > $CERTIFICATE_PATH
echo "$BUILD_PROVISION_PROFILE_BASE64" >> prof
base64 -i prof --decode > $PP_PATH
echo "$APP_STORE_CONNECT_KEY_BASE64" >> key_base64
base64 -i key_base64 --decode > $KEY_FILE_PATH
export APP_STORE_CONNECT_KEY_FILEPATH="$KEY_FILE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
gem install bundler
bundle install
bundle exec fastlane beta
echo $ABC
envVariables:
ABC: samples
- step:
type: Run
name: Run_2
identifier: Run_2
spec:
shell: Sh
command: echo $ABC
Full pipeline examples
The following pipeline examples install dependencies, cache dependencies, and build and test an Xcode project.
- Harness Cloud
- Self-hosted
This pipeline uses Harness Cloud build infrastructure and Cache Intelligence.
If you copy this example, replace the placeholder values with appropriate values for your code repo connector, repository name, and other applicable values. Depending on your project and organization, you may also need to replace projectIdentifier
and orgIdentifier
.
pipeline:
name: macostest
identifier: macostest
projectIdentifier: default
orgIdentifier: default
tags: {}
stages:
- stage:
name: build
identifier: build
description: ""
type: CI
spec:
cloneCodebase: true
caching:
enabled: true
paths:
- /Users/anka/Library/Developer/Xcode/DerivedData
sharedPaths:
- /Users/anka/Library/Developer/Xcode/DerivedData
platform:
os: MacOS
arch: Arm64
runtime:
type: Cloud
spec: {}
execution:
steps:
- step:
type: Run
identifier: dependencies
name: dependencies
spec:
shell: Sh
command: xcodebuild -resolvePackageDependencies
- step:
type: Run
name: Run xcode
identifier: Run_xcode
spec:
shell: Sh
command: |-
xcodebuild
xcodebuild test -scheme SampleApp
properties:
ci:
codebase:
connectorRef: YOUR_CODE_REPO_CONNECTOR_ID
repoName: YOUR_REPO_NAME
build: <+input>
This pipeline uses a self-hosted VM build infrastructure and Save and Restore Cache from S3 steps.
If you copy this example, replace the placeholder values with appropriate values for your code repo connector, repository name, and other applicable values. Depending on your project and organization, you may also need to replace projectIdentifier
and orgIdentifier
.
pipeline:
name: macos-test-vm
identifier: macostestvm
projectIdentifier: default
orgIdentifier: default
tags: {}
stages:
- stage:
name: build
identifier: build
description: ""
type: CI
spec:
cloneCodebase: true
execution:
steps:
- step:
type: RestoreCacheS3
name: Restore Cache From S3
identifier: Restore_Cache_From_S3
spec:
connectorRef: YOUR_AWS_CONNECTOR_ID
region: us-east-1 ## Use your S3 bucket's region.
bucket: YOUR_S3_BUCKET
key: cache-{{ checksum "cache.ipa" }}
archiveFormat: Tar
- step:
type: Run
identifier: dependencies
name: dependencies
spec:
shell: Sh
command: xcodebuild -resolvePackageDependencies
- step:
type: Run
name: Run xcode
identifier: Run_xcode
spec:
shell: Sh
command: |-
xcodebuild
xcodebuild test -scheme SampleApp
- step:
type: Run
identifier: create_cache
name: create cache
spec:
shell: Sh
command: |-
xcodebuild archive
xcodebuild -exportArchive
- step:
type: SaveCacheS3
name: Save Cache to S3
identifier: Save_Cache_to_S3
spec:
connectorRef: YOUR_AWS_CONNECTOR_ID
region: us-east-1 ## Use your S3 bucket's region.
bucket: YOUR_S3_BUCKET
key: cache-{{ checksum "cache.ipa" }}
sourcePaths:
- /Users/anka/Library/Developer/Xcode/DerivedData
archiveFormat: Tar
infrastructure:
type: VM
spec:
type: Pool
spec:
poolName: YOUR_VM_POOL_NAME
os: MacOS
properties:
ci:
codebase:
connectorRef: YOUR_CODE_REPO_CONNECTOR_ID
repoName: YOUR_REPO_NAME
build: <+input>
Next steps
Now that you have created a pipeline that builds and tests an iOS/macOS app, you could:
- Create triggers to automatically run your pipeline.
- Add steps to build and upload artifacts.
- Add a step to build and push an image to a Docker registry.
- Explore other ways to optimize and enhance CI pipelines.