Motivation
If you are building an iOS app on Azure DevOps, you will notice that pipeline tasks related to Apple are not well maintained. So relying on xcodebuild and altool scripts was my best bet.
1. Using Automatic Code Signing
For automatic signing you need to create a Apple Development certificate. Download and add it to keychain.
Enable Automatically manage signing and select Team. Push your changes to git remote.
Create a new pipeline on Azure DevOps. Your newly generated azure-pipelines.yml will look like this:
trigger:
- main
pool:
vmImage: 'macos-latest'
steps:
- task: Xcode@5
inputs:
actions: 'build'
scheme: ''
sdk: 'iphoneos'
configuration: 'Release'
xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
xcodeVersion: 'default' # Options: 8, 9, 10, 11, 12, default, specifyPath
Add Secure Files to Azure DevOps
Add the following files to pipeline -> library -> secure files as below.
- Exported apple development certificate from keychain as .p12 file. See dev-cert.p12 below.
2. Api Key from https://appstoreconnect.apple.com/ with Admin or App Manager Role (This is important for auto provisioning). See AuthKey_ — .p8 below.
Add Pipeline Variables
Add your Api Key Id and Issuer Id variables which can be obtained from appstoreconnect -> Keys page.
Add ExportOptions.plist
Add a file named ExportOptions.plist to your project root where your azure-pipelines.yml is. And update it for automatic code signing. Replace your team id below. Push your changes to remote git repo.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>generateAppStoreInformation</key>
<false/>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>{REPLACE_WITH_YOUR_TEAM_ID}</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
Update your azure-pipelines.yml
Following steps are added to pipeline:
- Install apple development certificate to keychain
- Download AuthKey_XXX.p8 file to use in the following steps
- Archive iOS project
- Export archive to ipa file
- Move AuthKey file to ~/.private_keys directory so altool can use it.
- Upload ipa to AppStore
# Xcode
# Build, test, and archive an Xcode workspace on macOS.
# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode
trigger:
- main
pool:
vmImage: 'macos-latest'
variables:
- name: PROJECT_NAME
value: azure-pipeline-demo
steps:
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'dev-cert.p12'
keychain: 'temp'
- task: DownloadSecureFile@1
name: apiKey
displayName: 'Download AuthKey_{KeyID}.p8 file'
inputs:
secureFile: 'AuthKey_$(API_KEY_ID).p8'
- task: Bash@3
displayName: 'Archive'
inputs:
targetType: 'inline'
script: |
xcodebuild archive -project '$(PROJECT_NAME).xcodeproj' -scheme '$(PROJECT_NAME)' -sdk iphoneos -archivePath $(PROJECT_NAME).xcarchive -allowProvisioningUpdates
- task: Bash@3
displayName: 'Export Archive'
inputs:
targetType: 'inline'
script: |
xcodebuild -exportArchive -archivePath $(PROJECT_NAME).xcarchive -exportPath $(PROJECT_NAME).ipa -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates -authenticationKeyID $(API_KEY_ID) -authenticationKeyIssuerID $(API_KEY_ISSUER_ID) -authenticationKeyPath $(apiKey.secureFilePath)
- task: Bash@3
displayName: 'Upload to AppStore'
inputs:
targetType: 'inline'
script: |
mkdir ~/.private_keys
mv $(apiKey.secureFilePath) ~/.private_keys
xcrun altool \
--upload-app \
-f $(PROJECT_NAME).ipa \
-t ios \
--apiKey $API_KEY_ID \
--apiIssuer $API_KEY_ISSUER_ID \
--verbose \
2. Using Manual Code Signing
We need to create an Apple Distribution certificate to be able to use manual code signing. So create an Apple Distribution certificate from the https://developer.apple.com/account/resources/certificates/add. Download and add to your keychain. Export to p12 file again. Also create a new Provisioning Profile for App Store distribution. Download and double click to add it to Xcode. Finally add p12 file and provisioning file to your secure files on Azure DevOps Pipeline Library.
Uncheck “Automatically manage signing” in Xcode to enable manual code signing and make sure new Apple Distribution certificate and provisioning profile is found and there is no warning. Push your changes to git remote.
Now we will still need AutKey.p8 file but App Manager role will be sufficient since we don’t need auto provisioning.
Update your ExportOptions.plist for manual code signing. Push your changes to git remote.
<key>signingStyle</key>
<string>manual</string>
Your new azure-pipelines.yml file will look like below. Only difference here is we used dist.p12 (Apple Distribution certicifate) and InstallAppleProvisioningProfile@1 step.
# Xcode
# Build, test, and archive an Xcode workspace on macOS.
# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode
trigger:
- main
pool:
vmImage: 'macos-latest'
variables:
- name: PROJECT_NAME
value: azure-pipeline-demo
steps:
- task: Bash@3
displayName: Select Xcode Version
inputs:
targetType: 'inline'
script: |
echo Mac OS version:
sw_vers -productVersion
echo selecting latest xcode...
sudo xcode-select -s /Applications/Xcode_15.2.app
xcrun xcode-select --print-path
xcodebuild -version
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'dist.p12'
keychain: 'temp'
- task: InstallAppleProvisioningProfile@1
displayName: 'Install Profiles iOS'
inputs:
provisioningProfileLocation: 'secureFiles'
provProfileSecureFile: 'azuredemodist.mobileprovision'
- task: DownloadSecureFile@1
name: apiKey
displayName: 'Download AuthKey_{KeyID}.p8 file'
inputs:
secureFile: 'AuthKey_$(API_KEY_ID).p8'
- task: Bash@3
inputs:
targetType: 'inline'
script: |
xcodebuild archive -project '$(PROJECT_NAME).xcodeproj' -scheme '$(PROJECT_NAME)' -sdk iphoneos -archivePath $(PROJECT_NAME).xcarchive
- task: Bash@3
inputs:
targetType: 'inline'
script: |
xcodebuild -exportArchive -archivePath $(PROJECT_NAME).xcarchive -exportPath $(PROJECT_NAME) -exportOptionsPlist ExportOptions.plist
- task: Bash@3
inputs:
targetType: 'inline'
script: |
mkdir ~/.private_keys
mv $(apiKey.secureFilePath) ~/.private_keys
xcrun altool \
--upload-app \
-f $(PROJECT_NAME)/$(PROJECT_NAME).ipa \
-t ios \
--apiKey $API_KEY_ID \
--apiIssuer $API_KEY_ISSUER_ID \
--verbose \
Run your pipeline and give me some claps if it works! 🎉