How to Create Bitrise Step in Go — Flutter Example

Written by karol.wrotniak | Published 2018/05/07
Tech Story Tags: bitrise | flutter | go | steps | create-bitrise-step

TLDRvia the TL;DR App

Background

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.

The problem

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!

1. Preparation

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.

Apart from that, we need a Bitrise CLI. Just install and set it up by invoking bitrise setup. Note that you need a Linux or Mac machine. CLI for Windows is not available.

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 is_requires_admin_user to 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.

Finally our deps section in step.yml file should look like this:

3. Inputs

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. test and/or build.

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: version, working_dir and 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:

4. Implementation

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.

Error handling

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.

Dependencies

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.

✔️ Configuration

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:

  1. Ensure that Android SDK is up to date (only if it is present) — Flutter requires at least 26.0.0
  2. Download and extract Flutter SDK (destination path is OS-specific)
  3. Execute supplied Flutter commands

The full source code is available on GitHub.

✔️ Hints

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. github.com/bitrise-io/go-utils/pathutil.

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:

  • Unit tests
  • Go linter
  • errcheck — additional static code analysis tool
  • Integration tests

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 😊:

If you are releasing an update to already existing step you should also create release notes on GitHub repo of the step. It is the source of the StepLib changelog.

Wrap up

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.

Resources

Originally published at iOS & Android Mobile App Development Company — Droids On Roids — Poland.


Written by karol.wrotniak | Android developer @ Droids On Roids
Published by HackerNoon on 2018/05/07