There is no modern software development without continuous integration, and Docker plays a key role in enhancing this process. I’m not going to explain what a pipeline is or how to set one up in Bitbucket —well, maybe just a little. I also assume you have experience using Git, including the most common commands like commit and push. Additionally, you should have a basic understanding of what continuous integration is.

At Modular MX, we rely heavily on Atlassian tools, and when used together, they create a very powerful workflow. This is why we’ll be using Bitbucket for this tutorial. However, if you follow the concepts, you should be able to apply them to GitHub, GitLab, or similar platforms. To get started, create a Bitbucket account —it’s free for what we’ll be doing.

Warming up with pipelines

If you've never set up a pipeline in Bitbucket before, this first part will serve as a simple introductory tutorial. You should also refer to the official documentation for further details.

Start by creating a project and then a repository in Bitbucket (do not add a README.md or .gitignore file). Once your repository is created, set up SSH access. You have no clue about SSH??, Here’s a quick guide to help you with that:

On your local machine, create a Git repository and add a simple main.c file (just something basic to start with).

#include <stdio.h>

int main( void )
{
    printf( "Hello, World!\n" );

    return 0;
}

Also its corresponding makefile. After writing both files git add, commit and push, and leave like that for the moment

all:
    gcc main.c -o main -Wall

In Bitbucket, you need to set up a runner. This is essentially a small program that connects your Bitbucket repository to your local repository. You should choose a Linux Docker runner. Here’s a guide to help you set up your runner:

The last step will provide you with a command to run a Docker container that includes everything necessary to connect your local machine with your Bitbucket repository. Copy the command it gives you and save it somewhere safe.

Run the container Bitbucket gave you, here is mine, ( use your own, this one is not going to work for you )

docker container run -it -v /tmp:/tmp -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/containers:/var/lib/docker/containers:ro -e ACCOUNT_UUID={bda5c83f-563b-4d5e-8431-4fda2b4f4da0} -e REPOSITORY_UUID={85642ca4-2d1b-4cc4-a5e0-4b305f6e9c30} -e RUNNER_UUID={2ff898c1-2219-5d0c-8892-5894c1891da4} -e RUNTIME_PREREQUISITES_ENABLED=true -e OAUTH_CLIENT_ID=kUhyjjqd6D61DHowftS4jKnWxznsNbbb -e OAUTH_CLIENT_SECRET=ATOA69EQldpNg11UHCDMKABUd4jhbXuvcdg1wqfKlvqhJ9_bS9hju4LtaMXgLmlpwz7XCA9F78FE -e WORKING_DIRECTORY=/tmp --name runner-2ff898c1-2219-5d0c-8892-5894c1891da4 docker-public.packages.atlassian.com/sox/atlassian/bitbucket-pipelines-runner

If you do a docker ps you can see is just a container running like any other, we are not going to interact with it, just leave there doing its own thing, you can also run int in the background replacing the flag -it for -d

$ docker ps                                                                                                                                                                       
CONTAINER ID   IMAGE                                                                           COMMAND                  CREATED             STATUS             PORTS     NAMES
c7eb46721775   docker-public.packages.atlassian.com/sox/atlassian/bitbucket-pipelines-runner   "/bin/sh -c ./entryp…"   About an hour ago   Up About an hour             runner-2ff898c1-2219-5d0c-8892-5894c1891da4

Create a file called bitbicket-pipelines.yml with just one single step to display a simple echo. Something important is to put all the labels you set for your runnner. In case you aren’t familiar with yaml here is a small tutorial from Dojo Five

pipelines:
  default:
      - step:
          name : Echo
          runs-on:
            - self.hosted
            - linux
          script:
            - echo "This step will run on a self docker runner.";

Git add, commit, and push. Then, take a look at your pipelines in your Bitbucket repo. They should run successfully with no errors. Of course, it's just a simple echo with no interaction with our code, but we can see that the pipelines are running fine.

This was a simple warm-up, just to refresh your memory on how to set up a pipeline in Bitbucket. What we really want to do now is test whether our project builds correctly, passes unit tests, and so on. For example, if we want to add a second stage specifically to build the project, we can update the bitbucket-pipelines.yml file with the following lines.

Go ahead and add, commit, and push the changes to test the pipeline. (Note: You need to have GCC and Make installed on your local machine.)

pipelines:
  default:
      - step:
          name: Echo
          runs-on:
            - self.hosted
            - linux
          script:
            - echo "This step will run on a self docker runner.";
      - step:
          name: Build
          runs-on:
            - self.hosted
            - linux
          script:
            - make

It is important to note these pipelines we set-up are running in our own local machine, meaning make command has been called from our computer it does not run in Bitbucket server or somewhere else.

Using our own Images

The last pipeline using Make locally was fine, but it’s not what we want. We want to build the project using a toolchain inside a container. For the following steps, first remove the previous main.c and Makefile (version this change, but do not push it to the remote repo). Then, fetch our template repository and rebase it (make sure to resolve all conflicts). Don’t push anything yet

$ git remote add template git@bitbucket.org:modularmx/template-g0.git
$ git fetch template
$ git rebase template/master

Prepare a new image for our cross-compiler, arm-none-eabi-gcc, using Alpine. The Dockerfile can be placed somewhere outside our repo if you don't want to version it, or you can place it in the .gitignore file. Which option is best is up to you.

# Fetch a new image from alpine version 3.20
FROM alpine:3.20

# install openocd version 12.0 revision 0
RUN apk add make=4.4.1-r2 gcc-arm-none-eabi=14.1.0-r0 newlib-arm-none-eabi=4.4.0.20231231-r0

#create and change directory to app
WORKDIR /app

# Create a volume using the app directory
VOLUME /app

Upload this image to Docker Hub or any other registry account you have, but it needs to be public (for now). I’m going to name my new image arm-none-eabi-gcc:14

$ docker build -t <username>/arm-none-eabi-gcc:14 .
$ doker push <username>/arm-none-eabi-gcc:14

Our pipeline will remain mostly the same, but with one very important new feature: we’ll add a first line with the name of our image to indicate that we want to use it in every step of our pipeline.

image: <username>/arm-none-eabi-gcc:14

pipelines:
  default:
      - step:
          name: Echo
          runs-on:
            - self.hosted
            - linux
          script:
            - echo "This step will run on a self docker runner.";
      - step:
          name: Build
          runs-on:
            - self.hosted
            - linux
          script:
            - make
💡
If for some reason you stop the Bitbucket docker container, you have to start it again using docker start <name of your container>

Git again, and this time push all your changes with the -f flag (because we rebased): git push origin master -f. See how our program builds flawlessly, and guess what? You don’t have anything installed on your local machine!

Multiple images

In the previous example, we set one Docker image for all the required steps, but we can also use different images for different steps. Let’s create an example for a unit test using Ceedling. First, add two files called dummy.h and dummy.c, and provide a simple add function

#ifndef __DUMMY_H__
#define __DUMMY_H__

uint32_t sum( uint32_t a, uint32_t b );

#endif // __DUMMY_H__

And dummy.c

#include <stdint.h>
#include "dummy.h"

uint32_t sum( uint32_t a, uint32_t b )
{
    return a + b;
}

Lucky for ya our template already comes prepare to use ceedling, you only need to locate the file test_dummy.c and fill with the following test case

#include "unity.h"
#include "dummy.h"

void setUp( void )
{
}

void tearDown( void )
{
}

void test__sum__two_integers( void )
{
    uint32_t res = sum( 2, 3 );
    TEST_ASSERT_EQUAL_MESSAGE( 5, res, "2 + 3 = 5" );
}

In our pipeline file, we set a new line for every step to indicate the image we want to use for that particular step. In the build step, we use our own image to build the project. In the second step, we use the official Ceedling image, which is also stored in Docker Hub. This is a great example of reusing something someone else has already done

pipelines:
  default:
      - step:
          name: Build
          image: account-name/arm-none-eabi-gcc:12
          runs-on:
            - self.hosted
            - linux
          script:
            - make
      - step:
          name: Test
          image: feabhas/ceedling
          runs-on:
            - self.hosted
            - linux
          script:
            - ceedling test:all

You know what to do, Git everything and push to see how our pipeline is running and execute both steps correctly

Well , there is more you can actually do with pipelines, consider this as a brief introduction but take your time to read the documentation to know how to properly configure your pipelines for the platform you choose named, Gitlab, Jenkins, Circle CI, etc..

Link to Bitbucket pipelines official documentation: https://support.atlassian.com/bitbucket-cloud/docs/build-test-and-deploy-with-pipelines/