Using App Store Connect API to trigger Xcode Cloud workflows
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
App Store Connect API allows developers to create, manage and trigger Xcode Cloud workflows using network requests. While this is a lesser known feature of the API, it can certainly prove incredibly useful to automate processes and extend the capabilities of Xcode Cloud by, for example, adding custom workflow triggers.
I am planning on making a follow up article implementing a workflow trigger which is currently unavailable by default from Xcode Cloud using webhooks and App Store Connect API, so keep your eyes peeled for it! 👀
This article goes through how to interact with the App Store Connect API in Swift using Antoine van der Lee’s appstoreconnect-swift-sdk Swift Package to:
- Get a list of all available Xcode Cloud products.
- Select an Xcode Cloud product based on its repository name.
- Retrieve a list of workflows for an Xcode Cloud product.
- Retrieve information for a workflow.
- Trigger a workflow run.
This article doesn’t go through how to create an App Store Connect API key. If you would like to learn more about how to do so, please refer to this article on my blog.
Setting up the Swift SDK
To start making requests to the App Store Connect API using the appstoreconnect-swift-sdk libary, one must first create an instance of APIConfiguration
with the required credentials and pass this configuration to an APIProvider
instance, which will make all authenticated requests.
func startWorkflow(in repo: String, withCredentials credentials: ASCCredentials) async throws {
let config = APIConfiguration(
issuerID: credentials.issuerID,
privateKeyID: credentials.privateKeyID,
privateKey: credentials.privateKeyContents
)
let provider = APIProvider(configuration: config)
}
Getting an Xcode Cloud product
The next step is then to retrieve the information for the Xcode Cloud product containing the workflow we want to trigger by making a GET
request to the ciProducts endpoint.
We can use the APIEndpoint
type to create the request to the ciProducts route and filter by product type (in this case we only want to show app products).
Furthermore, since we want to select the correct product from its repository name, we need to specify that we want to include the primaryRepositories
data associated with each product to the response.
func startWorkflow(in repo: String, withCredentials credentials: ASCCredentials) async throws {
// ...
let producstEndpoint = APIEndpoint
.v1
.ciProducts
.get(parameters: .init(filterProductType: [.app], include: [.primaryRepositories]))
let productResponse = try await provider.request(producstEndpoint)
}
Now that we have a list of Xcode Cloud products and their repository names, we can select the product which matches the repository name we’re looking for.
// ...
func startWorkflow(in repo: String, withCredentials credentials: ASCCredentials) async throws {
// ...
guard let repositoryId: String = productResponse
// 1
.included?
// 2
.compactMap({ includedItem in
switch includedItem {
case .scmRepository(let scmData) where scmData.attributes?.repositoryName == repo:
return scmData.id
default: return nil
}
})
// 3
.first,
// 4
let productId = productResponse.data.first(where: {
$0.relationships?.primaryRepositories?.data?.contains { $0.id == repositoryId } == true
})?.id else { return }
}
Let’s step through the code above and explain what’s going on in more detail:
- Retrieve the
included
data from the response. This is where the repository data for each product will be. - Map the
included
items into repository ids and remove any where their name doesn’t match the repo we’re looking for. - Retrieve the first repository id in the list.
- Find the first product which contains a repository with the same id as
repositoryId
.
Retrieving the workflow information
Now that the we have found the Xcode Cloud product we’re looking for, we can get all available workflows via the same ciProducts endpoint by specifying an id and querying the /workflows
subpath this time:
struct WorkflowsResponse: Decodable {
let data: [Data]
struct Data: Decodable {
let id: Int
}
}
func startWorkflow(in repo: String, withCredentials credentials: ASCCredentials) async throws {
// ...
let allWorkflowsEndpoint = APIEndpoint
.v1
.ciProducts
.id(productId)
.relationships
.workflows
let workflows = try await provider
.request(
Request<WorkflowsResponse>(
method: "GET",
path: allWorkflowsEndpoint.path
)
)
}
Note that while the appstoreconnect-swift-sdk Swift Package provides response models for most endpoints, you might sometimes have to define your own
Decodable
models, just like I did for retrieving all workflows for a product above.
We can now retrieve the information for a specific workflow by making a GET
request to the ciWorkflows endpoint with a workflow id:
func startWorkflow(in repo: String, withCredentials credentials: ASCCredentials) async throws {
// ...
guard let workflowId = workflows.data.first?.id else {
return
}
let workflowEndpoint = APIEndpoint
.v1
.ciWorkflows
.id(workflowId)
.get()
let workflow = try await provider.request(workflowEndpoint).data
}
This product only has a single product, so we didn’t really need to retrieve the workflow information (we could have retrieved the id from the workflows list instead). I decided to still include this request for completion sake.
Starting a workflow
The App Store Connect API allows you to start a new workflow by making a POST
request to the ciBuildRuns endpoint and passing a CiBuildRunCreateRequest as the request’s body.
The CiBuildRunCreateRequest body must include the type of request (.ciBuildRuns
) and the id of the workflow that we want to run as a relationship.
let requestRelationships = CiBuildRunCreateRequest
.Data
.Relationships(workflow: .init(data: .init(type: .ciWorkflows, id: workflow.id)))
let requestData = CiBuildRunCreateRequest.Data(
type: .ciBuildRuns,
relationships: requestRelationships
)
let buildRunCreateRequest = CiBuildRunCreateRequest(data: requestData)
let workflowRun = APIEndpoint
.v1
.ciBuildRuns
.post(buildRunCreateRequest)
_ = try await provider.request(workflowRun)
That’s it! Running the startWorkflow
function will trigger a new workflow run! 🎉