In our previous installment we talked about why you might want to use your own API client to supplant some features of fastlane match and we wrote some code, in Swift, to help us do that, starting with fetching provisioning profiles and signing certificates that already exist in App Store Connect.
In this installment we’ll build on top of that to continue fleshing out more of the functionality fastlane match provides: creating and invalidating provisioning profiles and signing certificates, the bulk of fastlane match’s nuke and match actions. Thankfully, the App Store Connect API provides the required endpoints, which we’ll be exploring in this post. Let's dive in!
Creating a signing certificate
To create a certificate we'll be calling POST v1/certificates, which is the first time we’ve used the <code>POST<code> verb. To save some time, let's update our API generator configuration with all the new paths we'll be using (note that the verbs don't matter for this part as the generator creates code for all the supported verbs for a path).
A quick build and the code to generate our certificates has been created and we can get down to business. The first thing you'll need is a Certificate Signing Request or CSR. This is used by Apple to generate your certificate. You'll also need to know what type of certificate you want to make. Thankfully we created an enum of these possible values in the last article, and we’ll also be able to reuse the <code>Certificate<code> type that we made last time. Here's how we'll request the certificate's creation:
To simplify things, we’ve created a <code>typealias<code> for the certificate payload type (to avoid a lot of nesting), and broken out the creation of the payload to an extension on that type. We’ve also created a helper which converts the <code>Components.Schemas.CertificateType<code> to our own <code>CertificateType<code>.
Our method on the <code>APIClient<code> will take in the contents of the CSR (which is formatted much like an SSH private key) and will pass that along to the API. It also takes in the type of certificate to generate and makes the API call. The result of this is much like the payload type that we got back when we queried the API for existing certificates. The only difference is that the response is the single certificate that was created and not an array of certificates attached to our development team.
Revoking a signing certificate
On the flip side of creation is revoking a certificate. Revoking in App Store Connect parlance is the equivalent of deleting the certificate and only needs an ID mapping to the certificate which needs deletion.
Looking at the API client call here, the picture continues to fill out of how the API client generator code names things. <code>certificates_hyphen_delete_instance<code> maps to the <code>DELETE /v1/certificates<code> endpoint, and the <code>path<code> input appends the certificate's ID to the path when the request is formed. You'll notice that we don't do anything with the response, and that's because these <code>DELETE<code> calls can be considered "fire and forget" and for our purposes we don't need to worry about error handling.
However, this doesn't compile because we didn't put an <code>id<code> property on our <code>Certificate<code> type, so let's do that to get our client compiling:
Now the library compiles and we can delete a certificate!
Creating a provisioning profile
Provisioning profiles require more data than certificates, making them a bit trickier to create. We'll also be working with an incredibly nested set of data types and as a result are going to build this in a slightly different order than we did certificates above. Let's start with building out the data payload that we'll be sending:
There's a lot going on in this chunk so let's walk through it piece by piece. Firstly the <code>ProfilePayload<code> type alias represents the full body payload of our eventual request. This initial alias is useful to make not only this body type more compact but a couple of other things more compact as well — namely the <code>CertificateRelationship<code> and <code>SchemaProfileType<code> types.
We haven't talked about <code>ProfileType<code> yet but it looks and works much like the <code>CertificateType<code>. <code>ProfileType<code> is analogous to the profileType property on the <code>attributes<code> member of our payload. It's an enum of strings with each case mapping to the possible values of the strings on the documentation link. The initializer there is much like our convenience for certificates and behaves the same way.
Next up is our first foray into relationships. Relationships tell the backend to tie into other types of data rather than including them in the attributes payload. There are 2 required relationships for creating profiles: the certificates used to sign the profile, and the bundle identifier. The certificate relationship value in particular is defined as an array of certificate data (documented here). The convenience above will let us map an array of our <code>Certificate<code> type to this data payload (where there is a hard-coded type <code>.certificates<code> as well). It's a nice thing to have and we deserve nice things.
All this leads to our static function to create a payload. This method is pretty straightforward, where we utilize our newly-created helpers to create the attributes and relationships of the payload, and nest those in the structure of the payload initializer itself.
Doing this legwork at the beginning lets us write an API client method that hopefully will look pretty familiar at this point:
The method takes as inputs the things needed to build up the payload (notice that the payload creation <code>throws<code> and that lets us create it with a <code>try<code>, and in the event of a thrown error we don't need to handle it in the API client method, it will be forwarded along to our caller). From there we create the request, wait for it to complete, and unwrap the profile from the successful response.
We have a brand new shiny profile!
Deleting a provisioning profile
Deleting a profile that is not needed anymore is thankfully just as easy as deleting a certificate.
It is once again a fire-and-forget operation, and much like certificates we've also added an <code>id<code> property to the <code>Profile<code> type we made before.
We have arrived
We've come a long way in this fastlane match mini-series. We can create, retrieve, and delete signing certificates and provisioning profiles using Swift code alone. This code can run as a step in your build process or as a pre-flight on your Continuous Integration systems. In an earlier article we talked about how we can use fastane and its match_nuke action to automate the renewal of your code signing credentials. Using our App Store Connect API client we can replicate that custom lane using exclusively Swift code. We'll start with a new <code>SigningManager<code> which can have a method to rebuild distribution signing and go from there.
The method here looks complicated but is broken down in a few logical steps:
- This class has instances of the <code>ProfileManager<code> and <code>CertificateManager<code> classes that we made in earlier articles. We'll use those to get the distribution certificates first, and then filter just the profiles that use those certificates.
- We make a task group, which lets us run async tasks concurrently, so that we can make tasks to delete the profiles and certificates from App Store Connect. This will give us the clean slate we need to create the new credentials.
- The first thing we'll remake is the distribution certificates. We'll create an array of the new certificates and loop through our local copies of the ones we just deleted to create the new ones and add them to the array. You'll have to supply a CSR for each one, and this is doable using the <code>openssl<code> process (but is outside of the scope of the article).
- Lastly we'll remake the provisioning profiles. Like certificates we'll loop over the local copies of the profiles we deleted and remake them. Note how we're finding the correct renewed certificates to include in the profile request. And from there we use the rest of the data from the old profile to create the new one.
And with this method we can now rebuild our distribution signing using our own Swift code, just like we did using fastlane!
Bonus: troubleshooting the App Store Connect API
In building allthis out, we've done a lot with the App Store Connect API. We've talked a little bit about the quirks of the API when it comes to date formatters, but there's another “quirk” you may run into (and it is very applicable to our topic). There can be times when the OpenAPI spec and documentation are wrong. Not a little wrong — completely wrong!
For example, the API call for fetching provisioning profiles returns a profile response that does not include things we need when making our new profile, such as the bundle ID and the certificates used to make the old profile. To get this data we have to make 2 other API calls, to Read the BundleID in a Profile and to List All Certificates in a Profile. Those documentation pages will list the URLs to be called as well as the expected response objects, like we've seen before.
The problem is that they don't return those types. At all. The certificates response says that a successful call will return something called the CertificatesWithoutIncludesResponse and inside that object is an array of profiles, only our call was expecting certificates! So how do we get the certificates we want? Well, it turns out that we do get them in the API call we make to the <code>v1/profiles/{id}/certificates<code> endpoint, but the documentation and OpenAPI spec are wrong. We're just going to have to do this parsing ourselves.
Firstly we'll make a method on our API client to handle fetching a URL and decoding its response. This method does all the auth that we need and handles the date oddities we looked at before. At the end of it if we have a successful payload decode then we can return it and be on our way. But what kind of payload do we decode if everything to this point has been generated?
Here we have a method on our <code>ProfileManager<code> to grab the associated certificates. It constructs the URL based on the identifier of the profile and the payload it's expecting is one of our own creation — kind of. It turns out that the generated <code>Components.Schemas.Certificate<code> type is what is actually returned by the API, it's just wrapped in a different outer shell. This is where the power of Swift comes in really handy and we can make a disposable <code>Decodable<code> type which mirrors the API response we get back from App Store Connect, and this will decode perfectly! We already have the ability to make a certificate by the response schema, so we can map the array of those schemas to our certificates and return them from our method.
Likewise we need to have a similar method for fetching the bundle IDs. This will give us the bundle ID associated with the existing profile, so that we can use it to make our new profile:
Learning how to troubleshoot these issues can be difficult. It's very helpful to have a tool like Proxyman to help out when making an API client of any kind. Proxyman can sit between your code and the traffic going in and out of your Mac, and actually show you the requests and responses in real-time. That way you can see if you're getting responses like the documentation and specs say you are (because if you run into decoding errors there’s a decent chance the response is not what you'd expect it to be).
OK, now we have arrived
We have now built out in Swift an API client to talk with App Store Connect, and all the pieces we need to rebuild our signing credentials just like we did in fastlane match. We've also looked at ways the API specification and documentation may differ from the responses that are actually returned to you. You're now ready to go and conquer the App Store Connect API!
See even earlier posts in our App Store Connect API series:Â