Mobile development with Harness
Harness CI supports mobile app development.
If you don't have a Harness account yet, you can create one for free at app.harness.io.
Mobile app development guides
These guides contain a variety of information about mobile development with Harness CI, including installing dependencies, running tests, and distribution.
Plugins and scripts for mobile development
There are several ways to run scripts and use plugins in Harness CI. You can run Bitrise Steps, GitHub Actions, fastlane scripts, Firebase scripts, Xcode commands, and more.
- Use Bitrise Workflow Steps in Harness CI
- Use GitHub Actions in Harness CI
- Write your own plugins (reusable custom scripts)
- Use a Run step to run any script in Harness CI
- Deploy Android apps with Harness CI
- Deploy iOS apps with Harness CI
Variables and secrets for mobile development
You can use Run steps to run all manner of commands or scripts. If your Run step defines or ingests a variable, make sure you understand how output variables and environment variables work in Run steps. You can also declare variables at the stage level and redefine their values in steps.
Store tokens, passwords, and other sensitive data as secrets and then use expressions to reference secrets in your pipelines. For example, you can use an expression as the value for a variable:
APP_STORE_PASSWORD=<+secrets.getValue("my_app_store_password_secret")>
App distribution with Harness CI
You can run distribution scripts for your preferred mobile development tool's CLI in a Run step.
For more information about the commands to use in your Run steps, refer to the provider's documentation. Here are some examples:
- Firebase: iOS distribution
- Firebase: Android distribution
- Flutter: iOS deployment
- Flutter: Android deployment
- fastlane: iOS beta deployment
- fastlane: Android beta deployment
- Xcode: App distribution
Mobile development YAML examples
Here are some examples of pipelines for mobile development. These pipelines are for example purposes only. They are meant to help you conceptualize how you can organize a Harness pipeline for mobile development.
- Example: Build and upload to JFrog
- Example: Use Fastlane and Swift
- Example: Build, test, deploy in one stage
- Example: Use Bitrise Steps
This example builds an iOS app from an Xcode project and then uploads the artifact to JFrog Artifactory.
pipeline:
identifier: buildiOSApp
name: build-iOS-app
stages:
- stage:
identifier: Build_iOS_App
name: Build iOS App
type: CI
spec:
cloneCodebase: true
infrastructure: ## This examples uses a macOS VM build infrastructure.
type: VM
spec:
type: Pool
spec:
poolName: <+pipeline.variables.build_anka_image>
harnessImageConnectorRef: account.Jfrogartifactory
os: MacOS
execution:
steps:
- step:
identifier: Definevariables
name: Define variables
type: Run
spec:
shell: Sh
command: |-
## Insert script to populate variables required for the rest of the stage. Populated variables are declared in outputVariables so they are available to later steps. Variable values can change in later steps.
envVariables: ## These variables use Harness expressions for their values. Expressions beginning with stage.variables are references to stage variables.
PROVISIONING_PROFILE: <+stage.variables.xcode_provisioningProfile>
CERTPATH_PREFERENCE: <+stage.variables.certpath_preference>
PROFILES_PREFERENCE: <+stage.variables.profiles_preference>
EXPORTOPTIONS_PREFERENCE: <+exportoptions_preference>
SUPPRESS_COCOAPODS: <+stage.variables.suppress_cocoapods>
LOAD_CERTIFICATES: <+stage.variables.load_certificates>
outputVariables:
- name: XCODE_CERT_PATH
- name: XCODE_PROFILES_PATH
- name: XCODE_EXPORTOPTIONS_PATH
- name: XCODE_COCOAPOD_PATH
- name: XCODE_COCOAPOD_ENABLE
- name: XCODE_PROVISIONING_ID
- step:
identifier: Install_Certs_and_Provision_Profiles
name: Install Certs and Provision Profiles
type: Run
spec:
shell: Sh
command: |-
## Insert script to install certs and provision profiles.
envVariables: ## By declaring a variable in envVariables, the step can use that variable value in its command. For example, this step could use the LOAD_CERTIFICATES stage variable.
LOAD_CERTIFICATES: <+stage.variables.load_certificates>
- step:
identifier: Install_Cocoapods
name: Install Cocoapods
type: Run
spec:
shell: Sh
command: |-
## Insert script to install cocoapods.
when: ## This step only runs if all prior steps passed and the XCODE_COCOAPOD_ENABLE variable was set to true in the Definevariables step.
stageStatus: Success
condition: <+stage.spec.execution.steps.Definevariables.output.outputVariables.XCODE_COCOAPOD_ENABLE>
== "true"
- stepGroup: ## These steps are organized in a step group. This group of steps creates an .ipa file from an xcode project or workspace, depending on the value of the XCODE_TYPE variable.
identifier: Archive_and_Export_IPA
name: Archive and Export IPA
steps:
- parallel:
- step:
identifier: xcworkspace
name: xcworkspace
type: Run
spec:
shell: Sh
command: |-
## Insert script to build xcode archive and export the archive to .ipa. The following commands are shortened examples.
xcodebuild -list -workspace "<+stage.variables.xcode_xcworkspaceLocation>"
xcodebuild -workspace "<+stage.variables.xcode_xcworkspaceLocation>" -scheme ...
xcodebuild -exportArchive -archivePath "/tmp/harness/<+stage.variables.xcode_archivePath>" -exportPath . ...
envVariables:
schemeName: <+stage.variables.xcode_scheme>
XCODE_PROVISIONING_ID: <+stage.spec.execution.steps.Definevariables.output.outputVariables.XCODE_PROVISIONING_ID>
when: ## Run this step only if XCODE_TYPE is xcworkspace.
stageStatus: Success
condition: <+stage.variables.xcode_type> == "xcworkspace"
- step:
identifier: xcodeproj
name: xcodeproj
type: Run
spec:
shell: Sh
command: |-
## Insert script to build xcode archive and export the archive to .ipa. The following commands are shortened examples.
xcodebuild -list -workspace "<+stage.variables.xcode_xcworkspaceLocation>"
xcodebuild -workspace "<+stage.variables.xcode_xcworkspaceLocation>" -scheme ...
xcodebuild -exportArchive -archivePath "/tmp/harness/<+stage.variables.xcode_archivePath>" -exportPath . ...
envVariables:
schemeName: <+pipeline.stages.Build_iOS_app.variables.xcode_scheme>
password: <+secrets.getValue("my_password_secret")>
when:
stageStatus: Success
condition: <+stage.variables.xcode_type> == "xcproject"
- step:
identifier: Extract_Build_Settings
name: Extract Build Settings
type: Run
spec:
shell: Sh
command: |-
## Insert script to extract build settings to variable values. Populated variables are declared in the outputVariables so they can be referenced in later steps.
outputVariables:
- name: PRODUCT_BUNDLE_IDENTIFIER
- name: PRODUCT_NAME
- name: FULL_PRODUCT_NAME
- name: PRODUCT_SETTINGS_PATH
- name: TARGETED_DEVICE_FAMILY
- name: MARKETING_VERSION
- name: CURRENT_PROJECT_VERSION
- step:
identifier: Tag_Artifact
name: Tag Artifact
type: Run
spec:
shell: Sh
command: |-
## Insert script to tag final build artifact. For example, rename the exported .ipa.
- stepGroup: ## This step group uploads the final artifact to JFrog. The destination depends on the value of the jfrog_InstanceType variable.
identifier: Upload_Artifact
name: Upload Artifact
steps:
- parallel:
- step:
identifier: Upload_IPA_to_Artifactory
name: Upload to Cloud Artifactory
type: Run
spec:
shell: Sh
command: |-
## Insert commands to upload the artifact to JFrog artifactory. You could also use the built-in Upload Artifact to JFrog Artifactory step, instead of a Run step.
when: ## Run this step only if the jfrog_InstanceType is saas.
stageStatus: Success
condition: <+stage.variables.jfrog_InstanceType> ==
"saas"
- step:
identifier: Upload_IPA_to_onPrem_Artifactory
name: Upload to onPrem Artifactory
type: Run
spec:
shell: Sh
command: |-
## Insert commands to upload the artifact to JFrog artifactory. You could also use the built-in Upload Artifact to JFrog Artifactory step, instead of a Run step.
when: ## Run this step only if the jfrog_InstanceType is onprem.
stageStatus: Success
condition: <+stage.variables.jfrog_InstanceType> ==
"onprem"
when: ## Run this step group only if the jfrog_uploadArtifact variable is true.
stageStatus: Success
condition: <+stage.variables.jfrog_uploadArtifact> == "true"
failureStrategies: []
variables:
- name: xcode_scheme
type: String
description: ""
value: my_xcode_scheme
- name: xcode_provisioningProfile
type: String
description: ""
value: my_app.provisioning.profile
- name: jfrog_InstanceType
type: String
description: ""
value: saas
- name: xcode_xcworkspaceLocation
type: String
description: ""
value: my.xcworkspace
- name: xcode_archivePath
type: String
description: ""
value: my.xcarchive
- name: xcode_type
type: String
description: ""
value: xcworkspace
- name: jfrog_uploadArtifact
type: String
description: ""
value: "true"
- name: certpath_preference
type: String
description: ""
value: Profiles
- name: profiles_preference
type: String
description: ""
value: Profiles
- name: exportoptions_preference
type: String
description: ""
value: Profiles
- name: suppress_cocoapods
type: String
description: ""
value: "true"
- name: load_certificates
type: String
description: ""
value: "true"
delegateSelectors:
- macos-ci-delegate
delegateSelectors:
- macos-ci-delegate
variables:
- name: build_anka_image
type: String
description: ""
value: osx-pool
properties:
ci:
codebase:
connectorRef: YOUR_CODEBASE_CONNECTOR_ID
repoName: YOUR_CODEBASE_REPO_NAME
build:
type: tag
spec:
tag: 1.2.3456
tags: {}
projectIdentifier: default
orgIdentifier: default
This example uses fastlane to build and test a Swift-based iOS app project. This example uses step templates to repeat common steps in each stage. You can use input sets to make customizable templates that allow users to use the same pipeline for multiple use cases by varying the input for certain values provided at runtime.
pipeline:
name: iOS Build
identifier: iOS_Build
projectIdentifier: default
orgIdentifier: default
tags: {}
stages:
- stage:
name: iOS Unit Tests
identifier: iOS_Unit_Tests
type: CI
spec:
caching: ## This pipeline uses Cache Intelligence.
enabled: true
paths:
- /Users/anka/Library/Caches/org.swift.swiftpm
cloneCodebase: true
platform: ## This pipeline uses Harness Cloud build infrastructure.
os: MacOS
arch: Arm64
runtime:
type: Cloud
spec: {}
execution:
steps:
- stepGroup: ## This pipeline uses step templates to setup Fastlane variables and install dependencies. Using templates ensures uniformity across pipelines.
name: Environment Setup
identifier: Environment_Setup
steps:
- parallel:
- step:
name: Fastlane Variables
identifier: Fastlane_Variables
template:
templateRef: Fastlane_Variables
versionLabel: "1.0"
- step:
name: Bundle Install
identifier: Bundle_Install
template:
templateRef: Bundle_Install
versionLabel: 1.0.0
- step:
type: Run
name: Brew Install
identifier: Brew_Install
spec:
shell: Sh
command: brew install <+repeat.item>
strategy: ## This step uses a repeat strategy to loop over 'brew install' commands.
repeat:
items:
- xcbeautify
- swiftlint
- step:
type: Run
name: Unit Tests
identifier: Unit_Tests
spec:
shell: Sh
command: fastlane do_unit_tests
- stage:
name: iOS Build
identifier: iOS_Build
type: CI
spec:
caching:
enabled: true
paths:
- /Users/anka/Library/Caches/org.swift.swiftpm
cloneCodebase: true
platform:
os: MacOS
arch: Arm64
runtime:
type: Cloud
spec: {}
execution:
steps:
- stepGroup:
## Repeat environment setup step group from first stage.
- step:
type: Run
name: Build and upload QA test
identifier: Build_and_upload_QA_test
spec:
shell: Sh
command: |-
## Insert commands to upload build for QA testing.
- stage: ## This is a custom stage that include a Jira Update step. The step updates all Jira issues associated with this build so that testers can test them in QA.
name: JIRA Status
identifier: JIRA_Status
description: ""
type: Custom
spec:
execution:
steps:
- step:
type: JiraUpdate
name: Update JIRA
identifier: Update_JIRA
spec:
connectorRef: YOUR_JIRA_CONNECTOR_ID
issueKey: <+repeat.item>
transitionTo:
transitionName: ""
status: Ready for QA
fields:
- name: Assignee
value: qa.tester@company.com
- name: Description
value: This is available for QA testing.
timeout: 10m
strategy:
repeat:
items: ## List Jira keys to update.
tags: {}
when:
pipelineStatus: Success
condition: "false"
variables: ## These are some possible fastlane variables that could be set in the Environment Setup steps.
- name: FL_BUILD_NUMBER ## This variable gets its value from a Harness expression.
type: String
description: Job ID for pipeline.
required: false
value: <+pipeline.sequenceId>
- name: IS_CI
type: String
description: ""
required: false
value: "1"
- name: FL_REPO_BRANCH
type: String
description: Branch from which this pipeline was triggered.
required: false
value: <+input>
- name: FL_RELEASE_NOTES ## This variable accepts user input for the value, and it provides a list of allowed values for the user to choose from.
type: String
description: ""
required: false
value: <+input>.default(automatic).allowedValues(automatic,manual)
- name: FL_OVERRIDE_RELEASE_NOTES ## This variable accepts any user input as the value.
type: String
description: Use this option if you want to override automatic JIRA Release Notes. Must set FL_RELEASE_NOTES to manual.
required: false
value: <+input>
- name: FL_LOGGING_ENABLED
type: String
description: ""
required: false
value: "true"
- name: FL_BUILD_SCHEMES ## This variable accepts user input for the value, and it provides a list of allowed values for the user to choose from.
type: String
description: ""
required: false
value: <+input>.allowedValues(AppStore Test, Prod, Test).executionInput()
- name: FL_BUILDSCRIPT_BRANCH
type: String
description: ""
required: false
value: feature/CI_CD_2_0
- name: FL_QA_ASSIGNEE
type: String
description: Email Address of the QA tester for the build
required: false
value: <+input>
- name: FL_BUILD_VERSION
type: String
description: Version number of the project which is used to generate builds. The format of the version number should always be Major.Minor.Patch, such as 10.8.0.
required: false
value: <+input>
properties:
ci:
codebase:
connectorRef: YOUR_CODE_REPO_CONNECTOR_ID
repoName: YOUR_CODE_REPO_NAME
build: <+input>
This example demonstrates a pipeline that builds, tests, and deploys a mobile app in one stage.
pipeline:
name: macostest
identifier: macostest
projectIdentifier: default
orgIdentifier: default
tags: {}
stages:
- stage:
name: build
identifier: build
description: ""
type: CI
spec:
cloneCodebase: true
platform:
os: MacOS
arch: Arm64
runtime:
type: Cloud
spec: {}
execution:
steps:
- step:
type: Run
identifier: build
name: build
spec:
shell: Sh
command: |-
xcodebuild clean build -workspace "myProject.xcworkspace" -scheme "myProject"
- step:
type: Run
name: test
identifier: test
spec:
shell: Sh
command: |-
xcodebuild test -workspace "myProject.xcworkspace" -scheme "myProject"
- step:
type: Run
name: deploy
identifier: deploy
spec:
shell: Sh
command: |-
## Install certs
CERTIFICATE_P12=certificate.p12
echo $APPLE_DISTRIBUTION_CERTIFICATE_KEY | base64 --decode > $CERTIFICATE_P12
security unlock-keychain -p $BUILD_KEY_CHAIN_PASSWORD $BUILD_KEY_CHAIN
security set-keychain-settings $BUILD_KEY_CHAIN
security import $CERTIFICATE_P12 -k $BUILD_KEY_CHAIN -P $APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $BUILD_KEY_CHAIN_PASSWORD $BUILD_KEY_CHAIN
rm -fr *.p12
## Provison profile
PROFILE_FILE=${DISTRIBUTION_PROVISION_UUID}.mobileprovision
echo $DISTRIBUTION_PROVISION_KEY | base64 --decode > $PROFILE_FILE
cp ${PROFILE_FILE} "$HOME/Library/MobileDevice/Provisioning Profiles/${DISTRIBUTION_PROVISION_UUID}.mobileprovision"
rm -fr *.mobileprovision
## Define variables for archive
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/myProject/${CI_COMMIT_SHA}/${CI_JOB_ID}.xcarchive"
EXPORT_PATH="$HOME/Library/Developer/Xcode/Archives/myProject/${CI_COMMIT_SHA}/${CI_JOB_ID}/"
## Create archive
xcodebuild -workspace myProject.xcworkspace -scheme "myProject" clean archive -sdk iphoneos -archivePath $ARCHIVE_PATH PROVISIONING_PROFILE_SPECIFIER="${DISTRIBUTION_PROVISION_UUID}" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" | xcpretty --c
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportOptionsPlist ExportOptionsAppStore.plist -exportPath $EXPORT_PATH PROVISIONING_PROFILE_SPECIFIER="${DISTRIBUTION_PROVISION_UUID}" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}"
IPA="${EXPORT_PATH}myProject.ipa"
## Upload to App Store
xcrun altool --upload-app -t ios -f $IPA -u $ITC_USER_NAME -p $ITC_USER_PASSWORD
envVariables:
ITC_USER_NAME: <+secrets.getValue("my_app_store_id)>
ITC_USER_PASSWORD: <+secrets.getValue("my_app_store_app_password")>
properties:
ci:
codebase:
connectorRef: YOUR_CODE_REPO_CONNECTOR_ID
repoName: YOUR_REPO_NAME
build: <+input>
The above example was adapted from a Canopas blog post that used GitLab CI to demonstrate mobile app deployment. For more information about converting GitLab workflows to Harness CI pipelines, go to Migrate from GitLab CI to Harness CI.
This example shows how you can run Bitrise Steps in the Harness CI Bitrise step to deploy an Android app. This example is based on Bitrise workflow recipes for Android apps, and the same concepts apply to Bitrise workflow recipes for iOS apps.
- step:
type: Bitrise
name: change android version
identifier: change_android_version
spec:
uses: github.com/bitrise-steplib/steps-change-android-versioncode-and-versionname.git ## Specify the Bitrise Step's GitHub repo.
with: ## Define settings (inputs) required for the steps.
new_version_name: '1.1.0'
new_version_code: '<+pipeline.sequenceId>'
build_gradle_path: '/path/to/build.gradle'
- step:
type: Bitrise
name: bitrise android build
identifier: bitrise_android_build
spec:
uses: github.com/bitrise-steplib/bitrise-step-android-build.git
with:
project_location: '/path/to/build.gradle'
variant: 'release'
build_type: 'aab'
- step:
type: Bitrise
name: bitrise sign apk
identifier: bitrise_sign_apk
spec:
uses: github.com/bitrise-steplib/steps-sign-apk.git
with:
android_app: '/path/to/aab'
- step:
type: Bitrise
name: bitrise google play deploy
identifier: bitrise_google_play_deploy
spec:
uses: github.com/bitrise-steplib/steps-google-play-deploy.git
with:
service_account_json_key_path: '$SERVICE_ACCOUNT_KEY_URL'
package_name: 'my.android.project'
app_path: '/path/to/signed/aab'
track: 'beta'
env: ## Define environment variables, if required by the step.
SERVICE_ACCOUNT_KEY_URL: <+secrets.getValue("json_key_path")>