Using App Store Connect API to trigger Xcode Cloud workflows

Sponsored
RevenueCat logo
Relax, you can roll back your mobile release

No one is immune from shipping critical bugs to production, but Runway helps you limit the amount of havoc that can cause.

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:

  1. Get a list of all available Xcode Cloud products.
  2. Select an Xcode Cloud product based on its repository name.
  3. Retrieve a list of workflows for an Xcode Cloud product.
  4. Retrieve information for a workflow.
  5. 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.

XcodeCloudAPI.swift
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.

XcodeCloudAPI.swift
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.

XcodeCloudAPI.swift
// ...
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:

  1. Retrieve the included data from the response. This is where the repository data for each product will be.
  2. Map the included items into repository ids and remove any where their name doesn’t match the repo we’re looking for.
  3. Retrieve the first repository id in the list.
  4. 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:

XcodeCloudAPI.swift
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:

XcodeCloudAPI.swift
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.

XcodeCloudAPI.swift
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! 🎉

An image showing the resulting workflow run after making a call to App Store Connect