How to Create Bitrise Step in Go – Flutter Example

Karol Wrótniak of Droids On Roids demonstrates creating & publishing a Bitrise step, using the example of Flutter and focusing on programming in Go, which is the main language used by Bitrise.

In this article, we will show you how to create & publish your own Bitrise step, using the example of Flutter. We will focus on programming in Go, which is the main language used by Bitrise.

Guest blog by Karol Wrótniak, mobile developer at Droids On Roids. The original post appeared on Droids On Roids blog.

Droids On Roids is a full-service mobile & web development company from Poland, building high-performing applications for great brands and start-ups like GIPHY, Skybuds & Electric Objects. With 50+ projects in their portfolio, Droids On Roids' solutions serve millions of users across platforms and channels. Member of Google Developers Agency Program. Ranked twice in the Deloitte Technology Fast 50 Central Europe.

Background

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.

The problem

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!

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 this, 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. 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 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 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.

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


deps:
  brew:
  - name: git
  - name: curl
  - name: unzip
  apt_get:
  - name: git
  - name: curl
  - name: unzip
  - name: libglu1-mesa
  
Copy code

3. Inputs

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

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:


inputs:
  - version: 0.3.1-beta
    opts:
      title: "Flutter version"
      summary: Flutter version including channel. Without `v` prefix. E.g. `0.2.8-alpha` or `0.3.0-dev`.
      is_expand: true
      is_required: true

  - working_dir: $BITRISE_SOURCE_DIR
    opts:
      title: "Root directory of Flutter project"
      summary: The root directory of your Flutter project, where your `pubspec.yaml` file is located.
      is_expand: true
      is_required: true

  - commands: build
    opts:
      title: "Flutter commands to be executed"
      summary: |
        `|` separated Flutter commands to be executed in order. E.g. `build`, `test` or `install`.
      is_expand: true
      is_required: true
Copy code

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

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 what is a string or nil here. 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 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.

Dependencies

At the time of writing, there is no standard, built-in dependency management system in Go. Bitrise uses depan 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.

✔️ Configuration

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:


// Config ...
type Config struct {
	Version    string   `env:"version,required"`
	WorkingDir string   `env:"working_dir,dir"`
	Commands   []string `env:"commands,required"`
}
Copy code

var config Config
if err := stepconf.Parse(&config); err != nil {
	log.Errorf("Configuration error: %s\n", err)
	os.Exit(7)
}
stepconf.Print(config)
Copy code

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:

  1. Ensure that the Android SDK is up to date (only if it is present) – Flutter requires at least 26.0.0
  2. Download and extract the Flutter SDK (destination path is OS-specific)
  3. Execute the supplied Flutter commands
The full source code is available on GitHub.

✔️ Hints

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

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:


func downloadFile(downloadURL string, outFile *os.File) error {
	response, err := http.Get(downloadURL)
	if err != nil {
		return err
	}

	defer func() {
		if err := response.Body.Close(); err != nil {
			log.Warnf("Failed to close (%s) body", downloadURL)
		}
	}()

	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to download file from %s, error: %s", downloadURL, response.Status)
	}

	_, err = io.Copy(outFile, response.Body)
	if err != nil {
		return fmt.Errorf("failed to save file %s, error: %s", outFile.Name(), err)
	}

	return nil
}
Copy code

✔️ Tests & static code analysis

According to the StepLib pull request template, each step should have a testworkflow. 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 the test workflow can look like:


- change-workdir:
    title: Switch working dir to test / _tmp dir
    description: |-
      To prevent step testing issues, like referencing relative
      files with just './some-file' in the step's code, which would
      work for testing the step from this directory directly
      but would break if the step is included in another `bitrise.yml`.
    run_if: "true"
    inputs:
    - path: ./_tmp
    - is_create_path: true
- errcheck: {}
- go-test: {}
- golint: {}
- path::./:
    title: Step Test
    run_if: "true"
    inputs:
    - version: $FLUTTER_VERSION
    - working_dir: $WORKING_DIR
    - commands: $FLUTTER_COMMANDS
Copy code

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:


func TestDownloadFileUnreachableURL(t *testing.T) {
	dummyFile, err := os.Open("/dev/null")
	require.NoError(t, err)

	err = downloadFile("http://unreachable.invalid", dummyFile)
	require.Error(t, err)
}
Copy code

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

How to Create Bitrise Step in Go - Flutter Example

If you are releasing an update to an already existing step, you should also create release notes on the 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 also need to remember that Bitrise offers $25 discount for step contributors!

Resources

No items found.
The Mobile DevOps Newsletter

Explore more topics

App Development

Learn how to optimize your mobile app deployment processes for iOS, Android, Flutter, ReactNative, and more

Bitrise & Community

Check out the latest from Bitrise and the community. Learn about the upcoming mobile events, employee spotlights, women in tech, and more

Mobile App Releases

Learn how to release faster, better apps on the App Store, Google Play Store, Huawei AppGallery, and other app stores

Mobile DevOps

Learn Mobile DevOps best practices such as DevOps for iOS, Android, and industry-specific DevOps tips for mobile engineers

Mobile Testing & Security

Learn how to optimize mobile testing and security — from automated security checks to robust mobile testing and more.

Product Updates

Check out the latest product updates from Bitrise — Build Insights updates, product news, and more.

The Mobile DevOps Newsletter

Join 1000s of your peers. Sign up to receive Mobile DevOps tips, news, and best practice guides once every two weeks.