Introduction: When "It Works on My Machine" is a Disaster
Every C++ developer has heard the phrase at least once: "It works for me, strange that it crashes for you." Or they've said it themselves. Template compilation, linking, quirks of different standards (C++17, C++20), platform-dependent macros — C++ is full of surprises that surface when the environment changes.
A CI/CD pipeline (Continuous Integration / Continuous Deployment) for C++ is not a luxury, but a necessity. It automatically builds the project, runs tests, checks code quality, and, upon success, deploys the application to a server (or publishes a library). In this article, we will set up a full-fledged pipeline from scratch using GitHub Actions, CMake, and C++. You will get a ready-made configuration that you can adapt to your own project.
1. Project Preparation: Structure and Build System
Before writing the pipeline, you need to get the project in order. We will use CMake — the de facto standard for modern C++ projects. Our example will be a simple string processing utility with unit tests using Google Test.
Repository Structure
my_cpp_project/├── .github/│ └── workflows/│ └── ci.yml # Pipeline file├── src/│ ├── main.cpp│ └── string_utils.cpp├── include/│ └── string_utils.hpp├── tests/│ ├── CMakeLists.txt│ └── test_string_utils.cpp├── CMakeLists.txt # Root CMake├── Dockerfile # For final deployment└── README.mdRoot CMakeLists.txt
We build the library and the executable file. Note the export of tests:
cmake_minimum_required(VERSION 3.20)project(MyStringUtils VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Main libraryadd_library(string_utils STATIC src/string_utils.cpp include/string_utils.hpp)target_include_directories(string_utils PUBLIC include)
# Executable fileadd_executable(my_app src/main.cpp)target_link_libraries(my_app PRIVATE string_utils)
# Tests (optional, enabled via option)option(BUILD_TESTS "Build unit tests" ON)if(BUILD_TESTS) enable_testing() add_subdirectory(tests)endif()Example Test (tests/test_string_utils.cpp)
#include <gtest/gtest.h>#include "string_utils.hpp"
TEST(StringUtilsTest, ToUpper) { EXPECT_EQ(to_upper("hello"), "HELLO"); EXPECT_EQ(to_upper("C++20"), "C++20");}
TEST(StringUtilsTest, Trim) { EXPECT_EQ(trim(" spaced "), "spaced"); EXPECT_EQ(trim("\\t\\r"), "");}Now that the project is ready, let's move on to the main part — setting up the CI/CD pipeline for C++.
2. GitHub Actions: Writing Your First Pipeline
GitHub Actions is the most popular tool for CI/CD in Open Source. Its configuration is written in YAML and resides in the .github/workflows folder. Let's create the ci.yml file.
Basic Structure: Trigger on Push and PR
name: C++ CI/CD Pipeline
on: push: branches: [ main, develop ] pull_request: branches: [ main ]
jobs: build-and-test: runs-on: ubuntu-latest
steps: - name: Checkout code uses: actions/checkout@v4
- name: Install dependencies run: | sudo apt-get update sudo apt-get install -y cmake g++ libgtest-dev
- name: Configure CMake run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build run: cmake --build build --parallel $(nproc)
- name: Run tests run: cd build && ctest --output-on-failureThis is a minimal but already functional CI/CD pipeline for C++. If the tests fail, the pipeline ends with an error, and you receive a notification. But we will go fu