🔀 How to untangle and manage build distribution — Webinar, May 9th — Register
🔀 How to untangle and manage build distribution — Webinar, May 9th — Register

How to build a custom fastlane action — and share it with the world as a fastlane plugin

fastlane's virtues have been praised here on the blog over and over again. It's such a fantastic tool for automating everything from running tests, to capturing screenshots, to building and distributing your apps to the App Store and Play Store. If you’ve dabbled in fastlane before, you’ll know that fastlane scripts are built up through the use of actions which are combined into methods called lanes. Lanes are basically functions that can include pure Ruby code, calls to existing fastlane actions, or a mix of both. You build up lanes in your Fastfile to accomplish specific tasks, and they can be invoked directly from the command line.

fastlane comes with a bevy of pre-built actions out-the-box, but you can also make your own! Plus, in addition to building custom fastlane actions for your own internal use, you can share them with the world by publishing them as fastlane plugins. Plugins can be used by anyone in the fastlane ecosystem. Building custom fastlane actions can come in handy for a few different reasons: fastlane actions provide an easy way to share code between multiple repositories, which can be a way to standardize the process of building, testing, and distributing your apps. Or maybe you work on a product where there’s a piece of automation that could help your customers — Sentry (a popular stability monitoring tool) for example built an action to help automate the upload of dSYM files to their servers. There are potentially many different use cases for building custom fastlane actions.

In this post we are going to look at how to build an action using a lane that we wrote about in a recent post on automating away the pain of code signing. We'll adapt this lane to be its own action, and then look at how we might go about publishing it as a fastlane plugin. By the end, you’ll be well-equipped to evaluate areas of overlap in your own Fastfiles, and refactor them into actions of their own.

Let's dive in!

Anatomy of a fastlane action

fastlane gives you a command to create the shell of your new action: <code>fastlane new_action<code>. For our action I'm going to call it <code>rebuild_signing<code> (which mirrors what the prior lane was called). We'll get a new file at <code>fastlane/actions/rebuild_signing.rb<code> with the action code.

The file we're given is written in Ruby (like the rest of fastlane and your fastfile) so hopefully the syntax isn't too foreign. Let's take a look at the boilerplate that's given to us:


#0
module Fastlane
  module Actions
    # 1
    module SharedValues
	    CUSTOM_VALUE = :CUSTOM_VALUE
    end

    class RebuildSigningAction < Action
      def self.run(params)
      # 2
      end

      #####################################################
      # @!group Documentation
      #####################################################

      def self.description
			# 3
      end

      def self.details
        # 4
      end

      def self.available_options
        # 5
        []
      end

      def self.output
        # 6
        []
      end

      def self.return_value
        # 7
      end

      def self.authors
        # 8
        []
      end

      def self.is_supported?(platform)
        # 9
        platform == :ios
      end
    end
  end
end

0. This class that we get becomes part of the <code>Fastlane.Actions<code> namespace. This stays as it is.

1. <code>SharedValues<code> works together with #6 to define the output — if any — of your action. This is how your action could interact with other actions, or how a lane using your action would work with your action's output. It is not necessary to have any output (and our action will not). The format of these is as a key-value pair where the left side is the output from your action and the right side is the key used to access the shared values within the lane context.

2. The <code>run<code> method is part of the <code>Action<code> superclass that we are subclassing. It's the meat of what the action will do. Any output we define in items 1 & 6 will be filled in by this method.

3. Now we get into documentation. This little block is a short (80 characters or less) blurb about what the action does.

4. This is where we can expand documentation with additional details about what the action does.

5. <code>available_options<code> lets you define inputs for your action.  Inputs can be passed in to your action via arguments or environment variables — and thankfully fastlane takes care of mapping these values to the action so you're not left with code paths that check both arguments and environment variables for inputs. There are a lot of properties that can be sent into an option (which are all very helpfully documented here).There's some really good stuff in here, like value validation and default values.

6. This output array is what is prettily formatted by fastlane at the beginning of your action running (if you've used match, gym, scan, or any of the standard tools, you've seen these). Each value in the array is a 2-member tuple of strings with the first member being a key containing an exported shared value (like we defined in #1) and the value is a description for what that value is.

7. If your action is going to return a value then this is where that is described.

8. The <code>authors<code> block is where you take credit for your creation 😀

9. Last but not least, the platform block lets you run logic that tells fastlane what platforms your action will support (if it is platform-bound). 

Filling out the fastlane action

Full code of our action is available in this gist.

Now that we've seen what the shell of an action looks like, let's add some functionality to it. Our custom action is pretty simple and won't need to have any return values. Let's first take a look at what an input to the action will look like.

FastlaneCore::ConfigItem.new(
  key: :ios_bundle_ids,
  env_name: 'FL_IOS_BUNDLE_IDS',
  description: 'An array of strings which are the iOS bundle IDs to re-provision. These should be passed in space-separated as in a shell string that is parsed in to an array',
  type: Array
)

Here, our input is an array of bundle identifiers which will get the renewed provisioning profiles. fastlane will look for the input from the method call directly and then look to the <code>FL_IOS_BUNDLE_IDS<code> environment variable. If neither of those are present, an error will be thrown and the action will exit.

There's also an optional <code>verify_block<code> parameter to a new config item which lets you validate the input passed in by a user. This is helpful if your action is going to take in something like a number in a given range and you need to verify the input is in that range.

Running the fastlane action

Every action subclass has a <code>run<code> method attached that fastlane uses to do its work. There is a single argument to the method of <code>params<code>, which is a key-value pair hash of all the config items defined in the <code>self.available_options<code> array. Accessing a config item is done by using the key for a given item as a subscript. So, to get at our array of bundle IDs we'll use <code>params[:ios_bundle_ids]<code> (the leading <code>:<code> is Ruby syntax).

Inside the action’s <code>run<code> method we can do lots of different things. We can execute shell commands using <code>sh<code>. We can use the <code>UI<code> class methods to communicate information, warnings, and errors to the user. And we can call other fastlane actions from our action's <code>run<code> method. To call a fastlane action, we need to prefix the action name with <code>other_action<code> – literally. For example, <code>other_action.match<code> calls the <code>match<code> action.

We can even import other gems into our actions to do what we need. It really is super flexible.

Getting the word out: packaging your action up into a fastlane plugin

The action we created lives inside of our local fastlane directory and is picked up automatically. But what if we wanted to distribute this action to others? There's a way to do that too! Actions can be packaged up as fastlane Plugins. They are then distributed as Ruby gems and imported as plugins in to fastlane via the <code>Pluginfile<code> using the same <code>gem<code> syntax as the <code>Gemfile<code>.

There's one tweak to make in the <code>Gemfile<code> so that it looks for the <code>Pluginfile<code>:

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

This supplies the path to your <code>Pluginfile<code> and tells <code>bundler<code> to evaluate that file if it exists. Once this is done, your <code>bundle install<code> command will pick up the plugins as gems and you're in business.

Lights, camera, action!

Having the ability to build out your own custom actions is undoubtedly a very powerful feature of fastlane — but it can come with some difficulties. Debugging custom actions can be tricky, documentation hard to come by, and the syntax to call other actions from your action is easy to mis-remember (like forgetting to call <code>resume<code> on a <code>URLSessionDataTask<code> 😂).

But getting custom actions in place can also prove incredibly powerful. They are easy to share, so if you work with multiple repositories, sharing functionality across projects becomes simpler. The validation on action input can be pretty useful for making sure that you're working with correct values. And being able to make your actions available to the world as plugins (and take advantage of plugins others have written!) certainly makes the fastlane ecosystem more dynamic and accessible. If there’s some piece of functionality you need from fastlane that doesn’t exist, there’s a good chance that someone out there has already created a plugin for it.

Hopefully this post has helped demystify fastlane actions a bit and has even gotten your wheels turning and thinking about what custom actions you may want to build into your project.

Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?
Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.
Try our free tool ->
Sign up for the Flight Deck — our monthly newsletter.
We'll share our perspectives on the mobile landscape, peeks into how other mobile teams and developers get things done, technical guides to optimizing your app for performance, and more. (See a recent issue here)
The App Store Connect API is very powerful, but it can quickly become a time sink.
Runway offers a lot of the functionality you might be looking for — and more — outofthebox and maintenancefree.
Learn more
App Development

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 release to rollout. No more cat-herding, spreadsheets, or steady drip of manual busywork.

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 release to rollout. No more cat-herding, spreadsheets, or steady drip of manual busywork.

Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?

Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.

Looking for a better way to distribute all your different flavors of builds, from one-offs to nightlies to RCs?

Give Build Distro a try! Sign up for Runway and see it in action for yourself.

Release better with Runway.

What if you could get the functionality you're looking for, without needing to use the ASC API at all? Runway offers you this — and more — right out-of-the-box, with no maintenance required.

How to build a custom fastlane action — and share it with the world as a fastlane plugin

fastlane's virtues have been praised here on the blog over and over again. It's such a fantastic tool for automating everything from running tests, to capturing screenshots, to building and distributing your apps to the App Store and Play Store. If you’ve dabbled in fastlane before, you’ll know that fastlane scripts are built up through the use of actions which are combined into methods called lanes. Lanes are basically functions that can include pure Ruby code, calls to existing fastlane actions, or a mix of both. You build up lanes in your Fastfile to accomplish specific tasks, and they can be invoked directly from the command line.

fastlane comes with a bevy of pre-built actions out-the-box, but you can also make your own! Plus, in addition to building custom fastlane actions for your own internal use, you can share them with the world by publishing them as fastlane plugins. Plugins can be used by anyone in the fastlane ecosystem. Building custom fastlane actions can come in handy for a few different reasons: fastlane actions provide an easy way to share code between multiple repositories, which can be a way to standardize the process of building, testing, and distributing your apps. Or maybe you work on a product where there’s a piece of automation that could help your customers — Sentry (a popular stability monitoring tool) for example built an action to help automate the upload of dSYM files to their servers. There are potentially many different use cases for building custom fastlane actions.

In this post we are going to look at how to build an action using a lane that we wrote about in a recent post on automating away the pain of code signing. We'll adapt this lane to be its own action, and then look at how we might go about publishing it as a fastlane plugin. By the end, you’ll be well-equipped to evaluate areas of overlap in your own Fastfiles, and refactor them into actions of their own.

Let's dive in!

Anatomy of a fastlane action

fastlane gives you a command to create the shell of your new action: <code>fastlane new_action<code>. For our action I'm going to call it <code>rebuild_signing<code> (which mirrors what the prior lane was called). We'll get a new file at <code>fastlane/actions/rebuild_signing.rb<code> with the action code.

The file we're given is written in Ruby (like the rest of fastlane and your fastfile) so hopefully the syntax isn't too foreign. Let's take a look at the boilerplate that's given to us:


#0
module Fastlane
  module Actions
    # 1
    module SharedValues
	    CUSTOM_VALUE = :CUSTOM_VALUE
    end

    class RebuildSigningAction < Action
      def self.run(params)
      # 2
      end

      #####################################################
      # @!group Documentation
      #####################################################

      def self.description
			# 3
      end

      def self.details
        # 4
      end

      def self.available_options
        # 5
        []
      end

      def self.output
        # 6
        []
      end

      def self.return_value
        # 7
      end

      def self.authors
        # 8
        []
      end

      def self.is_supported?(platform)
        # 9
        platform == :ios
      end
    end
  end
end

0. This class that we get becomes part of the <code>Fastlane.Actions<code> namespace. This stays as it is.

1. <code>SharedValues<code> works together with #6 to define the output — if any — of your action. This is how your action could interact with other actions, or how a lane using your action would work with your action's output. It is not necessary to have any output (and our action will not). The format of these is as a key-value pair where the left side is the output from your action and the right side is the key used to access the shared values within the lane context.

2. The <code>run<code> method is part of the <code>Action<code> superclass that we are subclassing. It's the meat of what the action will do. Any output we define in items 1 & 6 will be filled in by this method.

3. Now we get into documentation. This little block is a short (80 characters or less) blurb about what the action does.

4. This is where we can expand documentation with additional details about what the action does.

5. <code>available_options<code> lets you define inputs for your action.  Inputs can be passed in to your action via arguments or environment variables — and thankfully fastlane takes care of mapping these values to the action so you're not left with code paths that check both arguments and environment variables for inputs. There are a lot of properties that can be sent into an option (which are all very helpfully documented here).There's some really good stuff in here, like value validation and default values.

6. This output array is what is prettily formatted by fastlane at the beginning of your action running (if you've used match, gym, scan, or any of the standard tools, you've seen these). Each value in the array is a 2-member tuple of strings with the first member being a key containing an exported shared value (like we defined in #1) and the value is a description for what that value is.

7. If your action is going to return a value then this is where that is described.

8. The <code>authors<code> block is where you take credit for your creation 😀

9. Last but not least, the platform block lets you run logic that tells fastlane what platforms your action will support (if it is platform-bound). 

Filling out the fastlane action

Full code of our action is available in this gist.

Now that we've seen what the shell of an action looks like, let's add some functionality to it. Our custom action is pretty simple and won't need to have any return values. Let's first take a look at what an input to the action will look like.

FastlaneCore::ConfigItem.new(
  key: :ios_bundle_ids,
  env_name: 'FL_IOS_BUNDLE_IDS',
  description: 'An array of strings which are the iOS bundle IDs to re-provision. These should be passed in space-separated as in a shell string that is parsed in to an array',
  type: Array
)

Here, our input is an array of bundle identifiers which will get the renewed provisioning profiles. fastlane will look for the input from the method call directly and then look to the <code>FL_IOS_BUNDLE_IDS<code> environment variable. If neither of those are present, an error will be thrown and the action will exit.

There's also an optional <code>verify_block<code> parameter to a new config item which lets you validate the input passed in by a user. This is helpful if your action is going to take in something like a number in a given range and you need to verify the input is in that range.

Running the fastlane action

Every action subclass has a <code>run<code> method attached that fastlane uses to do its work. There is a single argument to the method of <code>params<code>, which is a key-value pair hash of all the config items defined in the <code>self.available_options<code> array. Accessing a config item is done by using the key for a given item as a subscript. So, to get at our array of bundle IDs we'll use <code>params[:ios_bundle_ids]<code> (the leading <code>:<code> is Ruby syntax).

Inside the action’s <code>run<code> method we can do lots of different things. We can execute shell commands using <code>sh<code>. We can use the <code>UI<code> class methods to communicate information, warnings, and errors to the user. And we can call other fastlane actions from our action's <code>run<code> method. To call a fastlane action, we need to prefix the action name with <code>other_action<code> – literally. For example, <code>other_action.match<code> calls the <code>match<code> action.

We can even import other gems into our actions to do what we need. It really is super flexible.

Getting the word out: packaging your action up into a fastlane plugin

The action we created lives inside of our local fastlane directory and is picked up automatically. But what if we wanted to distribute this action to others? There's a way to do that too! Actions can be packaged up as fastlane Plugins. They are then distributed as Ruby gems and imported as plugins in to fastlane via the <code>Pluginfile<code> using the same <code>gem<code> syntax as the <code>Gemfile<code>.

There's one tweak to make in the <code>Gemfile<code> so that it looks for the <code>Pluginfile<code>:

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

This supplies the path to your <code>Pluginfile<code> and tells <code>bundler<code> to evaluate that file if it exists. Once this is done, your <code>bundle install<code> command will pick up the plugins as gems and you're in business.

Lights, camera, action!

Having the ability to build out your own custom actions is undoubtedly a very powerful feature of fastlane — but it can come with some difficulties. Debugging custom actions can be tricky, documentation hard to come by, and the syntax to call other actions from your action is easy to mis-remember (like forgetting to call <code>resume<code> on a <code>URLSessionDataTask<code> 😂).

But getting custom actions in place can also prove incredibly powerful. They are easy to share, so if you work with multiple repositories, sharing functionality across projects becomes simpler. The validation on action input can be pretty useful for making sure that you're working with correct values. And being able to make your actions available to the world as plugins (and take advantage of plugins others have written!) certainly makes the fastlane ecosystem more dynamic and accessible. If there’s some piece of functionality you need from fastlane that doesn’t exist, there’s a good chance that someone out there has already created a plugin for it.

Hopefully this post has helped demystify fastlane actions a bit and has even gotten your wheels turning and thinking about what custom actions you may want to build into your project.

Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?
Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.
Try our free tool ->
Sign up for the Flight Deck — our monthly newsletter.
We'll share our perspectives on the mobile landscape, peeks into how other mobile teams and developers get things done, technical guides to optimizing your app for performance, and more. (See a recent issue here)
The App Store Connect API is very powerful, but it can quickly become a time sink.
Runway offers a lot of the functionality you might be looking for — and more — outofthebox and maintenancefree.
Learn more