Using GitHub Actions With Your C++ Project

GitHub_Actions and C++

Joseph Sibony

reading time: 

7 minutes

GitHub Actions are a way for developers and administrators to implement workflows based on code changes and events in a repository. Events can be a push (such as writing new C++ code), opening or changing a pull request (to merge updated C++ code), creating a new branch, opening or changing an issue, cron schedule, and much more.

Workflow steps are defined in YAML code and stored in the directory .github/workflows 

Actions execute on runners, which listen for available jobs and execute one job at a time to completion. By design, runners are installed in a container hosted in the GitHub virtual environment or optionally self-hosted by administrators. 

Why Use GitHub Actions with your C++ project?

GitHub Actions offer a very effective vehicle for standardizing and automating all manner of work related to code – from compiling C++ code to performing dependency checks to executing testing and more. By implementing repeatable and portable processes for activities, businesses can significantly raise confidence in the code being deployed. Actions can also increase developer velocity by improving the time to isolate code issues through testing, reporting, and notification as well as eliminating manual Continuous Integration and Continuous Deployment (CI/CD) activities such as performing security analysis, initiating tests, etc. 

For those already using other DevOps tools such as Jenkins or Codeship – Actions may appear to have several overlaps with processes already in place. Such teams may most likely benefit from Actions in either of two ways:

1. Migration – Many CI/CD frameworks already implement processes similar to Actions – such as Codeship Steps and Services – and teams already invested in such frameworks may find that the benefits of consolidating within GitHub outweigh the effort of translating existing processes into Actions. Benefits could include:

  • consolidating vendors by eliminating disparate CI/CD tools
  • cost savings
  • better integration into GitOps processes
  • greater reliability by shifting testing and validation of C++ code left

2. Capabilities – Actions aren’t necessarily mutually exclusive with other established processes; some teams find that when adding a capability – such as testing for a new library or automatically updating dependencies – Actions can be a quick and effective path to success.

Actions may be best understood with a practical example. 

GitHub Actions C++ – Let’s Combine Them

In this example we will:

  • create a simple C++ program to print “Hello, World!”
  • write supporting code for compiling the program such as a configure script and a Makefile
  • add a test to validate the code
  • implement a GitHub Action, that will compile and test the C++ code on any push or pull request to the main branch.

The code presented in this example is quite simple, nevertheless, if you wish to follow the example without creating it on your own by typing or copying the code from this post, you can find the example code here in GitHub.

Enable Actions

Make sure that the option “Allow all actions” is checked:

Enable Actions_GitHub Actions C++

Write The Code

A simple Hello World program will provide the basis for the work (hello.cpp):

#include <iostream>

int main()
{
    std::cout << "Hello, World!" << std::endl;
}

Write a configuration script named configure: 

CXX=g++          # The C++ compiler
CXXFLAGS=-g       # C++ complilation flags
NATIVE=on        # compiles code targeted to current hardware
TUNE=generic     # performance-tuning switch

And a Makefile: 

all:
      g++ -std=c++17 hello.cpp -o hello

clean:
      $(RM) hello

Manually test the code: 

$ ./configure && make && ./hello
g++ -std=c++17 hello.cpp -o hello
g++ -std=c++17 helloTest.cpp -lcppunit -o helloTest
Hello, World!

Write A Test

With the code working, write a CppUnit test (helloTest.cpp):

#include <iostream>
#include <cppunit/TestRunner.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/BriefTestProgressListener.h>
#include <cppunit/extensions/TestFactoryRegistry.h>

class Test : public CPPUNIT_NS::TestCase
{
  CPPUNIT_TEST_SUITE(Test);
  CPPUNIT_TEST(testHelloWorld);
  CPPUNIT_TEST_SUITE_END();

public:
  void setUp(void) {}
  void tearDown(void) {}

protected:
  void testHelloWorld(void) {
    system("./hello >nul 2>nul");
  }
};


CPPUNIT_TEST_SUITE_REGISTRATION(Test);

int main()

{
  CPPUNIT_NS::TestResult controller;

  CPPUNIT_NS::TestResultCollector result;
  controller.addListener(&result);

  CPPUNIT_NS::BriefTestProgressListener progress;
  controller.addListener(&progress);

  CPPUNIT_NS::TestRunner runner;
  runner.addTest(CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest());
  runner.run(controller);

  return result.wasSuccessful() ? 0 : 1;
}

Update the Makefile to include the test code and add a test rule: 

all:
      g++ -std=c++17 hello.cpp -o hello
      g++ -std=c++17 helloTest.cpp -lcppunit -o helloTest

test:
      chmod +x hello
      ./helloTest

clean:
      $(RM) hello helloTest

Again, manually test the code: 

$ ./configure && make && make test
g++ -std=c++17 hello.cpp -o hello
g++ -std=c++17 helloTest.cpp -lcppunit -o helloTest
chmod +x hello
./helloTest
Test::testHelloWorld : OK

Write The Action

With code and tests written, it’s time to add a GitHub Action. Actions are stored by default in the .github/workflows/ directory as YAML files, such as .github/workflows/helloAction.yml 

The name directive configures a string that will be displayed in the Actions dialog in GitHub. 

The on directive tells the Action when to run, which can be as general or as granular as needed corresponding to events – such as a push – and restricted to specific branches – such as main. 

Action workflows are made up of one or more jobs, which run in parallel by default. Jobs define where steps should be run – steps below – and what actions to take in the step – such as commands, setup tasks, or other actions in the repository. This Action will use primarily commands – such as make and configure – but also import the action checkout from upstream. 

name: C/C++ CI

on:
  push:
      branches: [ main ]
  pull_request:
      branches: [ main ]

jobs:
  build-and-test:
      runs-on: ubuntu-latest
      steps:
      - uses: actions/checkout@v2
      - name: install cppunit
      run: sudo apt install -y libcppunit-dev
      - name: configure
      run: ./configure
      - name: make
      run: make
      - name: make test
      run: make test

 Commit The Code

Add the code to the repository:

$ git add hello.cpp helloTest.cpp Makefile configure .github/workflows/helloAction.yml
$ git commit -m "initial commit" -a
$ git push origin main

Check GitHub

Observe the results of the Action:Observe the results of the Action_GitHub Actions C++

The Action can also be expanded to view step details: step details_GitHub Actions C++

Test A Failure

Certainly, successful tests are the goal, but it’s always prudent to test with a known failure when implementing something for the first time. To that end, modify the helloTest.cpp code to force a failure:

#include <iostream>
#include <cppunit/TestRunner.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/BriefTestProgressListener.h>
#include <cppunit/extensions/TestFactoryRegistry.h>

class Test : public CPPUNIT_NS::TestCase
{
CPPUNIT_TEST_SUITE(Test);
CPPUNIT_TEST(testHelloWorld);
CPPUNIT_TEST(failHelloWorld);
CPPUNIT_TEST_SUITE_END();

public:
  void setUp(void) {}
  void tearDown(void) {}

protected:
  void testHelloWorld(void)
  {
    system("./hello >nul 2>nul");
  }

  void failHelloWorld(void)
  {
    exit(1);
  }

};


CPPUNIT_TEST_SUITE_REGISTRATION(Test);

int main(int ac, char **av)
{
  CPPUNIT_NS::TestResult controller;

  CPPUNIT_NS::TestResultCollector result;
  controller.addListener(&result);

  CPPUNIT_NS::BriefTestProgressListener progress;
  controller.addListener(&progress);

  CPPUNIT_NS::TestRunner runner;
  runner.addTest(CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest());
  runner.run(controller);

  return result.wasSuccessful() ? 0 : 1;
}

Confirm with a manual test: 

$ ./configure && make && make test
g++ -std=c++17 hello.cpp -o hello
g++ -std=c++17 helloTest.cpp -lcppunit -o helloTest
chmod +x hello
./helloTest
Test::testHelloWorld : OK
Test::failHelloWorldMakefile:6: recipe for target 'test' failed
make: *** [test] Error 1

Commit and push the code changes: 

$ git commit -m "test failure" helloTest.cpp && git push origin main
[main 8b39841] test failure
 1 file changed, 3 insertions(+), 3 deletions(-)
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 311 bytes | 155.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:jonathanmhurley/demo.git
   3828a97..8b39841  main -> main

Observe the results in GitHub: 

Observe the results in GitHub_1_GitHub Actions C++

Observe the results in GitHub_2_GitHub Actions C++

Of course, we could have tested a failure, and in many cases, this is what actually being done, by mutating the unit under test (i.e. the tested code), so for example we could have instead of imitating a failure in the test code, perform a temporary change to our hello.cpp code, make it print something else and see that the test catches it and fails.

GitHub Actions C++ – Conclusion

Although this example is using GitHub actions C++ and Make, GitHub Actions are a good fit for any other language and project type. Examples for using GitHub actions for other languages can be found in GitHub guides, here for python, and here for Java. A guide for deploying code to AWS cloud using GitHub actions can be found in GitHub guides here.

GitHub Actions are for everyone, but not necessarily everything, there are still processes that require some manual handling or are too costly for setting as an automatic trigger. Usually writing an action would be most effective after you establish and stabilize the process that you wish to trigger. Before rushing to write all your imagined triggers as a GitHub action, be sure to balance the effort with your other tasks and timelines, to use it in a productive fashion. But when you see that a certain operation repeats itself, based on operations done in your repo, do consider using GitHub actions to automate it. 

speed up c++