In a previous post, I built a basic CI pipeline with CTest that uploaded its results to a CDash board. It worked fine except that we had to start the pipeline manually from the command line. In this post, we define a pipeline with GitHub Actions that runs the CI pipeline whenever a developer pushes a code change to a GitHub repository.
GitHub Actions in a Nutshell
It’s a tiny nutshell, as I am “only” interested in running the CTest-based CI pipeline – at least for now. My plan is to improve the pipeline iteratively.
GitHub Actions is a platform for building continuous integration and continuous deployment (CI/CD) pipelines. We specify a CI/CD pipeline in a workflow. A workflow consists of one or more jobs. Each job executes one or more steps, predefined actions or custom scripts. Certain events on the repository (e.g., a push or merge request) trigger the execution of the workflow.
Jobs run on a computer in the GitHub cloud or on a local computer, that is, on GitHub-hosted runners or on self-hosted runners. GitHub-hosted runners execute the workflow in a virtual machine (Linux, Windows or macOS). The workflow would have to install additional software such as Qt or an embedded Linux SDK every time the workflow is run. This may quickly become time and money consuming, because billing is by the minute.
Hence, we probably fare better with a self-hosted runner. For my first steps with GitHub Actions, my Ubuntu 20.04 workstation will do. In a later step, I will probably move to Docker containers.
Setting up a Self-Hosted Runner
We go to the main page of the repository (for me: the private repository foss-compliance), navigate to the page Settings > Actions > Runners and press the button New self-hosted runner. We select Linux as the Runner image and x64 as the Architecture. We follow the instructions on the web page. The instructions differ in the repository URL, the runner version, the operating system and the configuration token.
Downloading and installing the runner:
$ mkdir actions-runner && cd actions-runner
$ curl -o actions-runner-linux-x64-2.292.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.292.0/actions-runner-linux-x64-2.292.0.tar.gz
$ echo "14839ba9f3da01abdd51d1eae0eb53397736473542a8fae4b7618d05a5af7bb5 actions-runner-linux-x64-2.292.0.tar.gz" | shasum -a 256 -c
$ tar xzf ./actions-runner-linux-x64-2.292.0.tar.gz
We are free to choose the installation directory of the runner. I installed it in/public/Tools/actions-runners
, which I’ll refer to as $RUNNER_DIR
.
Configuring the runner:
./config.sh --url https://github.com/bstubert/foss-compliance --token ABCDEFGHIJKLMNOPQRSTUVWXYZ123
[...]
# Authentication
√ Connected to GitHub
# Runner Registration
Enter the name of the runner group to add this runner to: [press Enter for Default]
Enter the name of runner: [press Enter for godspeed]
This runner will have the following labels: 'self-hosted', 'Linux', 'X64'
Enter any additional labels (ex. label-1,label-2): [press Enter to skip]
√ Runner successfully added
√ Runner connection is good
# Runner settings
Enter name of work folder: [press Enter for _work]
√ Settings Saved.
For now, we use the default values for the four input requests by pressing Return. The runner will use $RUNNER_DIR/_work
as its working directory.
Starting the runner:
$ ./run.sh
√ Connected to GitHub
Current runner version: '2.292.0'
2022-06-06 08:40:19Z: Listening for Jobs
The runner is waiting for the first push to the repository.
Defining the Workflow for a CI Pipeline
We define the GitHub Actions workflow in the file .github/workflows/github-actions-ci-cd-pipeline.yml
in the root directory of our repository.
name: CI/CD Pipeline
on: [push]
jobs:
Commit-Stage:
runs-on: self-hosted
steps:
- name: Checking out repository ${{ github.repository }}
uses: actions/checkout@v3
- name: Changing to working directory ${{ github.workspace }}
run: cd ${{ github.workspace }}
- name: Running FossCompliancePipeline with CTest
run: ctest -S ./FossCompliancePipeline.cmake
The workflow CI/CD Pipeline
has one job Commit-Stage
, which runs on the self-hosted runner set up earlier. The job is triggered when a developer integrates code changes on any branch with git push
. The job performs three steps.
- The first step checks out the repository into
$RUNNER_DIR/_work/foss-compliance/foss-compliance/
with version 3 of the predefined GitHub actioncheckout
. - The second step changes to the directory where the repository was checked out – as given in the first step.
- The third step runs the CTest-based pipeline for the
foss-compliance
project (build, test and coverage) and uploads the results to CDash – as described in my post Building a CI Pipeline with CTest and CDash.
When we push a change to the repository, GitHub Actions executes the workflow. We can watch the progress on the repository’s GitHub page under Actions > Workflows > CI/CD Pipeline.
Once finished, we click on the first workflow run “Named each step” to see some statistics about the job Commit-Stage.
We click on the button Commit-Stage to see the details of each step of the successful job.
The page shows the status (here a check mark for success), the name and the running time for each step. When we open the folder for a step, we see the log of executing the step. GitHub Actions added three more steps to the three steps of our workflow file: Set up job (step 1 in the image above), Post Run actions/checkout@v3 (step 5), and Complete job (step 6).
Step 4 – Run FossCompliancePipeline with CTest – builds the foss-compliance project, runs all unit tests, calculates the coverage and uploads the results to the CDash board – as described in my post Building a CI Pipeline with CTest and CDash. The dashboard for the first run of the CI/CD Pipeline looks as follows. Every further run of the pipeline adds another row in the tables.
Next Steps
Our rudimentary CI pipeline is running. It’s a good start, but not yet fit for the Continuous Delivery of a smart HMI running on a Qt embedded system. Here are some ideas how to improve the pipeline step by step.
- The pipeline only takes care of continuous integration (CI) but not of continuous deployment (CD). It could use the CMake installation commands and CPack or Conan to create deployable packages.
- Our pipeline lacks an Acceptance Stage complementing the Commit Stage (see also Chapters 8-10 in Dave Farley’s book Continuous Delivery Pipelines: How To Build Better Software Faster). The Commit Stage runs unit tests and builds the software. It ensures that we can integrate our code changes into the main branch with a high probability of not breaking anything. The Acceptance Stage runs acceptance tests on the software deployed to the embedded device. Running tests on the embedded device has some implications:
- The pipeline must cross-compile the software against an embedded Linux SDK built with Yocto.
- The pipeline must deploy the binaries to the device and run the acceptance tests on the device.
- In bigger projects than foss-compliance, we must optimise the Commit and Acceptance Stage to run in less than 10 minutes and less than 60 minutes, respectively. Reducing include dependencies, stopping the build if unit tests fail, using ccache and using more processor cores are some means for optimisation.
- For better reproducibility, we may want to run the pipeline in a Docker container instead of directly on a workstation.
- The workstation running the CI/CD pipeline should start the runner as a (systemd) service. The same is true for the CDash server.
- The pipeline could check for each push to main whether the code changes are covered by tests.
- We may want to replace the CDash board by a more powerful dashboard like Grafana.
Resources
- GitHub Actions: Documentation: Entry point for the documentation of GitHub Actions.
- Marketplace / Actions: Search for available GitHub Actions.