Xcode Cloud Logo

Some thoughts on Xcode Cloud

Anticipation

Ever since Apple acquired BuddyBuild back in 2018 there has been been speculation on what it’s plans are for providing automation infrastructure and tooling as part of its offering to developers. Indeed, when our team ran a sweepstakes for WWDC 2020, based on the format from the StackTrace podcast, it was the first item to be speculated on. It’s fair to say, that when Apple finally revealed the results of this at WWDC 2021, it was met with great excitement — at least from my team at work.

Xcode Cloud promises an easy to setup, secure, automated workflow for your apps, heavily integrated with Xcode and App Store Connect. But does it meet these promises? I attempted to build a pipeline similar to one I’ve previously written about to find out. You can find the demo pipeline in the repository for my open-source London Underground widgets.

Setup

There’s nothing much to say here, which in itself says a lot. You’ll need to enable Automatic Code Signing (if you haven’t already), but once you do, in true Apple-style, “it just works”. I think this is the first time I’ve used a CI/CD provider where the build passes on the first run with no code-signing issues and no missing dependencies.

Great work, Apple! ✅

Integration

Apple Tools

The biggest differentiator of Xcode Cloud from its competition is that it’s built right into Xcode and App Store Connect. You complete the initial setup within Xcode and you can view the results of your builds in both of these places. Combining this with the pull request support in Xcode 13, developers may have little reason to leave the IDE in a few years time. Should we expect an XcodeOS (à la Chrome OS) to be announced in 2025.

Other Tools

Version Control

Xcode Cloud works with Bitbucket, GitHub and GitLab version control providers. According to a highly unscientific Twitter poll, it looks like this should work for around 100% of teams. If you happen to use Azure DevOps (like me) – unlucky!

Build Scripts

Xcode Cloud allows you to implement your own build scripts at specific points in the workflow: after cloning the code, before running the build and then after running the build. Since one of the main selling points of Xcode Cloud is that it’s quick to setup and simple to use, the more custom scripts we implement, the less value we are getting from this, but it’s nice to know we have this power if we need it.

An illustration that shows the different steps Xcode Cloud performs when it performs an action, including the custom build scripts from left to right.
The Xcode Cloud Workflow Lifecycle, © Apple, 2021.

We just need to create a file called ci_post_clone.sh, ci_pre_xcodebuild.sh or ci_post_xcodebuild.sh in a folder called ci_scripts. Since Xcode Cloud agents have Homebrew pre-installed and configured, we’re able to quickly add most the tools we would use in iOS Development, including SwiftLint and Fastlane.

Don’t forget to make your scripts executable using chmod +x script_name!

If I’m honest, I’d love to see some additions to the pre-installed software here as build times are pretty lengthy when I have to install all my dependencies first. For comparison, I’ve made pretty good use of Azure DevOps in the past and their agents already come with both Fastlane and SwiftLint pre-installed.

SwiftLint

SwiftLint is a great way to ensure your whole codebase uses a consistent style. We can install it on Xcode Cloud by adding the following command in our post-clone (or other) script:

brew install swiftlint

It’s then trivial to run the tool, though we will need to pass in the path to our source code, which is helpfully provided by the $CI_WORKSPACE environment variable. There are a number of useful environment variables that are available during our builds, so it’s well worth reading through the documentation.

swiftlint --strict $CI_WORKSPACE

When running SwiftLint on the CI, I use the strict flag. This ensures that the whole team are following the code-style when merging into shared branches and prevents our project from building up with a large number of warnings, which may prevent more important issues from being surfaced.

A meme from the TV show, "The Office". A woman is shown two pieces of paper one saying "compiled successfully" and another saying "compiled with warnings". There is a subtitle saying "Corporate needs you to find the differences between this picture and this picture". The woman, who is labelled "developers" responds: "they're the same picture".
Fastlane

Fastlane can be installed in a very similar way to SwiftLint. Fastlane really supercharges our automation with a vast number of plugins that can help us integrate with a wide number of services. No doubt, Apple see Xcode Cloud as an alternative for some of Fastlane’s functionality, but with such a wide array of integration with third-party services (i.e. ability to upload symbol files to Firebase Crashlytics), it’s unlikely that it’s going to replace everything.

I’m not going to go into detail of what Fastlane plugins might be useful — there are other blog posts for that — but one example could be to add an “Alpha” or “Beta” badge to your app icon, so you can distinguish between them on the home-screen.

First we’d need to install the dependencies, once our clone is complete:

#!/bin/sh
set -e

brew install fastlane
fastlane add_plugin badge

Next, once we’re about to build, we should add the badge to our icons:

#!/bin/sh
set -e

if [ "$CI_WORKFLOW" = "Alpha Release" ];
then
    (cd $CI_WORKSPACE && fastlane badge_alpha)
elif [ "$CI_WORKFLOW" = "Beta Release" ];
then
    (cd $CI_WORKSPACE && fastlane badge_beta)
fi
SonarQube

I went down a bit of a rabbit-hole attempting to run SonarQube on Xcode Cloud. Suffice to say, it’s possible and should be easy enough for you to follow my lead.

I’ve spun off a separate article for this if you’re interested to find out how:

Communication

When builds complete, we often want to perform actions depending on whether they pass or fail. Xcode Cloud has a few options here, but it’s not as exhaustive as I’d like. The only options so far are to output to Slack or email. These are both great for letting our team know what’s happened, especially if something goes wrong or if there are new features to be tested. If you use another messaging platform, you’re out of luck for now, unless you implement a custom ci_post_xcodebuild.sh script yourself.

There’s also no support for any project management tools (i.e. Jira) yet, so I guess we will continue forgetting to move our tickets across the scrum board just like we always have!

Parallel Testing

One area where Xcode Cloud shines is allowing you to run your tests against multiple types of devices in parallel. I’ve attempted to do this locally, since parallel simulator testing was introduced in Xcode 9, but, at least for me, this has always ended up with the tests becoming flaky and randomly failing much more often: particularly with XCUITest cases. I’ve not written a massive number of tests yet, but I’ve not come across any non-deterministic behaviour so far.

What’s also worth noting here is that Xcode Cloud does run all these tests inside simulators of the device. If you want to test your apps on physical devices, you’ll still need your own device farm or to use something like BrowserStack’s App Live.

Vendor Tie-in

Perhaps this isn’t surprising from Apple, but the easy setup comes with the cost of heavy tie-in. There’s no way to import or export a pipeline from anywhere else. I have a feeling that it’s going to be a bit of an outlier in the future particularly as other providers move further towards pipelines, infrastructure and repository policies that are defined in code rather than using UI tools.

What about my server-side code?

Swift as a general purpose language is growing in popularity even outside of the Apple ecosystem. It’s being used to create server-side APIs in production by many companies around the world. Xcode Cloud is designed with Apple Platforms in mind but unfortunately, for the moment, this is limited to iOS, macOS, tvOS and watchOS apps. If you’re using something like Vapor to build your backend API, you’ll be greeted by this “No Apps or Frameworks” message. If you’re making use of (and paying for!) another CI/CD provider, then it’s probably harder to justify having two different solutions, but once Apple announce the pricing we’ll be able to know for sure.

Xcode window. The sidebar is set to show Cloud builds with a message reading "No Apps or Frameworks". The code editor shows a file called `route.swift` with a basic Hello World implementation.
A Hello World Vapor Swift App, Xcode Cloud builds are unavailable.

Summary

Xcode Cloud is probably going to work great for you if you’re new to automation or don’t have any CI/CD workflow setup for your project yet. Depending on the pricing, I’ll be happy to continue using it for my standalone side-projects. However, for me, it’s not quite compelling enough to invest time in switching over an existing pipeline from another provider.

In the future, I’d love to see some additions to the pre-installed software (SwiftLint and Fastlane in particular), access to the code in the post-testing step (see my SonarQube post) and support for server-side Swift projects.

How have you found Xcode Cloud? Are you going to adopt it? Let me know on Twitter:

Attempting SonarQube Analysis on Xcode Cloud

SonarQube can be a great tool for finding smells, bugs and duplications in your code. I like to use a combination of SonarQube and SwiftLint to enforce quality standards on the codebases I work on. These tools can help to ensure developers always meet the required standards in their code, and can reduce (or even prevent) the amount of bike-shedding on our merge requests.

Bike-shedding is where we spend a disproportionate amount of time discussing trivial things and leave important matters undiscussed. This is usually because the important items are more complex and we don’t spend the time to fully understand them, so we focus on the those that we can understand quickly. From a development perspective, I’ve seen intense debates on what the format for a pull request title should be, while the code itself is violating multiple SOLID principles.

Here I’ll be implementing SonarQube for a project I’ve written about before: my London Underground Status app. I’ll be using the Fastlane plugin for SonarQube and attempting to output a report on code quality and test coverage from an Xcode Cloud pull request validation build. I’m using SonarCloud for this, as it’s free for open source projects, but you can also use this for your own privately hosted SonarQube instances.

I won’t cover how to create an Xcode Cloud build, as others have covered that already and the Apple Documentation is fairly well written.

Prerequisites

Xcode Cloud agents are currently quite light on pre-installed software. They have Xcode, Homebrew, anything that comes pre-installed on macOS and that’s about it. To run Sonar Analysis, we’ll need to install three additional things on the build agent: fastlane, sonar-scanner and the fastlane plugin for converting Xcode’s test coverage output into JUnit format.

We can do this in our ci_pre_xcodebuild.sh script. As a single agent performs the build before handing off to multiple agents to run the tests in parallel, placing this in the ci_post_clone.sh won’t work as this is only run on the initial build agent.

#!/bin/sh
set -e

brew install fastlane
brew install sonar-scanner

fastlane add_plugin xcresult_to_junit

Main Branch Analysis

We can start by performing an analysis on a shared branch, i.e. develop or main. This will help us to understand the overall health of our code-base. I’ve used the Fastlane plugin for SonarQube as it was the easiest way to install and run SonarQube on the build agent.

We can call this directly, or create a Fastfile:

fastlane run sonar \
   project_key:"tube-status-ios" \
   project_name:"tube-status-ios" \
   project_version:"1.0" \
   project_language:"swift" \
   sonar_runner_args:"-Dsonar.projectBaseDir=$CI_WORKSPACE -Dsonar.c.file.suffixes=- -Dsonar.cpp.file.suffixes=- -Dsonar.objc.file.suffixes=- -Dsonar.pullrequest.provider=github" \
   sources_path:$CI_WORKSPACE \
   sonar_organization:"oliver-binns" \
   sonar_login:$SONAR_TOKEN \
   sonar_url:"https://sonarcloud.io" \
Coverage: 0.0% on 8.3k new lines. Greater than or equal to 80% coverage is required. Red status.
0.0% code covered by the unit test suite

As you can see, this analysis has failed, in part because we haven’t met the code-coverage requirements. When running a test action, Xcode Cloud will provide us with a $CI_RESULT_BUNDLE_PATH variable which we can use to provide coverage results to SonarQube. Fastlane has a plugin which lets us convert from the xcresult file that Xcode outputs into the JUnit format that SonarQube requires:

default_platform(:ios)

platform :ios do
  desc "Exports Test Coverage and Code Quality Analysis to SonarCloud"
  lane :sonar_analysis do |options|
    xcresult_to_junit(
      xcresult_path: options[:result_path],
      output_path: "#{options[:workspace]}/test_output"
    )
    sonar(
      project_key: "tube-status-ios",
      project_name: "tube-status-ios",
      project_version: "1.0",
      project_language: "swift",
      exclusions: "vendor",
      sonar_runner_args: "-Dsonar.projectBaseDir=#{options[:workspace]} -Dsonar.c.file.suffixes=- -Dsonar.cpp.file.suffixes=- -Dsonar.objc.file.suffixes=- -Dsonar.pullrequest.provider=github -Dsonar.junit.report_paths=#{options[:workspace]}/test_output",
      sources_path: options[:workspace],
      sonar_organization: "oliver-binns",
      sonar_login: options[:sonar_token],
      sonar_url: "https://sonarcloud.io",
    )
  end
end

Great, we can now see how much of our code is covered and where we can improve. For me and my Tube Status demo project, it seems there’s a long way to go!

SonarQube report. 2.1k lines of code. Version 1, last analysis 14 days ago. Commit ID 2d3252a4. Quality Gate: Failed. 1 Failed Condition. New Code, since about 1 month ago. Reliability: 0 bugs. Maintainability: 1 code smell. Security: 0 vulnerabilities. Security Review: 0 security hotspots. Coverage: 8.6% coverage on 499 new lines to cover. Duplications 0.0% duplications on 2.1k new lines.
SonarQube Report complete with test coverage metrics.

Pull Request Analysis

When performing a pull request analysis, we need to pass some additional parameters to SonarQube so that it can determine the differences between the two branches. Luckily, these are all parameters that Xcode Cloud provides us as Environment Variables: $CI_PULL_REQUEST_TARGET_BRANCH, $CI_PULL_REQUEST_SOURCE_BRANCH and $CI_PULL_REQUEST_NUMBER. We can pass these in, as above, and then consume them as additional parameters in our Fastlane file.

default_platform(:ios)

platform :ios do
  desc "Exports Test Coverage and Code Quality Analysis to SonarCloud"
  lane :sonar_analysis do |options|
    xcresult_to_junit(
      ...
    )
    sonar(
      project_key: "tube-status-ios",
      ...,
      pull_request_branch: options[:source_branch],
      pull_request_base: options[:target_branch],
      pull_request_key: options[:pr_number] 
    )
  end
end

When Xcode retrieves our code for a pull request, it creates a new repository and checks out the target branch. After this it merges in the source branch. The outcome of this is that the agent only has one branch: which has the same name as our target branch (likely develop or main). This is a problem as when we compare the difference against the target branch, we’ll find no changes.

If we just run the same script that we used for our main branch, with the additional parameters, we’ll get a very boring report:

SonarQube analysis results. 0 new lines. Quality Gate: Passed. Last analysis 7 minutes ago. Commit hash. Reliability: A rating, 0 bugs. Maintainability: A rating, 0 code smells. Security: A rating, 0 vulnerabilities. Security Review: A rating, 0 security hotspots.
A SonarQube report from Xcode Cloud showing no changes against the develop branch.

We have to do some Git magic to add the true target branch back into the refspec.

  1. Create a new branch, we can just call it temp.
  2. Delete the target branch so that we have no reference to it.
  3. Reset the refspec to be able to retrieve the true target branch again.
  4. Perform a git fetch to retrieve a clean reference to the remote target branch.
#!/bin/sh
set -e

git -C $CI_WORKSPACE checkout -b temp
git -C $CI_WORKSPACE branch -d $CI_PULL_REQUEST_TARGET_BRANCH

# fetch a reference to the develop branch on GitHub
# this will allow SonarQube analysis to work
git -C $CI_WORKSPACE config remote.origin.fetch \
"+refs/heads/$CI_PULL_REQUEST_SOURCE_BRANCH:refs/remotes/origin/$CI_PULL_REQUEST_SOURCE_BRANCH"
git -C $CI_WORKSPACE config remote.origin.fetch \
"+refs/heads/$CI_PULL_REQUEST_TARGET_BRANCH:refs/remotes/origin/$CI_PULL_REQUEST_TARGET_BRANCH"
git -C $CI_WORKSPACE fetch

That’s better, we can now run an analysis and changes will get detected: 47 new lines.

SonarQube report. 47 new lines. From branch feature/sonar-qube to develop. Last analysis 2 days ago, commit ID fbb4d929. 1 warning. Quality gate failed. 1 failed condition. Reliability: 0 bugs. Maintainability: 0 code smells. Security: 0 vulnerabilities. Security Review: 0 security hotspots. Coverage: 0.0% coverage on 26 new lines to cover. 0.0% estimated after merge. Duplications 0.0% duplications on 47 new lines. 1.3% estimated after merge.
A SonarQube report from Xcode Cloud showing 47 lines changed against the develop branch.

Summary

As you can see, it’s possible to get some of the features of SonarQube working on Xcode Cloud. Unfortunately, a lack of pre-installed software makes it slow to run, and a number of the implementation details make it tricky to implement. There no guarantee that this will be improved in the future, or that future changes won’t break the workarounds that we’ve implemented to get this to work. All in all, if you require SonarQube, I’d probably suggest steering clear of Xcode Cloud for the timebeing.

Checkout the final implementation on GitHub:

Find me on Twitter:

The text “GitFlow” alongside a git branching icon and a rocketship to represent deployment.

Gitflow with Automation for Mobile Apps

Gitflow Workflow

Gitflow is a great branching strategy for mobile apps. As mobile developers we can only publish one stable release through the App Store and Google Play Store, therefore we do not need to—nor are we able to—ship bug fixes for older intermediate versions of the software.

Automation

Automation is essential for achieving our agile principle of delivering working software frequently. Automated code-gates on our repository are like the brakes on our car, without them we cannot move faster without fear of crashing. Other parts of our automation, such as deployment, are more like the auto-pilot on an aeroplane; they do all the heavy lifting once a merge is complete.

Deliver working software frequently, from a couple of weeks to a couple of months, with a preference to the shorter timescale.

Principles behind the Agile Manifesto, agilemanifesto.org

Adapting Gitflow for Automation

One limitation I’ve found with Gitflow, is that it assumes fixes to a release will be committed directly to the release branch. However this does not allow us to code-review changes and use the same automated code-gates as with any of our other changes. I propose a new branch type for this purpose which we’ll call bugfix. I’ve seen arguments that a feature branch should be used for this, however I think that adds some ambiguity and defeats the aim of branch prefixes: identifying where this branch should eventually merge to.

Therefore the slight adaptation of GitFlow I’ve been using works like this:

  • main tracks the current live or pending App Review release
    • hotfix taken from and merged to main
  • release taken from develop, merged to main
    • bugfix taken from and merged to release
  • develop contains the latest and greatest changes to the codebase
    • feature taken from and merged to develop

Automating Code Gates

We can set up a whole automation pipeline around this approach where a number of gates, both manual and automated must be opened before a developer is allowed to merge code. There is a balance that needs to be struck to ensure quality without triggering developer frustration at a bar which is too high. I think this documentation from Google’s Engineering Practices does a great job at defining a compromise between perfection and progress for code review in particular, but the principles can also be applied to automated gates too.

There are a number of automated gates that you should consider adding to your branches. These vary from basic linting through to full automated end-to-end integration tests. Some of these are free, so you can get started straight away, but some come at a high price, so you’ll have to consider if they’re worth it for your project.

While some CI/CD providers, such as Bitrise, have a great setup process with drag and drop components, I recommend using Fastlane directly for setting up automation scripts because it can be executed on any of these and even locally as required.

Before a change…

Merging to a stable branch, such as develop, release/vX-X-X or main, should be done via a pull-request. This allows us to guarantee it remains stable.

As well as a code review, we can also run:

  • the linter for our language: SwiftLint for Swift, ktlint for Kotlin, etc. to enforce a uniform code-style
  • unit tests to catch regressions in low-level code behaviour
  • UI tests to catch regressions in whole user journeys

We can add additional tools and checks as required. I’ve had a lot of luck using SonarQube to enforce a high level of test coverage, no code duplication and catch code-smells and bugs. On solo projects, Amazon CodeGuru might be a good alternative to manual code review, or you could even use it to complement your existing manual review.

After a change…

After we merge a pull request, we should build a binary of our application. Depending on which branch we merge to this build can perform a different purpose. For develop merges, this can be used for internal validation within the team by designers, product owners and anyone else with an interest in the change. For release merges, these can be used for a full QA cycle including various forms of manual and automated testing. For main merges, this is the final build which can then be used for pre-deployment testing before being released to users. At this point we may also want to generate screenshots for our App Store listing and any metadata, including credentials for App Review.

A diagram showing GitFlow branching with automated gates at each merge step.
The full automated Continuous Integration & Deployment Pipeline

Tips & tricks

Small Changes:
As developers we should aim to ensure each of our changes are as small as possible. This helps reduce the burden of code-review on the other members of our team, but also reduces that chance that a bug sneaks into our code. Releasing small changes regularly can also reduce the confusion for users as they update to new versions of our app.

Feature Flags:
It can be hard to merge small discrete changes when working on larger features, but it’s possible using Feature Flags. These allow functionality to be enabled for testing but disabled in production. It’s also a great way to A/B test features in production and eventually triggering them on, with a fallback in place.

Get in touch.

What do you think? I’d love to hear how you approach branching and automation in your team or for your side-projects and whether you’ve found any of these processes useful in your work.