SDPT Lab 7
Week 7 Lab Activity: Adding a Static Analysis Quality Gate
Introduction
In Week 6, you built a CI pipeline that automatically compiles and tests your Oven Controller code on every push. Today you will add a new stage that runs before the build: a lint stage that uses static analysis to catch entire classes of bugs that the compiler and the unit tests cannot.
By the end of this lab, you will have:
- Extended your Docker build environment to include
cppcheckandclang-tidy. - Created a
.clang-tidyconfiguration file at the root of your project. - Hooked clang-tidy into every CMake compile via
CMAKE_CXX_CLANG_TIDY. - Added a new
lintstage to your.gitlab-ci.ymlthat runs cppcheck. - Verified that introducing a deliberate bug causes the pipeline to fail and block a Merge Request.
- Fixed the deliberate bug and watched the pipeline turn green again.
Prerequisites
You should be starting from your finished Week 6 project, which has:
- A working
CMakeLists.txtfor the Oven Controller. - A passing Google Test suite.
- A
Dockerfileproducing a multi-arch build environment. - A
.gitlab-ci.ymlwith at leastbuildandteststages running on a custom GitLab Runner.
If any of those is missing or broken, fix it before continuing. Static analysis added on top of a broken pipeline does not help anyone.
Step 1: Create a Feature Branch
As always, do not work on main. Open an Issue first ("Add static analysis quality gate") and then create a feature branch:
git checkout main git pull git checkout -b feature/static-analysis
This branch will contain everything you do in this lab. At the end you will open a Merge Request just like in Week 2.
Step 2: Update the Dockerfile
Open your Dockerfile and add cppcheck and clang-tidy to the apt-get install line.
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
g++-aarch64-linux-gnu \
qemu-user-static \
libgtest-dev \
cppcheck \
clang-tidy \
&& rm -rf /var/lib/apt/lists/*
Rebuild the image locally to make sure it still works:
docker build -t oven-build:lint-test . docker run --rm oven-build:lint-test cppcheck --version docker run --rm oven-build:lint-test clang-tidy --version
You should see version strings for both tools. If either command is "not found," double-check the package names for your base image (Debian/Ubuntu use cppcheck and clang-tidy; on minimal images you may need clang-tools instead of clang-tidy).
Step 3: Create the .clang-tidy Configuration File
In the root of your repository (next to CMakeLists.txt), create a new file named exactly .clang-tidy (note the leading dot). Paste the following:
--- Checks: > -*, bugprone-*, cert-*, clang-analyzer-*, cppcoreguidelines-pro-*, cppcoreguidelines-slicing, misc-unused-*, modernize-use-nullptr, modernize-use-override, modernize-use-nodiscard, performance-*, portability-*, readability-braces-around-statements, readability-misleading-indentation, readability-redundant-*, -bugprone-easily-swappable-parameters, -cppcoreguidelines-pro-bounds-pointer-arithmetic WarningsAsErrors: '*' HeaderFilterRegex: '.*' FormatStyle: 'file'
A few notes on what this configuration does:
- The first line
-*disables every check, then we re-enable specific families. This is the safer approach because clang-tidy ships hundreds of checks and many are too noisy. WarningsAsErrors: '*'turns every remaining warning into an error. The pipeline will fail if any check fires.HeaderFilterRegex: '.*'tells clang-tidy to also analyze the project's headers (otherwise it skips them).- The two trailing
-...entries suppress specific checks that produce too many false positives in embedded code.
Commit this file:
git add .clang-tidy git commit -m "Add .clang-tidy configuration"
Step 4: Hook clang-tidy into CMake
Open CMakeLists.txt and add the following near the top, after the project(...) line but before any add_executable or add_library calls:
# Generate compile_commands.json for tooling
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Enable clang-tidy on every compile
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
if(CLANG_TIDY_EXE)
set(CMAKE_CXX_CLANG_TIDY
${CLANG_TIDY_EXE}
--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy)
message(STATUS "clang-tidy enabled: ${CLANG_TIDY_EXE}")
else()
message(WARNING "clang-tidy not found; lint disabled")
endif()
The reason we wrap it in if(CLANG_TIDY_EXE) is so that team members who do not have clang-tidy installed locally can still build. The CI environment does have it, so the pipeline will still enforce it.
Test it locally:
rm -rf build
docker run --rm -v $PWD:/work -w /work oven-build:lint-test \
bash -c "cmake -B build && cmake --build build"
You should see -- clang-tidy enabled: /usr/bin/clang-tidy in the configure step, and during the build clang-tidy runs alongside the compiler. If your code is clean, the build still succeeds. If clang-tidy finds something, the build fails with a clang-tidy error message.
Step 5: Add a Cppcheck Custom Target
cppcheck does not need CMake to drive it (it parses source directly), but adding a CMake target makes it convenient to run locally. Append this to CMakeLists.txt:
# Cppcheck target (optional locally, mandatory in CI)
find_program(CPPCHECK_EXE NAMES cppcheck)
if(CPPCHECK_EXE)
add_custom_target(cppcheck
COMMAND ${CPPCHECK_EXE}
--enable=warning,style,performance,portability
--inline-suppr
--error-exitcode=1
--suppress=missingIncludeSystem
--std=c++17
-I ${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/src
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running cppcheck on src/")
endif()
Adjust the -I include path and the source path to match your project layout if it differs.
You can now run cppcheck locally with:
cmake --build build --target cppcheck
If your code is clean, the command prints nothing and exits with status 0.
Commit your CMake changes:
git add CMakeLists.txt Dockerfile git commit -m "Wire clang-tidy and cppcheck into the build"
Step 6: Add the lint Stage to GitLab CI
Open .gitlab-ci.yml. Add lint as the first stage in your stages: list:
stages: - lint - build - test
Then add a new job that runs cppcheck. Place it above your existing build job:
cppcheck:
stage: lint
image: $CI_REGISTRY_IMAGE/oven-build:latest
script:
- cppcheck
--enable=warning,style,performance,portability
--inline-suppr
--error-exitcode=1
--suppress=missingIncludeSystem
--std=c++17
-I include
src
Note: clang-tidy is not a separate CI job. Because we wired it into CMAKE_CXX_CLANG_TIDY, it runs during the existing build job, on every translation unit. If clang-tidy reports an error, the build job fails just as if the compiler had failed. This is exactly the behavior we want: one pipeline run, multiple gates.
Commit and push your branch:
git add .gitlab-ci.yml git commit -m "Add lint stage running cppcheck" git push -u origin feature/static-analysis
Open a Merge Request from feature/static-analysis into main. The pipeline should run, the new lint stage should appear, and (assuming your code is clean) all stages should pass.
Step 7: Trigger a Failure on Purpose
Quality gates are only useful if they actually catch things. Let us prove ours does.
Pick any .cpp file in your project --- for example src/OvenController.cpp. Add the following intentionally broken function near the top of the file, but inside the same namespace and class so it actually compiles:
// Intentional bug for Week 7 lab. Remove before merge.
void OvenController::leakyDiagnostic() {
int* readings = new int[100];
if (sensors_.empty()) {
return; // <-- LEAK: never delete[] readings
}
for (int i = 0; i < 100; ++i) {
readings[i] = i;
}
delete[] readings;
}
Also declare it in the corresponding header. Commit and push:
git add src/OvenController.cpp include/OvenController.hpp git commit -m "Add deliberate leak (lab demo, will revert)" git push
Now watch your Merge Request. The new pipeline run should:
- Start the
lintstage. - cppcheck reports the leak and exits non-zero.
- The
lintjob turns red. - The
buildandtestjobs are skipped (becauselintfailed). - The Merge Request is automatically blocked from merging.
Click into the failed job and read the cppcheck output. You should see something close to:
src/OvenController.cpp:42:5: error: Memory leak: readings [memleak]
This is exactly what should happen. The pipeline did its job.
Step 8: Fix and Merge
Revert the deliberate bug:
git revert HEAD git push
(Or remove the function manually and commit. Either is fine.)
The new pipeline should pass all stages: lint green, build green (with clang-tidy clean), test green. Merge the MR via GitLab.
You now have a working, multi-stage CI pipeline that enforces both unit tests and static analysis on every change to the codebase.
Deliverables
To be marked as complete, your main branch must contain:
- An updated
Dockerfilewithcppcheckandclang-tidyinstalled. - A
.clang-tidyconfiguration file at the repo root. - A
CMakeLists.txtthat exportscompile_commands.json, setsCMAKE_CXX_CLANG_TIDY, and defines acppcheckcustom target. - A
.gitlab-ci.ymlwith alintstage running cppcheck before thebuildstage. - A merged Merge Request whose pipeline shows four green jobs (lint, build, test, plus any others you already had).
- Evidence in the MR's pipeline history of at least one failed pipeline caused by the deliberate bug, followed by the fix.
Common Issues
clang-tidy reports many warnings on third-party headers (Google Test, system includes).
- Make sure
HeaderFilterRegexin.clang-tidyonly matches your own headers, e.g.'^src/|^include/'.
cppcheck complains about missing system headers.
- That is what
--suppress=missingIncludeSystemis for. Make sure you kept that flag.
clang-tidy fails because it cannot find compile flags.
- Confirm that
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)is inCMakeLists.txt, thatbuild/compile_commands.jsonexists after configure, and that you ran CMake before clang-tidy.
The pipeline runs forever, then times out.
- clang-tidy is significantly slower than the compiler. If a full build now takes more than 10 minutes, consider narrowing your
Checks:list. Start tight, loosen as you learn which checks pull their weight.
My capstone project codebase already has dozens of warnings.
- That is normal for an existing codebase. Two strategies: (1) fix them all in one MR, or (2) configure clang-tidy to warn-but-not-fail until you have cleaned things up. Option (1) is cleaner. Option (2) is realistic when you inherit legacy code.
Looking Ahead
Next week we leave the CI pipeline alone and turn to the system architecture of your capstone: how does your Raspberry Pi actually talk to the VM server? Sockets, REST, MQTT, and the design trade-offs between them.