App Development

Better fastlane with environments

fastlane is awesome. It can save oodles of time on those repetitive tasks that tend to fill your weekly iOS routine: making builds for TestFlight and the App Store, running unit tests, and capturing marketing screenshots. If fastlane is new to you, be sure to check out this primer on the Runway blog to get started.

If you’re a small team working with one or two configurations of a single app, you’ll likely have a single Fastfile that uses a handful of basic, out-of-the-box fastlane <code>lanes<code>. But as your team grows and your needs expand beyond the basic setup to include enterprise test apps, multiple platforms, and even more build configurations, you’ll quickly find that managing lanes in a single (or multiple!) Fastfiles can quickly become unwieldy, verbose, and difficult to maintain.

That’s where fastlane environments come in. Fastlane environments provide a way to customize our fastlane lanes to work with many different configurations and allow for easily swapping between them all. This can be a really helpful technique if you find that your Fastfiles have lanes that do basically the same things, just with some values changed for the particular app or configuration that the lane is handling. fastlane environments can help you narrow down the number of lanes you have to manage and keep those lanes flexible to work with any of your apps.

Taking advantage of fastlane environments is a great way to reduce code duplication in your setup, and it will make your fastlane scripting more understandable, extensible, and robust as your team’s needs grow.

Typical fastlane configuration via *files

When we <code>init<code> on fastlane or actions such as match or gym we usually get some Ruby-based configuration file (Appfile, Snapfile, Matchfile, etc.) produced as a result. These files are one place to define configurations for the actions that are later called by our lanes. For example, in our earlier fastlane post we defined the app’s identifier in the <code>Appfile<code> and used that in a lane as follows:

app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)

It's great that we have the ability to use this to get at our app's identifier! But what if we didn't need to have bespoke <code>*file<code> files for each use case and could instead use different configuration files that can be swapped in as needed?

Enter fastlane environments

It turns out that we can do just this! Using environment configuration files we can unleash extra flexibility by declaring variables that fastlane can inject into the environment environment using <code>dotenv<code>, a package that lets us use key-value pairs to customize what our lanes do. Here's an example:

APP_IDENTIFIER = com.my-app.myapp
APP_NAME = MyApp

In our Fastfile we can access the environment using the <code>ENV<code> variable, which is a dictionary available to all of our lanes. So in this example, to read the app's identifier, we can call <code>ENV["APP_IDENTIFIER"]<code> in our lanes and use it how we like. This is great, but it gets even better.

We store these key-value pairs in files next to our Fastfile. The standard file is simply called <code>.env<code> and fastlane will load it for you automatically (you could also name it <code>.env.default<code> and fastlane will still pick it up). But you can also define your own custom dotenv files and import them as part of your lanes!

// .env.mac
MATCH_PLATFORM = macos
DELIVER_PLATFORM = osx

// .env.ios
MATCH_PLATFORM = ios
DELIVER_PLATFORM = ios


// Fastfile:
platform :ios do
  before_all do
    Dotenv.load ".env.ios"
  end

  lane: beta do
    match
    deliver
  end
end

platform :mac do
  before_all do
    Dotenv.load ".env.mac"
  end

  lane: beta do
    match
    deliver
  end
end

What we have here are two separate environment files (<code>.env.ios<code>, and <code>.env.mac<code>) which are loaded at the beginning of any lane for their respective platform. In the case of the <code>match<code> and <code>deliver<code> actions above they will always be properly configured for their correct platforms. This is super cool!

There are two main functions that we use when loading these environment files: <code>Dotenv.load<code> and <code>Dotenv.overload<code>. The difference between the two is that <code>overload<code> will overwrite any existing values in the environment from the files being loaded. <code>load<code> is purely additive; it does not overwrite any values. So this means that you could have some base value inside of the <code>.env<code> file and then overwrite that with a value later if you want to.

Next-level fastlane with environments

Let's play this out a little further. We can now see how using environments will let us swap out values at runtime and allow us to build generic lanes that can be customized by their environment. Let’s say that we have an app which has different builds for beta and releases, but those processes are very similar. Here’s what those lanes could look like (and notice all the overlap in the two).

ruby
lane :beta do
    match(app_identifier: "com.myapp.ios.beta")
    gym(
    	project: "MyProject.xcodeproj", 
    	scheme: "MyApp-Beta"
    )
    pilot(app_identifier: "com.myapp.ios.beta")
end


lane :release do
    match(app_identifier: "com.myapp.ios.release")
    gym(
    	project: "MyProject.xcodeproj", 
    	scheme: "MyApp-Release"
    )
    pilot(app_identifier: "com.myapp.ios.release")
    deliver(app_identifier: "com.myapp.ios.release")
end

Now let’s extract out all the variables to environment files and see how this lets us reduce down to a single lane. To detect if we are building for release all we have to do is check if the variables for the <code>deliver<code> action are set or not (since they’ll never be set in a beta release).

// .env.beta
MATCH_APP_IDENTIFIER = com.myapp.ios.beta
GYM_PROJECT = MyProject.xcodeproj
GYM_SCHEME = MyApp-Beta
PILOT_APP_IDENTIFIER = com.myapp.ios.beta
DELIVER_APP_IDENTIFIER = com.myapp.ios.beta

// .env.release
MATCH_APP_IDENTIFIER = com.myapp.ios.release
GYM_PROJECT = MyProject.xcodeproj
GYM_SCHEME = MyApp-Release
PILOT_APP_IDENTIFIER = com.myapp.ios.release
DELIVER_APP_IDENTIFIER = com.myapp.ios.release
// Fastfile
lane :build_binary do
    match
    gym
    upload_to_testflight
    if ENV["DELIVER_APP_IDENTIFIER"]
    	deliver
    end
end

To build our lane with a given release we’ll use this invocation:

bundle exec fastlane build_binary –env beta

(It's highly recommended that fastlane is installed via <code>bundler<code>, which is where <code>bundle exec<code> comes in at the start. Otherwise, if fastlane is in your <code>$PATH<code>, you can drop that.)

The key part here is the <code>--env beta<code>. This tells fastlane what environments to load, and they can stack. So it could be possible to have shared files for our app's base environment values, but also separate files for our app's macOS and iOS specific versions (<code>--env myapp.ios<code> or <code>--env myapp.mac<code> for example).

The end state here is that our Fastfile doesn't know anything in particular about our apps. That's all controlled by the environments that fastlane is told about. This makes our existing fastlane functionality endlessly extensible — adding new apps to our pipeline is as simple as creating a new <code>.env<code> file for each new app and filling in the necessary values. Beyond that, extracting all the app and configuration specific code from the lanes naturally makes their logic flow easier to read and reason about and, as a result, everything becomes more maintainable for engineers on the team.

fastlane environment gotchas

Nothing is perfect, and this technique for using fastlane environments is no exception. There are a few things to keep in mind when using environments:

  • It's possible that some actions may not always read the argument you're trying to pass in from the environment*. This is the case with snapshot's <code>devices<code> and <code>languages<code> arguments discussion about that topic. If you're struggling with missing environment variables then check out the docs for that particular action and don't be afraid to go spelunking for the source code if need be.
    <smallen>*It's helpful to read up on the docs for how fastlane processes action arguments.<smallen>
  • Environment files can't contain source code, and values can't reference other values. This will cause failures in really weird ways such as entire environment files failing to import. Programmers like their code DRY, but sometimes out in the real world things get a bit wet.

Set up for automation success using fastlane environments

We’ve seen how fastlane environments can be a powerful tool to take advantage of, helping keep your Fastfiles generic and easier to maintain, and allowing you to nimbly swap out different configurations as needed. By leveraging fastlane environments to keep your setup reusable, maintainable, and expandable, you can set your team up for success as its needs for automation using fastlane continue to grow.

Cheers!

P.S. For further reading about fastlane environments, check out their documentation.

Release better with Runway.

Runway integrates with all the tools you’re already using to level-up your release coordination and automation, from kickoff to submission to release. No more cat-herding, spreadsheets, or steady drip of manual busywork.
request a demo