Create custom bitrise step

Akshay Kale
5 min readApr 27, 2021

To make things easy it’s better to clone a sample bitrise step and start modifying it based on the needs.

Or use bitrse clientto initialize a project.

//Create the Step with the Bitrise Step pluginmkdir my_step_dir 
cd my_step_dir
bitrise :step create

And follow the steps and general inputs required. Once you are done with this and if everything went well, the plugin initialized a git repository in the current directory and added a step.yml, a README.md file, and either a main.go or a main.sh file.

Now we’ll go through how the step.yml file works and how to set it up.

The step.yml file is the Step interface definition, containing dependencies, Step inputs and Step outputs as well as other Step properties.

Let’s see how a step to change android version name will look like

title: Change Android versionCode and add versionName suffix
summary: Updates the Android versionCode and versionName in your project's `build.gradle` file.
description: Modifies the version information of your Android app by updating versionCode and versionName attributes in your project's `build.gradle` file before you'd publish your app to Google Play Store.

website: ___
source_code_url: ___(your git repo link)
support_url: ___
host_os_tags:
- osx-10.10
- ubuntu-16.04
project_type_tags:
- android
- react-native
- cordova
- ionic
- flutter
type_tags:
- utility
is_requires_admin_user: true
is_always_run: false
is_skippable: false
run_if: ""
toolkit:
go:
package_name: github.com/akshaykale/___name of step___
inputs:
- build_gradle_path: $BITRISE_SOURCE_DIR/app/build.gradle
opts:
title: Path to the build.gradle file
summary: Path to the build.gradle file shows the versionCode and versionName settings.
is_required: true
- version_name_seperator:
opts:
title: versionName and suffix seperator
summary: |-
Add a seperattor to version name andd suffix. Default "-".
description: |-
Add a seperattor to version name andd suffix.
Default "-"
- version_name_suffix:
opts:
title: versionName suffix
summary: |-
Add suffix to version name.
description: |-
Add suffix to version name.
- new_version_code: $BITRISE_BUILD_NUMBER
opts:
title: New versionCode
summary: |-
New versionCode to set.
description: |-
New versionCode to set.
Specify a positive integer value, such as `1`.
The greatest value Google Play allows for versionCode is 2100000000.
Leave this input empty so that versionCode remains unchanged.
- version_code_offset:
opts:
title: versionCode Offset
summary: |-
Offset value to add to `New versionCode`.
description: |-
Offset value to add to `New versionCode`, for example: `1`.
Leave this input empty if you want the exact value you set in `New versionCode` input.
outputs:
- ANDROID_VERSION_NAME:
opts:
title: Final Android versionName in build.gradle file
- ANDROID_VERSION_CODE:
opts:
title: Final Android versionCode in build.gradle file

And let’s see how the main.go file looks like

package mainimport (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"github.com/bitrise-io/go-steputils/stepconf"
"github.com/bitrise-io/go-utils/command"
"github.com/bitrise-io/go-utils/fileutil"
"github.com/bitrise-io/go-utils/log"
)
const (
versionCodeRegexPattern = `^versionCode(?:\s|=)+(.*?)(?:\s|\/\/|$)`
versionNameRegexPattern = `^versionName(?:=|\s)+(.*?)(?:\s|\/\/|$)`
)
type config struct {
BuildGradlePth string `env:"build_gradle_path,file"`
VersionNameSep string `env:"version_name_seperator"`
VersionNameSuffix string `env:"version_name_suffix"`
NewVersionCode int `env:"new_version_code,range]0..2100000000]"`
VersionCodeOffset int `env:"version_code_offset"`
}
type updateFn func(line string, lineNum int, matches []string) stringfunc findAndUpdate(reader io.Reader, update map[*regexp.Regexp]updateFn) (string, error) {
scanner := bufio.NewScanner(reader)
var updatedLines []string
for lineNum := 0; scanner.Scan(); lineNum++ {
line := scanner.Text()
updated := false
for re, fn := range update {
if match := re.FindStringSubmatch(strings.TrimSpace(line)); len(match) == 2 {
if updatedLine := fn(line, lineNum, match); updatedLine != "" {
updatedLines = append(updatedLines, updatedLine)
updated = true
break
}
}
}
if !updated {
updatedLines = append(updatedLines, line)
}
}
return strings.Join(updatedLines, "\n"), scanner.Err()
}
func exportOutputs(outputs map[string]string) error {
for envKey, envValue := range outputs {
cmd := command.New("envman", "add", "--key", envKey, "--value", envValue)
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}
func failf(format string, v ...interface{}) {
log.Errorf(format, v...)
os.Exit(1)
}
// BuildGradleVersionUpdater updates versionName and versionCode in the given build.gradle file.
type BuildGradleVersionUpdater struct {
buildGradleReader io.Reader
}
// NewBuildGradleVersionUpdater constructs a new BuildGradleVersionUpdater.
func NewBuildGradleVersionUpdater(buildGradleReader io.Reader) BuildGradleVersionUpdater {
return BuildGradleVersionUpdater{buildGradleReader: buildGradleReader}
}
// UpdateResult stors the result of the version update.
type UpdateResult struct {
NewContent string
FinalVersionCode string
FinalVersionName string
RealVersionName string
UpdatedVersionCodes int
UpdatedVersionNames int
}
// UpdateVersion executes the version updates.
func (u BuildGradleVersionUpdater) UpdateVersion(newVersionCode, versionCodeOffset int, versionNameSep, versionNameSuffix string) (UpdateResult, error) {
res := UpdateResult{}
var err error
res.NewContent, err = findAndUpdate(u.buildGradleReader, map[*regexp.Regexp]updateFn{
regexp.MustCompile(versionCodeRegexPattern): func(line string, lineNum int, match []string) string {
oldVersionCode := match[1]
res.FinalVersionCode = oldVersionCode
updatedLine := ""
if newVersionCode > 0 {
res.FinalVersionCode = strconv.Itoa(newVersionCode + versionCodeOffset)
updatedLine = strings.Replace(line, oldVersionCode, res.FinalVersionCode, -1)
res.UpdatedVersionCodes++
log.Printf("updating line (%d): %s -> %s", lineNum, line, updatedLine)
}
return updatedLine
},
regexp.MustCompile(versionNameRegexPattern): func(line string, lineNum int, match []string) string {
oldVersionName := match[1]
res.FinalVersionName = oldVersionName
res.RealVersionName = oldVersionName
updatedLine := ""
if versionNameSuffix != "" {
versionName := ""
if versionNameSep != "" {
versionName = oldVersionName + versionNameSep + versionNameSuffix
} else {
versionName = oldVersionName + "-" + versionNameSuffix
}
quotedVersionName := `"` + strings.Replace(versionName, "\"", "", -1) + `"`
log.Printf("final version name - %s", quotedVersionName)
res.FinalVersionName = quotedVersionName
res.RealVersionName = oldVersionName
updatedLine = strings.Replace(line, oldVersionName, res.FinalVersionName, -1)
res.UpdatedVersionNames++
log.Printf("updating line (%d): %s -> %s", lineNum, line, updatedLine)
}
return updatedLine
},
})
if err != nil {
return UpdateResult{}, err
}
return res, nil
}
func main() {
var cfg config
if err := stepconf.Parse(&cfg); err != nil {
failf("Issue with input: %s", err)
}
stepconf.Print(cfg)
fmt.Println()
if cfg.VersionNameSuffix == "" && cfg.NewVersionCode == 0 {
failf("Neither NewVersionCode nor VersionNameSuffix are provided, however one of them is required.")
}
//
// find versionName & versionCode with regexp
fmt.Println()
log.Infof("Updating versionName and versionCode in: %s", cfg.BuildGradlePth)
f, err := os.Open(cfg.BuildGradlePth)
if err != nil {
failf("Failed to read build.gradle file, error: %s", err)
}
versionUpdater := NewBuildGradleVersionUpdater(f)
res, err := versionUpdater.UpdateVersion(cfg.NewVersionCode, cfg.VersionCodeOffset, cfg.VersionNameSep, cfg.VersionNameSuffix)
if err != nil {
failf("Failed to update versions: %s", err)
}
//
// export outputs
if err := exportOutputs(map[string]string{
"ANDROID_VERSION_NAME": res.RealVersionName,
"ANDROID_FINAL_VERSION_NAME": res.FinalVersionName,
"ANDROID_VERSION_CODE": res.FinalVersionCode,
}); err != nil {
failf("Failed to export outputs, error: %s", err)
}
if err := fileutil.WriteStringToFile(cfg.BuildGradlePth, res.NewContent); err != nil {
failf("Failed to write build.gradle file, error: %s", err)
}
fmt.Println()
log.Donef("%d versionCode updated", res.UpdatedVersionCodes)
log.Donef("%d versionName updated", res.UpdatedVersionNames)
}

Once this is done. You can commit the changes to your own public repository.

Next step: Use this step in bitrise workflow

Go to you workflow editor on bitrise.

Then click on bitrise.yml tab. You will be shown all the workflows in this file. Go to the workflow you want to add this step to.

And add you custom as follows:

In our case best position is above the gradle runner task.

tmp: 
steps:
- activate-ssh-key@3.1.1:
run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
- git-clone@4.0.5: {}
- cache-pull@2.0.1: {}
- install-missing-android-tools@2.1.1: {}
- git::https://github.com/akshaykale/steps-change-android-versioncode-and-versionname.git@master:
inputs:
- version_name_seperator: ''
- version_name_suffix: "$BITRISE_BUILD_NUMBER"
- new_version_code: "$BITRISE_BUILD_NUMBER"
- gradle-runner@1.8.3:
... // other steps

This is how you add you custom step.

  • git:: source type can be used for not-yet-published or work-in-progress states of a step
  • Get the clone url from your repository.
  • Instead of version, we can use branch name or tag
- git::https://github.com/{username}/{stepName}.git@BRANCH-OR-TAG:

--

--