For backwards compatibility purposes, I like to keep my continuous integration and deployment workers on the lowest version available. For macOS, this was 10.15 up until recently when Microsoft (rightfully) retired it and made macOS 11 the lowest version available.
Here’s the reason for this post: there’s a change in the exit code behavior of the security command, making any pipeline with multiple certificates fail.
It took me a while to figure out why and there wasn’t an existing post documenting this error. So, here we go. 😉
App & Installer Certificates
I mostly build desktop apps, which have a code sign certificate for the app itself and a different installer sign certificate for the installer that contains the app. As a good pipeline citizen, I’m using the InstallAppleCertificate@2 task to install the certificate. Here’s what it used to look like:
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'developerID_installer.p12'
certPwd: '$(macOS.p12Password)'
keychain: 'temp'
displayName: Installing certificate to sign installer with
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'developerID_application.p12'
certPwd: '$(macOS.p12Password)'
keychain: 'temp'
displayName: Installing certificate to sign app with
The certificates were installed into a temporary keychain, mostly because I didn’t want to use the system keychain and risk them sticking around for whatever reason.
Keychain ‘temp’ always gets deleted
Here’s what I learned after a few hours. The code for the InstallAppleCertificate@2 task always cleans up the temp keychain at the first cleanup task, no matter whether there are multiple certificate tasks using that keychain. Here’s the code that explains it.
Solution: Use a Custom Keychain
I thought about patching the task to go ahead and check whether there are other certificates left in the temp keychain before deleting it, but found an easier way:
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'developerID_installer.p12'
certPwd: '$(macOS.p12Password)'
deleteCert: true
deleteCustomKeychain: true
keychain: 'custom'
customKeychainPath: ~/whatpulse.keychain
keychainPassword: '$(macOS.p12Password)'
displayName: Installing certificate to sign installer with
- task: InstallAppleCertificate@2
inputs:
certSecureFile: 'developerID_application.p12'
certPwd: '$(macOS.p12Password)'
deleteCert: false
deleteCustomKeychain: false
keychain: 'custom'
customKeychainPath: ~/whatpulse.keychain
keychainPassword: '$(macOS.p12Password)'
displayName: Installing certificate to sign app with
First, the custom keychain is not deleted by InstallAppleCertificate@2 if you set deleteCustomKeychain to false. Second, the cleanup order is reversed. The pre-install jobs will install the installer certificate first, and installs the app certificate second. Then the post-install jobs will remove the app certificate first, then the installer certificate second. That’s why the first task definition has deleteCert and deleteCustomKeychain set to true and the second definition has them set to false.
After this change, both tasks are successful:
Hope this helps the next person running into this. 😊
February 13, 2024 at 12:42
Thank you for the effort of diving into this codebase where worrisome string checks like `”if (keychain === ‘temp’) {“` live.
At least the official documentation hints into the correct direction on how this all works.
👋😆