Skip to main content

Hosting GitHub Actions and Running CI/CD on the Okdo ROCK 4 C+

In my last article, I got my ROCK board (230-6199) set up to start working on C++ projects. An important part of these projects, and likewise for any other software projects, is testing. As we work on and expand projects, we write tests of various types to ensure our code hasn't broken and that it will even compile for other people using our programs and libraries. This becomes even more important when multiple people are working on the same project who may not be aware of changes that other members of the team have made. This prompted me to look into the possibility of using the ROCK board as a little "server" that can handle automated testing and deployment for a GitHub repository.

The main advantage to doing this is that you won't have to pay GitHub (or anybody else) for any actions that we can perform on the ROCK board. The one drawback here is that we can only perform actions that can be done on ARM64 Linux, so YMMV with cross-compiling to deploy releases.

I did the following testing on a ROCK 4 C+ board with Debian Bullseye installed. If you wanted to use this setup to handle CI/CD (continuous integration / continuous deployment) on a repository permanently, I would recommend installing Ubuntu 20 Server and running the board headless with these instructions. If you're following along, I'm also assuming you have common build tools already installed, which I also go through in my previous article.

Something to Test With

First, I needed a repository to test with. You could do this with a project you're already working on, or set up a similar small and contrived example like I've done if you're just trying to get the hang of it. I set up this library to test with.

├── CMakeLists.txt
├── README.md
├── src
│   ├── lib.cpp
│   └── lib.hpp
└── tests
    └── tests.cpp
// lib.hpp

extern "C"
{
    int fib(int n);
}

// lib.cpp

#include "lib.hpp"

int fib(int n)
{
    if (n <= 1) {
        return n;
    } else {
        return fib(n - 1) + fib(n - 2);
    }
}
// tests.cpp

#include "../src/lib.hpp"

#include <catch2/catch_test_macros.hpp>

TEST_CASE("nth Fibonacci number is computed", "[fib]") {
    REQUIRE(fib(-10) == -10);
    REQUIRE(fib(0) == 0);
    REQUIRE(fib(1) == 1);
    REQUIRE(fib(5) == 5);
    REQUIRE(fib(10) == 55);
    REQUIRE(fib(20) == 6765);
    REQUIRE(fib(30) == 832040);
}
# CMakeLists.txt

cmake_minimum_required(VERSION 3.23)
project(cicd-test-lib)

include(FetchContent)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

FetchContent_Declare(Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v3.3.2
    GIT_SHALLOW ON
)
FetchContent_MakeAvailable(Catch2)

add_library(cicd-test-lib src/lib.cpp)
add_executable(tests tests/tests.cpp src/lib.cpp)

set_property(TARGET cicd-test-lib PROPERTY CXX_STANDARD 23)
if (CMAKE_BUILD_TYPE MATCHES Debug)
    target_compile_definitions(cicd-test-lib PRIVATE CICD-TEST-LIB_DEBUG)
endif()

target_compile_options(cicd-test-lib PRIVATE -Wall -Wextra -Wpedantic -Werror)

target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)

This C++ "library" has one function we can test that computes the nth Fibonacci number. I'm using CMake to compile it as a static library with the add_library function. I wrote tests for the function with the Catch2 library which I'm linking to with CMake after installing it using FetchContent. Note how tests.cpp is not part of the library, but is instead compiled to a separate executable with add_executable.

Ok, we have a little library to test with, time to turn it into a GitHub repository. I added a .gitignore file, created a new empty repository on GitHub, and ran the following in a terminal open in the directory of my library:

$ git init
$ git add README.md
$ git commit -m "first commit"
$ git branch -M main
$ git remote add origin https://github.com/ryanjeffares/cicd-test-lib.git
$ git push -u origin main

Self-Hosting an Actions Runner for the Repository

The first step to getting our CI/CD pipeline running is self-hosting an actions runner for the repository on the ROCK board as opposed to leaving it hosted by GitHub. This is very easy to do - on the GitHub repository in the browser, click "Settings", then "Actions", then "Runners", then "New self-hosted runner". Then with the correct OS and architecture selected (ARM64 Linux), follow the installation instructions (in a terminal open in the project directory):

Self Hosting - Runner image

At this point, I also added actions-runner/**/* to the .gitignore to make sure we don't push this folder to GitHub. After going through the configuration and running the run script, you should see an output similar to the following:

$ ./run.sh

√ Connected to GitHub

Current runner version: '2.305.0'
2023-07-04 12:35:03Z: Listening for Jobs

Now, we can set up actions that will be ran on the ROCK board as opposed to GitHub's servers, as long as the board is on with the runner connected.

Setting up Actions

An "action" in GitHub speak, if you weren't already aware, is a task performed in response to a commit or push to the repository's remote. These tasks usually include building the project, testing the build, and deploying the build, as long as everything in the pipeline was successful. Each of these steps are known as "jobs". For this example, let's set up jobs for building the library and tests and running the tests if the build was successful. Make the file .github/workflows/main.yml (and its enclosing folders) in the project. This is what I added to mine:

name: Build and Test on Okdo ROCK 4 C+

on:
  push:
    branches: [ main ]    # perform these jobs when we push to main

jobs:
  build:
    runs-on: self-hosted  # important - this job only runs on our self-hosted runner

    steps:
      - uses: actions/checkout@v2
      - name: Builds the library and tests
        run: |            # commands to run to perform the build
          mkdir build -p
          cmake -B build -S .
          cmake --build build

  test:
    runs-on: self-hosted  # important - this job only runs on our self-hosted runner
    needs: build          # only runs if the build job succeeded

    steps:
      - name: Runs tests
        run: |            # run the test executable
          ./build/tests

The success of each job is checked by the exit code of the commands that are ran - if the build fails, CMake will return a non-zero exit code, so the test job won't be run. The Catch2 library calls with make the test program return zero if the tests pass, or non-zero if any of them fail. With this file set up, we can push it to the repository and then we're ready to give it a try!

Start up the actions runner the same way as before. Then, in either another terminal on the ROCK board or on a different computer with a clone of the repository, make some changes such as adding a test case that will definitely pass, and push to the remote. In the terminal on the ROCK board with the actions runner, you should see output similar to the following:

2023-07-04 13:34:48Z: Running job: build
2023-07-04 13:41:07Z: Job build completed with result: Succeeded
2023-07-04 13:41:11Z: Running job: test
2023-07-04 13:41:24Z: Job test completed with result: Succeeded

You can also see the progress of the jobs in the browser in the GitHub repository by going into the "Actions" tab:

progress of the jobs in the browser in the GitHub repositor

If you add a test case that will definitely fail, for example, REQUIRE(fib(1) == 0), and do a push, you can see what happens when it fails:

When it fails

As you can see, GitHub recognises that our tests failed because of the non-zero exit code, and Catch2 is showing us where the problem was in the console output.

Conclusion and Further Reading

In this article, we've seen that the Okdo ROCK 4 C+ board (230-6199) is fully capable of being used in a CI/CD pipeline. We've seen how to self-host an actions runner for a GitHub repository on the board, and how to set up actions for that runner. I did this with a C++ project using Catch2 since that's my modus operandi - if this applies to you too then definitely check out the extensive documentation for Catch2 since it offers so much more than the simple example I have here. You can also use CMake and CTest to handle more complex testing setups. The same process of setting up the runner and actions will also apply to other programming languages and testing suites. You could also set up a deployment step that runs on successful completion of the tests - be it adding a build to a release on GitHub, running your webserver, etc. I hope this article was interesting/useful for you, thanks for reading!

I'm a software engineer and third-level teacher specialising in audio and front end. In my spare time, I work on projects related to microcontrollers, electronics, and programming language design.

Comments