Making Impossible Builds Impossible

I recently worked on an interesting bug in an unfamiliar codebase. This was an iOS application with offline capabilities, so it required local storage. When the app loaded for the first time, it performed a sync to the cloud and populated the local storage. Unfortunately, the initial sync time had increased from a few seconds to multiple minutes, so we had to investigate.

The first time I opened that codebase, it took a while to get my bearings. Unfortunately, this codebase didn’t have any documentation, so there weren’t any bread crumbs to follow. I started blindly poking around and soon realized there was a SQLite file checked in to the codebase. That seemed promising.

Unfortunately, this file was in a format I’d never encountered before:

oid sha256:4cac19622fc3ada9c0fdeadb33f88f367b541f38b89102a3f1261ac81fd5bcb5
size 84977953

The file was checked into the repo with Git Large File Storage. After reading about Git LFS (which I’ll definitely use in the future), retrieving the file was a breeze.

Evidently, the person who last built the app and released it to the App Store hadn’t retrieved this file before starting the build, which caused the bug.

Prevent Reintroduction

This had me wondering how to prevent this issue from happening again. The previous developer didn’t realize they had to retrieve a file from Git LFS before performing a build. There was no documentation indicating they would need to do that. The compiler didn’t care that the SQLite file wasn’t in the proper format.

I could add some documentation and hope that the next person would think to check there before starting a build, but there’s no guarantee they would. Ideally, this could be fixed by building the app on CI/CD, where the build process would be consistent. This was a different class of bug.

Generally, after a bug arises, I like to write an automated test to prevent that bug from being re-introduced. However, in this case, I couldn’t think of a way to do that.

I started thinking of one of my favorite conference talks: Making Impossible States Impossible. When I first heard this talk, it was like a paradigm shift in my head. It completely changed the way I write code. I started thinking of ways I could apply this here: the build should fail without the database.

Since I was working in Xcode, I discovered it can run scripts as part of the build process. It was pretty easy to add a quick shell script to check if the file was still in the Git LFS format.

Now, if someone tries to run the build without retrieving the file, the build will fail, and they’ll get a helpful error message with instructions on how to retrieve the file. I also made sure to add some much-needed documentation with clear instructions.

Add a run script step to the build phase in Xcode.
Xcode makes adding build scripts easy!

Think About Future You

When you’re working on a new feature (or even maintaining an old one), you have more context than you will months or years down the road. Moreover, another person might end up maintaining your feature and have even less context.

My memory isn’t great, so I need to write things down, and I err on the side of too much in my documentation. But documentation isn’t everything. Documentation falls out of date when people inevitably forget to update it.

It’s important to lock these behaviors in builds and partial scripts. Heck, let’s all just commit to embrace bringing back the Makefile.