Bitrise.io is a Continuous Integration and Delivery (CI/CD) Platform as a Service (PaaS) with the main focus towards mobile app development. Bitrise provides ready to use integrations with popular, widely used tools, like Gradle, Xamarin or Xcode. However, integrations for niche and/or brand new tools may not be available out of the box immediately. Usually, the fastest solution is to write an ad-hoc shell script. However, these scripts are also the hardest to reuse in multiple projects.
Moreover, shell scripts are often not suitable for complex actions. Is there a better way? Of course! In this article, we will show you how to create your own Bitrise step and how to publish it, so everyone can make use of it.
Why another article?
Instructions involving creating steps are available on the Bitrise DevCenter as well as a dedicated (guest) blogpost. Nevertheless, in this article, we will focus on programming in Go, which is the main language used by Bitrise. It is also preferred for non-trivial steps.
At DroidsOnRoids we have recently started using Flutter for mobile app development. Flutter is a cross-platform mobile application development SDKs. You can read more about it in the article: Flutter in Mobile App Development – Pros & Risks for App Owners.
We started using Flutter, but there was no Bitrise step available. So we decided to create one ourselves!
Before we start coding, we need to prepare our environment. You can use your favorite editor/IDE to work with source files. If you are familiar with Android Studio or other IDEs from JetBrains, you may be interested in GoLand.
Finally, we will need Go tools. You can install it using your package manager (apt-get, Homebrew etc.) or download it from the project site.
Next, we can actually create a step using
bitrise :step create. Note the colon – it is needed because :step here denotes a plugin, not a command. Keep in mind that we have to select
go as a toolkit. The creation process is interactive and you will be asked to enter the details step by step. Just like this:
2. Step properties
Now we have a working step skeleton, most of the properties are set to reasonable default values. Nevertheless, we need to adjust a few of them. First, we can set
false because executing Flutter commands does not require admin/superuser permissions. Next we can add dependencies. Each dependency here is an apt-get (on Linux) or Homebrew (on MacOS) package.
How can we find out which dependencies are needed? A good starting point is the documentation of a given tool. For example, Flutter has a Get Started: Install chapter in their docs which contains a list of required system components. A Linux version is also available. However, it turns out that there is one more unlisted requirement – libglu1-mesa.
deps section in
step.yml file should look like this:
Virtually all the Bitrise steps need to be somehow configurable by users. In the case of Flutter, they may want to choose which Flutter commands they want to run e.g.
Moreover, it will be useful to be able to specify the exact Flutter version. Optionally it may default to the current one.
Finally, we may also support cases when the Flutter project is located somewhere other than the repository root directory. The easiest way to do this is to establish a reasonable default value but allow users to change it when they need to. The complete inputs section of the Flutter step looks like this:
All the inputs have names:
commands respectively. Right after the name, each input contains a default value which will be used if users don’t set it explicitly in their configuration. Note that one of them is not a hardcoded text but an environment variable –
$BITRISE_SOURCE_DIR. It is exposed by Bitrise CLI. At runtime, it will be substituted by an actual value. In order for such a substitution to work, the
is_expand flag needs to be enabled. The inputs section should look like this in the graphical workflow editor:
Our step will be written in Go language. It’s used in virtually all non-trivial, official steps. Moreover, Bitrise provides go-utils – a collection of functions useful in Continuous Integration, so there is no need to implement everything from scratch and we can focus on business logic.
✔️ Golang basics
This article is not meant to be tutorial about programming in Go. It will only explain the most important things useful during step development. I also assume that you have basic programming knowledge, so I will not explain what is a string or nil here. You can use the official tour to quickly explore Go language basics.
In Golang there are no exceptions which can be thrown to interrupt current flow. If a given function invocation can fail, it has
error as the last return value (functions can return multiple values). We need to check if an error is not
nil to determine if the operation succeeded. If the error is fatal, it is usually propagated to the
main function, where we can print it to the log and exit with non-zero code.
Keep in mind that errors should not be swallowed but logged or returned to the caller. Go lint will complain about ignored errors.
At the time of writing, there is no standard, built-in dependency management system in Go. Bitrise uses dep – an official experiment ready for production use. Dep is not shipped with Go. It has to be installed separately.
Note that files generated by dep need to be checked into Version Control System. Apart from configuration files, there is also the source code of all the dependencies.
Step configuration comes from the environment. All the aforementioned inputs become environment variables. Note the snake_case in names, it’s a convention of Bitrise. The pipe character (
|) used as a multiple input values separator is also guided by convention.
Parsing environment variables into objects usable from Go code can be done using go-steputils. We can just declare the structure containing configuration parameters and let the
stepconf parse it:
Note that structure name starts with uppercase. It is needed if the structure is accessed from other go files. Comment with ellipsis is only used to make lint happy. Each structure field has a tag with the corresponding environment variable name.
A tag can also contain properties e.g. whether a given field is required or if it should represent a path to the directory. Parsing will fail if these conditions are not met. In the case of invalid configuration, we need to exit with non-zero code.
✔️ Flutter logic
The rest of the source code represents actions specific to Flutter invocation:
- Ensure that the Android SDK is up to date (only if it is present) – Flutter requires at least 26.0.0
- Download and extract the Flutter SDK (destination path is OS-specific)
- Execute the supplied Flutter commands
Before implementing something from scratch, first check if something similar does not exist either in the Go standard library or in external libraries. Operations on files, directories, paths, commands invocation, printing logs etc are commonly used in CI so they can likely exist somewhere in Bitrise open-source repos, such as bitrise-tools or go-utils.
To add a dependency on an external library, invoke:
dep ensure -add <import path> from the terminal, where
<import path> is a value placed in import declarations e.g.
If you need to perform cleanup after some operation, whether it succeeded or not, use a defer statement. It is similar to the
finally block in Java/Kotlin. Note that you cannot propagate an error from a deferred function. However, you should also not ignore errors but log them:
✔️ Tests & static code analysis
According to the StepLib pull request template, each step should have a
test workflow. Usually, it consists of several steps:
In StepLib, there are steps for all aforementioned kinds of tests. Here is how the test workflow can look like:
Switch working dir and Step test steps are generated automatically during step creation. Integration tests often need some reasonable inputs. If some of this input is non-public e.g. it’s an API key/token etc. you can define it as a secret. Unit test file names should have the
_test suffix in order to be recognized properly. Unit tests on Bitrise usually use testify framework for assertions. Here is a simple unit test example:
Keep in mind that, if a step is applicable for all platforms (Android and iOS), like Flutter, you should test it on more than one Bitrise stack. Steps like Trigger Bitrise workflow or Bitrise Start Build can be useful in this matter.
5. The finishing touches
If your step is ready, you can request to publish it to StepLib. To do this, you have to first fork the StepLib repo and set the
MY_STEPLIB_REPO_FORK_GIT_URL environment variable in
bitrise.yml to the URL of that fork. You also need a semver tag (e.g.
0.0.1) on the repo of your step (not the StepLib fork). If all these requirements are met, you can invoke
bitrise run share-this-step. Note that it will also run an audit required by the StepLib checklist so you don’t need to execute any other commands.
Now you can create a pull request from your StepLib fork to the upstream and wait for review by the Bitrise team. The step may be reviewed in just a few minutes 😊:
I hope that my article will help you to create and publish your own Bitrise steps. As you can see above, it’s not very difficult. You also need to remember that Bitrise offers $25 discount for step contributors!
Also published on Medium.