An Introduction to Github Actions for Firmware Developers

Most firmware engineers are familiar with GitHub as a code hosting platform for version controlled projects. However GitHub also has a very powerful automation tool called GitHub Actions. Since GH Actions is built into your GH repository it's very easy to integrate into your workflow.

For example, with GH Actions you can easily do all of the following on a {% c-line %}git push{% c-line-end %}:

  1. Run a code linter
  2. Build your project
  3. Run unit-tests (and detect failures)
  4. Create assets (e.g. merged hex image)

Adding GitHub Actions to your repository is straightforward:

  1. Create a top-level folder structure called {% c-line %}.github/workflows{% c-line-end %}
  2. Add 1 or more workflow files, e.g. {% c-line %}.github/workflows/build_actions.yml{% c-line-end %}
  3. In the workflow file add the actions you would like to perform

You can have multiple workflow files in your workflows folder. Each workflow is triggered by a specific event defined in the file. To demonstrate how easy it is to set up a simple workflow, fork and then clone the following project:

nRF52 Blinky Project

Now open the file {% c-line %}.github/workflows/build_actions.yml{% c-line-end %} in the project and let's walk through each section.

First we're going to describe the workflow and set which events will trigger it.

{% c-block language="yaml" %}
name: Simple pipeline to build firmware and save generated image #Describe workflow
on: [push] #trigger workflow on every push event
{% c-block-end %}

Next, we handle the possibility of multiple users triggering a workflow by enabling {% c-line %}concurrency{% c-line-end %}.

{% c-block language="yaml" %}
    group: DUT  #Name concurrency group
    cancel-in-progress: false #Allow in-progress workflow to continue to completion
{% c-block-end %}

GitHub Actions are organized by {% c-line %}jobs{% c-line-end %} and {% c-line %}steps{% c-line-end %}. A {% c-line %}job{% c-line-end %} consists of one or more {% c-line %}steps{% c-line-end %}. Jobs can run in parallel with other jobs and can also be configured to run conditionally, for example, to only run if a previous job succeeds. {% c-line %}steps{% c-line-end %} on the other hand always run in series, and are the set of instructions that a job is to carry out. 

We see that this workflow has a single job called {% c-line %}build{% c-line-end %}.

{% c-block language="yaml" %}
jobs: #jobs are independent tasks that can either run serially or in parallel, and can also depend on the outcome of other jobs
 build: #this the start of the job "build". this can be called anything
   runs-on: ubuntu-latest #what operating system this job will run on
{% c-block-end %}

Under the {% c-line %}build{% c-line-end %} job, there are four steps:

Step 1: Checkout the code

This step clones the project and pulls in any submodules.

{% c-block language="yaml" %}
- name: Checkout the code
  uses: actions/checkout@v2
    submodules: 'recursive'
{% c-block-end %}

Step 2: Run linter

This step will use {% c-line %}cpplint{% c-line-end %} to lint our source code before building it.

{% c-block language="yaml" %}
- name: Run linter
  uses: docker://lagerdata/devenv-cortexm-nrf52
    entrypoint: /usr/local/bin/cpplint
    args: /github/workspace/application/src/main.c
{% c-block-end %}

Step 3: Build Project

After verifying that our source code is properly linted, we'll go ahead and build the project

{% c-block language="yaml" %}
- name: Build Project
  uses: docker://lagerdata/devenv-cortexm-nrf52 #use build environment in docker image lagerdata/devenv-cortexm-nrf52 to build project
    entrypoint: /usr/bin/make #build project using make
{% c-block-end %}

Step 4: Save hex image

Finally we'll save the hex file so that we have a record of the image generated as part of this build.

{% c-block language="yaml" %}
- name: Save hex image
  uses: actions/upload-artifact@v2 #Save generated image
    name: blinky_hex_image
    path: _build/nrf52840_xxaa.hex
{% c-block-end %}

The last step {% c-line %}Save hex image{% c-line-end %} illustrates another advantage of using GH Actions to automate your builds. Because you can save assets as part of a workflow, which in turn is linked to a specific commit hash, you eliminate the need to include tagged images in the repository, avoiding repo bloat.

On a final note, it's really important that we stress how easy it is to set up build automations with GH Actions compared to setting up your own build server running Jenkins, etc. GH Actions requires just a single YAML file to get started, which massively reduces the friction and infrastructure costs needed to start taking advantage of more sophisticated techniques like continuous integration (CI).

Happy automating!

Want to stay up to date on the future of firmware? Join our mailing list.