Bitrise.io is a Continuous Integration and Delivery (CI/CD) Platform as a Service (PaaS) with the main focus on 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. Nonetheless, 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 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 one of the cross-platform mobile application development SDKs. More about it you can read 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. 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. Firstly 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 to find out which dependencies are needed? A good starting point is a documentation of given tool. For example Flutter has a Get Started: Install chapter in their docs which contains a list of required system components. 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 the users. In 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 exact Flutter version. Optionally it may default to the current one.
Finally, we may also support cases when Flutter project is located somewhere else than repository root directory. The easiest way to do that is to establish reasonable default value but allow users to change it when they need to. Complete inputs section of 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 actual value. In order for such substitution to work, the
is_expand flag needs to be enabled. Inputs section should look like this in 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 here what is a string or nil. 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 given function invocation can fail it has
error as the last return value (functions can return multiple values). We need to check if error is not
nil to determine if 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 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 on Bitrise. 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 another go files. Comment with ellipsis is only used to make lint happy. Each structure field has a tag with corresponding environment variable name.
A tag can also contain properties e.g. whether given field is required or it should represent a path to the directory. Parsing will fail if conditions are not met. In case of invalid configuration, we need to exit with non-zero code.
✔️ Flutter logic
Rest of the source code represents actions specific to Flutter invocation:
- Ensure that Android SDK is up to date (only if it is present) — Flutter requires at least 26.0.0
- Download and extract Flutter SDK (destination path is OS-specific)
- Execute supplied Flutter commands
The full source code is available on GitHub.
Before implementing something from scratch check first if something similar does not exist either in 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 like bitrise-tools or go-utils.
To add a dependency on external library invoke:
dep ensure -add <import path> from terminal. Where
<import path> is a value placed in import declarations e.g.
If you need to perform cleanup after some operation whenever it succeeded or not use a defer statement. It is similar to
finally block in Java/Kotlin. Note that you cannot propagate error from a deferred function. However, you should also not ignore errors but log them:
✔️ Tests & static code analysis
According to StepLib pull request template each step should have
test workflow. Usually it consists of several steps:
In StepLib there are steps for all aforementioned kinds of tests. Here is how 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 that input is non-public e.g. it’s API key/token etc. you can define it as a secret. Unit test file names should have
_test suffix in order to be recognized properly. Unit tests on Bitrise usually use testify framework for assertions. Here is the simple unit test example:
Keep in mind that if a step is applicable for all the 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 that 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
MY_STEPLIB_REPO_FORK_GIT_URL environment variable in
bitrise.yml to 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 those requirements are met you can invoke
bitrise run share-this-step. Note that it will also run audit required by 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 Bitrise team. Step may be reviewed even in 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 need to remember that Bitrise offers $25 discount for step contributors.
Originally published at iOS & Android Mobile App Development Company — Droids On Roids — Poland.