Beyond the basics: An in-depth look at Bitrise Pipelines

A hands-on guide to using Bitrise Pipelines to speed up your mobile CI/CD Workflows.

When mobile teams scales and becomes a large engineering team, this affects mobile build times, and teams face a new challenge. They start thinking about how to save the developer's hours, reduce the build time, and accelerate the process of building, testing, and deploying the apps. 

To keep scalability and keep build times low, parallel builds can help you save valuable developer time.

“Parallelization means running different tasks simultaneously, which will reduce the execution time and save the waiting hours from the developer's daily hours, which increases the developer's productivity.

At Bitrise, we want to help you save developer's hours and cut costs, so we introduced Bitrise Pipelines

What are Bitrise Pipelines?

Bitrise Pipelines is the top level of our CI/CD configuration. Pipelines can organize the entire CI/CD process and set up advanced configurations with multiple tasks running parallel and/or sequentially.  

A Pipelines’ building blocks are:

  • Steps: Blocks of script execution, each defining a single task in a CI/CD process.
  • Workflows: Collections of Steps. When a build of an app is running, the Steps will be executed in the order defined in the Workflow.

Stages: Collections of Workflows. A Stage can contain multiple Workflows that run in parallel in the same Stage. If all Workflows succeed in a Stage, the Pipelines move on to the next Stage. If any Workflows fail, the Pipelines ends without running the other Stages unless you configure a given Stage to always run.

How does it work

With your trigger of a Bitrise Build Pipelines, you start multiple workflows simultaneously (as long as they do not rely on each other, such as Workflow 1 and Workflow 2 in the following image). And as we discussed previously, the workflows contain different Steps that execute specific tasks.

Once the Pipelines is finished, you can check the status, and if you have any failed workflow, you can re-run it without running the whole Pipelines. After that, all the artifacts from the Workflows will be collected and you can see them in the Pipelines artifacts. 

The benefits of Bitrise Build Pipelines

By breaking your Workflows into Stages and Pipelines, you will be able to:

  • Receive faster PR feedback, and debugging becomes easier
  • Be able to set up more complex structures with easy parallelization
  • Be able to divide tests into separate Workflows (test sharding)
  • Save time and credits with 'partial rebuild' for the failed Workflows only

Parallel builds

You can run your builds in parallel and get mobile apps into production quicker. PR checks can be executed faster and more developers can commit changes and run more builds, and they gain the ability to compare sequential builds to ones already parallelized. Moreover, artifacts produced in one stage can be reused in the next stage resulting in faster CI results, faster builds, and lower credit consumption.

Test-sharding / UI test parallelization

Large test suites can be split into individual tests and run parallel to save developer time. UI, unit, simulator, and security tests (you name it) can run individually to help you identify where a step is failing faster.

Partial re-run

Why wait for a whole workflow to be re-run when you can run part of a workflow and get it done quicker? When a workflow fails, select to re-run only the workflow that failed in your stage. Build Pipelines’ partial re-run also makes 3rd party flakiness a non-issue giving you gains in Developer productivity, time, and money.

Getting started with Bitrise Pipelines

In our example, we will implement Bitrise Pipelines for an iOS application. You can follow the steps here if you already have an iOS application on Bitrise. You can watch our BitByte video about adding an iOS application. When adding the iOS app on Bitrise, we have two default Workflows, primary and deploy, as displayed in these images.

Or if you already have an existing app, you may have other Workflows or Steps anyway; it will be the same steps with both cases. 

If you don’t have the app, don’t worry. You can fork our sample app from here.

Configuring Bitrise Pipelines

Configuring Pipelines is only possible by directly editing the bitrise.yml file. You can create and modify Workflows in the graphical Workflow Editor, but you need to define Pipelines and Stages in YAML format. 

NOTE: If you’d rather use a visual Pipelines editor, try an unofficial editor written by Damien Murphy, Solutions Architect at Bitrise. Bitrise does not officially maintain the editor, which can be found here.

To start with the Pipelines, follow these steps:

1- Open the Bitrise project’s Workflow Editor

2- Go to the bitrise.yml tab and replace the existing bitrise.yml with the contents of the example bitrise.yml below.

In the following example, run_ui_tests and run_unit_tests Workflows are extended with a deploy-to-bitrise-io Step to make the generated test results available for the next Stage.

build_and_run_tests Pipelines is extended with a new Stage: deploy_test_results.

This Stage runs the deploy_test_results Workflow:

  1. pull-intermediate-files Step downloads the previous stage (run_tests) generated test results.
  2. script Step moves each test result into a new test run directory within the Test Report add-on deploy dir and creates the related test-info.json file.
  3. deploy-to-bitrise-io Step deploys the merged test results.


// YAML file

---

format_version: '11'

default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git

project_type: ios

app:

  envs:

  - BITRISE_PROJECT_PATH: BullsEye.xcworkspace

  - BITRISE_SCHEME: BullsEye

meta:

  bitrise.io:

    stack: osx-xcode-13.2.x

    machine_type_id: g2-m1-max.10core

pipelines:

  build_and_run_tests:

    stages:

    - build_tests: 

    - run_tests: 

    - deploy_test_results: 

stages:

  build_tests:

    should_always_run: true

    workflows:

    - xcode_build_for_test: 

  run_tests:

    abort_on_fail: true

    workflows:

    - run_ui_tests: 

    - run_unit_tests: 

  deploy_test_results:

    workflows:

    - merge_and_deploy_test_results: 

workflows:

  _pull_test_bundle:

    steps:

    - pull-intermediate-files@1:

        inputs:

        - artifact_sources: build_tests.xcode_build_for_test

  merge_and_deploy_test_results:

    steps:

    - pull-intermediate-files@1:

        inputs:

        - artifact_sources: run_tests\..*

    - script@1:

        inputs:

        - content: |

            #!/usr/bin/env bash

            set -eo pipefail

‍

            for item in "${BITRISE_UI_TEST_XCRESULT_PATH}" "${BITRISE_UNIT_TEST_XCRESULT_PATH}";

              do

                echo "Exporting ${item}"

‍

                test_name=$(basename "$item" .xcresult)

                echo "Test name: $test_name"

‍

                test_dir="${BITRISE_TEST_RESULT_DIR}/${test_name}"

                mkdir -p "${test_dir}"

                echo "Moving test result to: ${test_dir}"

                cp -R "${item}" "${test_dir}/$(basename ${item})"

‍

                test_info="${test_dir}/test-info.json"

                echo "Creating Test info at: ${test_info}"

                echo "{ \"test-name\": \"${test_name}\" }" > "$test_info"

              done

    - deploy-to-bitrise-io@2: {}

  run_ui_tests:

    before_run:

    - _pull_test_bundle

    steps:

    - xcode-test-without-building@0:

        inputs:

        - xctestrun: "$BITRISE_TEST_BUNDLE_PATH/BullsEye_UITests_iphonesimulator15.2-arm64-x86_64.xctestrun"

        - destination: platform=iOS Simulator,name=iPhone 12 Pro Max

    - deploy-to-bitrise-io@2:

        inputs:

        - pipeline_intermediate_files: "$BITRISE_XCRESULT_PATH:BITRISE_UI_TEST_XCRESULT_PATH"

  run_unit_tests:

    before_run:

    - _pull_test_bundle

    steps:

    - xcode-test-without-building@0:

        inputs:

        - xctestrun: "$BITRISE_TEST_BUNDLE_PATH/BullsEye_UnitTests_iphonesimulator15.2-arm64-x86_64.xctestrun"

        - destination: platform=iOS Simulator,name=iPhone 12 Pro Max

    - deploy-to-bitrise-io@2:

        inputs:

        - pipeline_intermediate_files: "$BITRISE_XCRESULT_PATH:BITRISE_UNIT_TEST_XCRESULT_PATH"

  xcode_build_for_test:

    steps:

    - activate-ssh-key@4: {}

    - [email protected]: {}

    - xcode-build-for-test@2:

        inputs:

        - destination: generic/platform=iOS Simulator

    - [email protected]:

        run_if: ".IsCI"

        inputs:

        - variables: TEST_BUNDLE_ZIP_PATH=$BITRISE_TEST_BUNDLE_ZIP_PATH

    - deploy-to-bitrise-io@2:

        inputs:

        - pipeline_intermediate_files: "$BITRISE_TEST_BUNDLE_PATH:BITRISE_TEST_BUNDLE_PATH"

trigger_map:

- pull_request_source_branch: "*"

  pipeline: build_and_run_tests

‍

// YAML file
Copy code

In the above example, we started by defining the Pipelines attribute, we have a Pipelines called build_and_run_tests, with three Stages that will run consecutively. This means that if build_tests finishes successfully, run_tests starts. If any of the Stages fail, the subsequent Stage will not start: instead, the Pipelines will be aborted and marked as failed.


Pipeliness:

  build_and_run_tests:

    stages:

    - build_tests: {}

    - run_tests: {}

stages:

  build_tests:

    workflows:

    - xcode_build_for_test: {}

  run_tests:

    workflows:

    - run_ui_tests: {}

    - run_unit_tests: {}
Copy code

Each Stage has to be defined separately under the stages attribute. Defining a Stage means specifying the Workflows that are part of the Stage. 

Specifying files to share between Stages

Add the Deploy to Bitrise.io - Apps, Logs, Artifacts Step to the Workflow (typically to the end of the Workflow) to generates a build artifact.

Then, configure the Step’s Files to share between Pipelines stages input under the Pipelines Intermediate File Sharing category. In our example, we used it in the xcode_build_for_test Workflow to share the Test Bundle Path with the run_ui_tests and run_unit_tests Workflows to be able to run the tests without the need to build the app again. 


- deploy-to-bitrise-io@2:

    inputs:

    - Pipelines_intermediate_files: "$BITRISE_TEST_BUNDLE_PATH:BITRISE_TEST_BUNDLE_PATH"
Copy code

And it can also be used in the run_ui_tests and run_unit_tests Workflows to share the test results with the merge_and_deploy_test_results Workflow


- deploy-to-bitrise-io@2:

    inputs:

    - Pipelines_intermediate_files: "$BITRISE_XCRESULT_PATH:BITRISE_UI_TEST_XCRESULT_PATH"
Copy code

Using artifacts from different Stages

You might have Workflows that rely on artifacts generated by Workflows in previous Stages, such as the run_ui_tests and run_unit_tests Workflows; they rely on xcode_build_for_test Workflow. To use them during your subsequent Workflows, you can use the Pull Pipelines intermediate files Step.

Configuring a Stage to always run

By default, if a Stage fails because one of its Workflows failed, any other subsequent Stages of the Pipelines will not run. However, you can configure your Pipelines to run certain Stages unless the Pipelines is aborted.

To do so, you just need to set the should_always_run attribute of the Stage to true in the yml file:


deploy_test_results:

    should_always_run: true

    workflows:

    - merge_and_deploy_test_results:
Copy code

Aborting the Workflows of a Failed Stage

By default, if a Workflow in a particular Stage fails, the other Workflows in the same Stage aren’t automatically aborted: these Workflows will run, but the next Stage won’t start. However, you can change this behavior to immediately and automatically abort all other Workflows in the same Stage.

To do so, you need to set the abort_on_fail attribute to true in the yml file:


run_tests:

    abort_on_fail: true

    workflows:

    - run_ui_tests: {}

    - run_unit_tests: {}
Copy code

Sharing Env Vars between Pipelines Stages

You can reuse any environment variable from a Workflow and reuse it in subsequent Stages using the Share Pipelines variables Step with the following steps:

  • Add the Share Pipelines variables Step to the Workflow.
  • Optionally, you can define additional run conditions in the Additional run conditions input. The Step will only run if the conditions you specify here are true.
  • Add the Env Var(s) you would like to use in subsequent Stages in the Variables to share between Pipelines Stages input. For example, you can share the Test Bundle Zip Path between the Stages or other environment variables.

Additionally, you can combine the Share Pipelines variables Step with run_if expressions to create Pipelines with optional Workflows. 

Setting up run_if conditions for optional Workflows using Pipelines

Optional Workflow is a Pipelines feature that allows you to decide whether a Workflow should run based on conditions you set in a run_if expression.

In bitrise.yml, under the stages/workflows field, you can add a run_if expression to any Workflow, and if they are part of a Pipelines build, the run_if will be evaluated to determine if the Workflow should run or not.

For example, we can add another workflow to send Slack notifications after all the Workflows are finished. In the bitrise.yml file we can add the run_if with the workflow like the following example: 


stages:

    build_tests:
  
      should_always_run: true
  
      workflows:
  
      - xcode_build_for_test: 
  
    run_tests:
  
      abort_on_fail: true
  
      workflows:
  
      - run_ui_tests: 
  
      - run_unit_tests: 
  
    deploy_test_results:
  
      should_always_run: true
  
      workflows:
  
      - merge_and_deploy_test_results: 
  
    send_notifications:
  
      workflows:
  
      - send_slack_notification:
  
          run_if: '{{ getenv "SLACK_URL" | eq "URL HERE" }}'
Copy code

In case the SLACK_URL is wrong or there is any issue with it, the send_slack_notification Workflow will be skipped as shown below. 

TIP: A run_if can be any valid Go template, as long as it evaluates to true or false (or any of the String representations, for example, True, t, yes, or y, are all considered to be true). If the template evaluates to true, the Workflow will run; otherwise, it won’t.

The final bitrise.yml file after the previous changes will be like the following example: 


// YAML file

---
    
    format_version: '11'
    
    default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
    
    project_type: ios
    
    app:
    
      envs:
    
      - BITRISE_PROJECT_PATH: BullsEye.xcworkspace
    
      - BITRISE_SCHEME: BullsEye
    
    meta:
    
      bitrise.io:
    
        stack: osx-xcode-13.2.x
    
        machine_type_id: g2-m1-max.10core
    
    pipelines:
    
      build_and_run_tests:
    
        stages:
    
        - build_tests: 
    
        - run_tests: 
    
        - deploy_test_results: 
    
        - send_notifications: 
    
    stages:
    
      build_tests:
    
        should_always_run: true
    
        workflows:
    
        - xcode_build_for_test: 
    
      run_tests:
    
        abort_on_fail: true
    
        workflows:
    
        - run_ui_tests: 
    
        - run_unit_tests: 
    
      deploy_test_results:
    
        workflows:
    
        - merge_and_deploy_test_results: 
    
      send_notifications:
    
        workflows:
    
        - send_slack_notification:
    
            run_if: '{{ getenv "SLACK_URL" | eq "URL here" }}'
    
    workflows:
    
      _pull_test_bundle:
    
        steps:
    
        - pull-intermediate-files@1:
    
            inputs:
    
            - artifact_sources: build_tests.xcode_build_for_test
    
      merge_and_deploy_test_results:
    
        steps:
    
        - pull-intermediate-files@1:
    
            inputs:
    
            - artifact_sources: run_tests\..*
    
        - script@1:
    
            inputs:
    
            - content: |
    
                #!/usr/bin/env bash
    
                set -eo pipefail
    
    ‍
    
                for item in "${BITRISE_UI_TEST_XCRESULT_PATH}" "${BITRISE_UNIT_TEST_XCRESULT_PATH}";
    
                  do
    
                    echo "Exporting ${item}"
    
    ‍
    
                    test_name=$(basename "$item" .xcresult)
    
                    echo "Test name: $test_name"
    
    ‍
    
                    test_dir="${BITRISE_TEST_RESULT_DIR}/${test_name}"
    
                    mkdir -p "${test_dir}"
    
                    echo "Moving test result to: ${test_dir}"
    
                    cp -R "${item}" "${test_dir}/$(basename ${item})"
    
    ‍
    
                    test_info="${test_dir}/test-info.json"
    
                    echo "Creating Test info at: ${test_info}"
    
                    echo "{ \"test-name\": \"${test_name}\" }" > "$test_info"
    
                  done
    
        - deploy-to-bitrise-io@2: {}
    
      run_ui_tests:
    
        before_run:
    
        - _pull_test_bundle
    
        steps:
    
        - xcode-test-without-building@0:
    
            inputs:
    
            - xctestrun: "$BITRISE_TEST_BUNDLE_PATH/BullsEye_UITests_iphonesimulator15.2-arm64-x86_64.xctestrun"
    
            - destination: platform=iOS Simulator,name=iPhone 12 Pro Max
    
        - deploy-to-bitrise-io@2:
    
            inputs:
    
            - pipeline_intermediate_files: "$BITRISE_XCRESULT_PATH:BITRISE_UI_TEST_XCRESULT_PATH"
    
      run_unit_tests:
    
        before_run:
    
        - _pull_test_bundle
    
        steps:
    
        - xcode-test-without-building@0:
    
            inputs:
    
            - xctestrun: "$BITRISE_TEST_BUNDLE_PATH/BullsEye_UnitTests_iphonesimulator15.2-arm64-x86_64.xctestrun"
    
            - destination: platform=iOS Simulator,name=iPhone 12 Pro Max
    
        - deploy-to-bitrise-io@2:
    
            inputs:
    
            - pipeline_intermediate_files: "$BITRISE_XCRESULT_PATH:BITRISE_UNIT_TEST_XCRESULT_PATH"
    
      xcode_build_for_test:
    
        steps:
    
        - activate-ssh-key@4: {}
    
        - [email protected]: {}
    
        - xcode-build-for-test@2:
    
            inputs:
    
            - destination: generic/platform=iOS Simulator
    
        - [email protected]:
    
            run_if: ".IsCI"
    
            inputs:
    
            - variables: TEST_BUNDLE_ZIP_PATH=$BITRISE_TEST_BUNDLE_ZIP_PATH
    
        - deploy-to-bitrise-io@2:
    
            inputs:
    
            - pipeline_intermediate_files: "$BITRISE_TEST_BUNDLE_PATH:BITRISE_TEST_BUNDLE_PATH"
    
      send_slack_notification:
    
        steps:
    
        - slack@3: {}
    
    trigger_map:
    
    - pull_request_source_branch: "*"
    
      pipeline: build_and_run_tests
    
    ‍
    
    // YAML file
Copy code

Now that we discussed the different options we can do with the Pipelines, it's time to run it. Let’s do it! 

Running a Pipelines

There are two ways to start a Pipelines:

  • Trigger the Pipelines automatically 
  • Trigger the Pipelines manually

In detail, let’s explain how we can run the Pipelines with these options. 

Trigger the Pipelines automatically 

To set up automatic build triggers, you need a trigger map. The trigger map defines what code events should trigger builds. In a Pipelines configuration, you need to specify the Pipelines triggered by certain code events, such as pushes, pull requests, and tags.

In the bitrise.yml file, it can be like this example: 


trigger_map:

- pull_request_source_branch: "*"

  Pipeline: build_and_run_tests
Copy code

That means we will run the Pipelines on every branch pull request.

This example focuses on configuring Pipelines triggers using bitrise.yml, the same configuration can be achieved from the Workflow Editor's Triggers tab.

Trigger the Pipelines manually

To trigger the Pipelines manually, you can follow the following steps:

  • Open the app on Bitrise.
  • Go to the Builds tab.
  • Click Start/Schedule a Build.
  • Scroll down to the Workflow, Pipelines dropdown menu.
  • Select the Pipelines you want to run.
  • Click Start Build.

Rebuilding a Failed Pipelines

When a Pipelines build fails, you have two options:

  • Rebuild unsuccessful Workflows
  • Rebuild the entire Pipeline
  • Rebuild unsuccessful Workflows with Remote access
  • Rebuild the entire Pipeline with Remote access

Rebuilding unsuccessful Workflows

When you have a failed or aborted Workflow in your Pipeline build, you can opt to modify that failed or aborted Workflow and rebuild it and subsequent Workflows without the need to rebuild your entire Pipeline. The new attempt will start from the first failed or aborted Workflow. If that Workflow is successful this time, the build will continue with subsequent Workflows.

Rebuilding unsuccessful Workflows

When you have a failed or aborted Workflow in your Pipeline build, you can opt to modify that failed or aborted Workflow and rebuild it and subsequent Workflows without the need to rebuild your entire Pipeline. The new attempt will start from the first failed or aborted Workflow. If that Workflow is successful this time, the build will continue with subsequent Workflows.

Note: There are some cases when rebuilding unsuccessful Workflows is not available for different reasons e.g. the pipeline configuration changed in yml.

Workflow Recipes for common use cases

Workflow Recipes are prewritten YML files to help you set up your Workflows and Stages for your Pipelines. Each Recipe is preconfigured for a use case. You can copy and paste them into your app’s bitrise.yml

For iOS:

For Android:

Helpful Resources

Our team constantly works to build the ultimate solutions for all mobile developers to learn and share best practices. If you have any suggestions or questions, join our community forums

We are always happy to help you.

Happy testing! 

Bitrise Developer Relation Team

Get Started for free

Start building now, choose a plan later.

Sign Up

Get started for free

Start building now, choose a plan later.