Case Study: Contributing to Fastlane

by Lou Franco

submit to r/iOSProgramming
Photo by Tom Quandt on Unsplash

I came into work Monday to see our builds failing with a problem that was unconnected to anything we merged recently. Reading the logs, I could see that fastlane's gym was claiming that our scheme was not found in the workspace, but I knew that it was definitely there.

Note for non-iOS devs: we use Xcode to manage workspace, project, and scheme files, which are just piles of undocumented XML. fastlane is an open-source project that makes it easy to script builds, tests, and a lot of other stuff. In order for fastlane to understand a workspace/project/scheme, the maintainers needed to reverse engineer the format and build parsers.

fastlane is an incredibly fast moving project. Since they have to track (and reverse engineer) undocumented Apple things like project file formats and the iTunesConnect API, it's important to adopt their latest version pretty aggressively. So our build machine's package manager is set to download the latest release, which keeps us up to date, but also exposes us to bugs.

I saw that fastlane had released a new version that day, so I tried reverting to the one right before. That did "fix" the issue, but pinning fastlane is not a good option. I did it temporarily to get builds, but I needed a plan to get the problem fixed.

I've written that ruby is important for iOS developers to know. My reasoning was that it was used for a lot of significant iOS tooling and if you rely on open-source projects, you need to be ready to fix your own problems.

Now, I don't know a lot of ruby. I probably couldn't make a real thing from scratch with it. But I do know enough to debug and fix small bugs.

Here was my process:

You'll see that each step wasn't really that hard and you can use this is a guide the next time you run into an issue with an open-source tool you rely on.

Step 1: Read the commits

Since I knew that the issue was introduced in the latest version, there was a small set of commits to look over. One of them was to swap out bespoke Xcode project file parsing code for Cocoapod's Xcodeproj project. This makes sense, but this commit is a good candidate for introducing my problem, which was not getting a full list of schemes from the workspace.

Step 2: Do a little debugging

I really wanted to write an issue at this point, but I still did not have any idea of what was going on. Xcodeproj is a mature project and used in cocoapods, so it's not likely that this bug was for a common case. If you want your issue to be taken seriously, you should do as much as you can to narrow down the problem and describe it well. Open-source projects get tons of issues opened by people that don't do enough to make the issue easy to deal with--you can stick out by doing more (which will get your issue fixed faster).

The commit above showed this new code:

def workspace
  return nil unless workspace?
  @workspace ||= Xcodeproj::Workspace.new_from_xcworkspace(path)
end

def schemes
  @schemes ||= if workspace?
                 workspace.schemes.reject do |k, v|
                   v.include?("Pods/Pods.xcodeproj")
                 end.keys
               else
                 Xcodeproj::Project.schemes(path)
               end
end

So, if the scheme list is not complete, it really looks like the issue is with Xcodeproj. That hypothesis is easy to test, I wrote this simple script

require 'xcodeproj'
project_path = './Trellis.xcworkspace/'
workspace = Xcodeproj::Workspace.new_from_xcworkspace(project_path)
workspace.load_schemes(project_path)
print workspace.schemes

And ran it in my project's root.

It had the same problem as fastlane, which was a good sign. I read load_schemes and saw that it looks for *.xcscheme files in project directories. I did a find and saw that I had xcscheme files under the workspace folder as well. load_schemes doesn't look in that folder, though, so that was the problem.

Step 3: Write up an Issue

At this point, I could describe exactly what was going wrong and recommend a course of action, so I wrote an issue. I was still going to do more, but the point of this was to see if anyone else had a workaround or was experiencing the same thing. Many people search issues when they have a problem, so I made sure to express the core problem in the issue title so it would be easy to find.

A couple of other devs did say they had the same issue, so I tried to help them diagnose based on my findings.

Step 4: Make a simple test case

Since I knew exactly what was going on, I could easily create a workspace that reproduced the issue. I did that and attached it to the issue. I also created an issue on Xcodeproj and attached the workspace to that one as well.

Step 5: Try to fix it

The bug was really in Xcodeproj, but the fix there was not obvious to me. I decided to work-around the issue in fastlane. The fix is pretty simple--I wrote a function to read the workspace schemes:

def workspace_contained_schemes
  Dir[File.join(path, 'xcshareddata', 'xcschemes', '*.xcscheme')].map do |scheme|
    File.basename(scheme, '.xcscheme')
  end
end

And appended this list to the one created in schemes.

Step 6: Read fastlane's CONTRIBUTING.md

At this point, I was sure I was going to make a PR, so I read their contribution guidelines. I learned

  1. I'd have to get a CLA signed by my job
  2. How to make my local fastlane used in my iOS project (for testing)
  3. How to run the tests
  4. Branch and commit naming guidelines

Step 7: Make a new spec test

Luckily I had made a simple test case workspace, so I added that to fastlane's fixtures and added a simple spec-based test to make sure the new code read the workspace schemes. I actually found a bug while doing this, so I fixed that (the code in Step 5 is the fixed version).

Step 8: Make a PR to fastlane

The contribution guideline has a template for the PR message, which is also pre-populated when you open the PR. I made sure to follow the format.

Step 9: Start the CLA signing process with my job

I had to wait here, because the CLA is only sent to you after you open the PR. I then had to go through the steps at my job for contributing to an open-source project. Luckily, we were already signatories to the CLA, so I just needed to get myself added to it.

Step 10: Make a PR to Xcodeproj

By this time, one of the maintainers on Xcodeproj answered my question on what to do about the issue, so I made the PR to load workspace schemes there too. Xcodeproj has a simpler process for contributing, but I did also add the simple workspace to their fixtures and specs.

Step 11: Shepherd the PRs until they are merged

Both of these projects have automatic tests and bots responding to PRs, so I just made sure they were satisfied. I got a suggestion from a maintainer on the fastlane PR, so I addressed that.

See? Simple.

It does seem like a lot of steps, but the only hard part was debugging (did I mention how much of a ruby newb I am?). fastlane has great contributor documentation that helps getting a local development environment set up quickly. Their spec tests and code-style checker (rubocop) helped me fix issues before committing.

submit to r/iOSProgramming

Never miss a post

Get more tips like this in your inbox