Pre-PR local checks¶
CI runs every lane in parallel and returns in minutes. Running the same checks locally before pushing trades human time for a reviewer-confidence margin we mostly don't need. This document defines what to run locally and when, so the wait before a push stays short and CI catches the rest.
Tiers¶
| Tier | When | What | Wall-clock |
|---|---|---|---|
| A — fast feedback | Every commit on the branch | cmake --build --preset debug --target junit for whatever preset matches the diff (gcc / clang / freertos-host) |
~30–60 s |
| B — pre-push | First push to the branch and any push that changes production source | A + format reflowed includes + misra_renumber.py, plus check_spdx_headers.py when a file was added |
~2–3 min |
| CI — everything else | After push | tidy, sanitize, coverage, Windows, BDD, integration, FreeRTOS host/cross, advisory IWYU, MISRA on cpputest |
runs in parallel |
IWYU is advisory. The lanes still run on every PR and the report is uploaded as an artifact, but findings no longer fail the build. Sweep the IWYU artifact when you do a release cleanup; do not treat it as a per-PR blocker.
Format-on-save in the editor handles formatting per-edit, so no separate
analyze-format step locally. If you skip an editor with format-on-save,
add a clang-format -i sweep over touched files to Tier A.
Path-gating Tier B¶
Tier B does MISRA-line-drift cleanup, so scope it to what changed:
- Touched only
Tests/,Bdd/Targets/,docs/,cmake/, or*.md: skip Tier B entirely. Push and let CI run. - Touched any
Core/Source/,Platform/*/Source/, or public-header file: runclang-format -iover touched files andscripts/misra_renumber.py --applyto update the suppressions. - Added any file under
Core/orPlatform/: runpython3 scripts/check_spdx_headers.py. It is a sub-second file scan, so it costs nothing to run on every push if you would rather not think about it.
Running Tier B¶
MISRA — fix line-number drift¶
When edits shift production lines, misra_suppressions.txt entries go
stale. Fix in one step:
# In any container that has cppcheck (all of them do):
scripts/misra_renumber.py # show proposed renumbers
scripts/misra_renumber.py --apply # write back updated suppressions
The script bails on genuine new findings (mismatched counts per rule+file); those need manual review. See the script's docstring.
IWYU (optional, advisory)¶
If you want a local look before push, the lane is still wired:
docker compose -f .devcontainer/docker-compose.yml run --rm clang \
bash -c 'cmake --preset iwyu && cmake --build --preset iwyu --target iwyu'
For FreeRTOS / Plus-TCP / lwIP / MbedTLS / FatFs trees, use freertos-host
with the clang-19 overrides instead:
docker compose -f .devcontainer/docker-compose.yml run --rm freertos-host \
bash -c 'cmake --preset iwyu \
-DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19 \
&& cmake --build --preset iwyu --target iwyu'
CI runs all three IWYU lanes advisory; findings appear in the iwyu-report,
iwyu-report-freertos-plustcp and iwyu-report-freertos-lwip artifacts and
don't block the build.
Licence headers¶
Every file under Core/ and Platform/ opens with the SPDX header, and none
of them may claim anyone else's copyright:
Both the copyright line and the licence expression are read out of LICENSE.md
at run time, so there is nothing to keep in step by hand — the check fails if
the tree and the licence disagree.
The second half is a tripwire rather than a style rule. Core/ and Platform/
contain no third-party code, and that invariant is what makes it safe to stamp
our copyright across every file in them. If it fires, the question is whether
the file belongs in the shipped library at all — there is deliberately no
allowlist.
Markdown¶
Markdown is linted in CI by the analyze-markdown lane (markdownlint-cli2
v0.22.1), wired into summary. The rules live in .markdownlint-cli2.jsonc,
our conventions (line-length and table-column-style off, fenced-code language
required); CHANGELOG.md and the verbatim licence texts under LICENSES/
are ignored.
If you touch any .md, lint the files you changed before pushing. Same pinned
engine as CI and CodeRabbit, via Docker (no Node needed):
# Collect the .md files changed on this branch. git's exit status is captured
# rather than consumed by a process substitution: if it fails — an unfetched
# origin/main, say — the array would silently be empty, both commands below
# would skip, and the check would report success having linted nothing.
# --diff-filter=ACMRT drops deletions, which would otherwise reach the linter as
# missing files, and the array keeps paths containing spaces intact:
diff_output=$(git diff --name-only --diff-filter=ACMRT origin/main...HEAD -- '*.md') \
|| echo 'Cannot list changed Markdown — is origin/main fetched?' >&2
changed=()
[[ -n "$diff_output" ]] && mapfile -t changed <<<"$diff_output"
# --no-globs is required: the "globs" entry in .markdownlint-cli2.jsonc is
# combined with any paths given on the command line, so without it the whole
# tree is linted whatever you pass. The "ignores" entry still applies.
if ((${#changed[@]})); then
docker run --rm -v "$PWD:/workdir" \
davidanson/markdownlint-cli2:v0.22.1 --no-globs "${changed[@]}"
fi
# Auto-fix the mechanical rules (blank lines, trailing space, list style, ...):
if ((${#changed[@]})); then
docker run --rm -v "$PWD:/workdir" \
davidanson/markdownlint-cli2:v0.22.1 --fix --no-globs "${changed[@]}"
fi
CI lints the whole tree, so a rule change or a config edit can surface findings
in files this branch did not touch. Run the no-argument form when you change
.markdownlint-cli2.jsonc itself.
With Node available, npx markdownlint-cli2@0.22.1 is equivalent. Fenced-code
languages (MD040) and a few structural rules are not auto-fixable; tag or
adjust those by hand.
What CI runs and you should not run locally¶
tidy,sanitize,coverage: minutes each, all gated by CIc99: thebuild-linux-c99lane builds the library at the C99 language standard on every PR. If it fails, either fix the construct or, if it genuinely belongs to a C11-only component, gate that component the wayPlatform/StdAtomicis gated. See builds.md- Windows MSVC + BDD + integration: depend on tools you may not have
- BDD-linux-syslog-ng, BDD-windows-otel, BDD-freertos-qemu: heavy multi-container stacks
If CI surfaces a finding you missed locally, fix in another commit on the same branch, cheaper than running every CI lane on every push.