Secret variables in Xcode AND your CI for fun and profit 💌

Let’s preface this article with one tiny fact:

You should never have any sensitive information in your code repository.

There are no exceptions. API Keys, passwords, your email (including things like an integration test). It’s really bad practice, and yet almost everyone does it. Even I still have an email or something lying around on a repo at work. It’s such a rampant problem that GitHub has an official guide to remove sensitive info from a repo. There’s also 300k+ commits on GitHub on removing passwords alone (which is ineffectual but ¯\_(ツ)_/¯). It’s such a rampant issue, that there’s tons of famous open source projects dedicated to solving this problem.

Now that we’ve outlined the problem, we need to devise a solution that’s both secure and easy. I think it’s as important to make The Wrong Thing Hard & The Right Thing Easy.

This would let developers (that’s you and me 👋) use secret keys for things like analytics, libraries, Integration Tests etc. And all of this without breaking our workflows (we’d still like to be able to hit ⌘+R/B/U, and to use existing workflows.)

Inspiration

Turns out, commercial CI providers have decent solutions to this problem (💯 props to the amazing folks at BuddyBuild for thinking of this 🙌)

Huh, sounds interesting. Let’s try building this out on our own 🤔.

We need to do the following:

  1. Get these variables from a secure source, both on our dev machines as well as on the CI.
  2. Inject them into Xcode (with code gen being one of the options).
  3. Use these variables in our code as usual. Nothing else should (ideally) be changed in our workflow/build pipeline.

Implementation

My first crack at this involved using Xcode’s own environment variables, but sadly those have to be static and stored as part of your `.xcscheme`(which defeats the purpose of this article) 😢

Most CI providers have a way to store encrypted variables (buddybuild: and travis) that are part of the parent process’ environment variables.

Two questions though:

  • How do you use these locally 🤔?
  • How do you access environment variables in Xcode?

To figure this out, I looked at the setups of my colleagues (who usually work from the CLI and not from an IDE).

Their local setups usually involve a bash script, called `env-vars.sh` (that’s in their .gitignore). It looks something like this:

The dev then `source`(imports any variables that are exported in the script) like so:

before running tests. On the CI, these variables are already provided, and so `env-vars.sh` isn’t even needed (which is why it’s in the gitignore — so plain text information never reaches the repo).

Okay, awesome 👏. Now, how do you get this working in Xcode? Turns out, there’s one more piece left in the puzzle. Inspired from the buddybuild example, we can extract those environment variables -> Compile them into a Swift file using code generation.

There’s a bunch of solutions for codegen in Swift, but Sourcery by far seems to be the most supported. It also lets you pass in arguments, so it’s perfect for us 🙌

Now, the question arises: how do you run arbitrary scripts as part of your build process?

Luckily, Xcode has a feature called `Build Phases` that lets us do so.

Awesome. We can now add a build phase (a `Run script`) to our build phases. Note that you need to add this just before the `Compile Sources` step. This is because we need to add a new `.swift` file to compile and source our environment script (if it exists) and run Sourcery.

We need to add a `.stencil` template file to our project and provide Sourcery it’s parent directory so it can do its magic ✨

I’m not going to go over the details of how Sourcery works, because that’s beyond the scope of this article.

Here’s what the build script looks like:

Here’s the script (for ⌘+C’ing):

Now, run ⌘+B once so that a source file is generated. (Don’t worry if your build fails. It’s because we haven’t added the generated file yet). It’ll have the extension `.generated.swift`. Add it to your project and now you’ll be able to access these variables from inside your source code 🚀.

PS: Do note that you’ll have to add `.generated.swift` to your `.gitignore` so your credentials won’t accidentally end up in your repo 😅.

Now, you can use the `testCreds` struct we declared earlier, seamlessly and easily without having to worry about the implementation details in your CI or dev machine 🎉. And your current workflow is unaffected (⌘+R/B/U still works 🎊)

Thanks for reading my article! If you have any questions, feel free to contact me on twitter (I’m @codeOfRobin at most places) or ask over Medium comments.

PPS: I’m going to be working on some really cool stuff with Abheyraj Singh soon. Stay tuned!