Repository navigation
639 lines (617 loc) · 37.2 KB
/
Copy pathmakefile.yml
File metadata and controls
639 lines (617 loc) · 37.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
name: Build Check
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# ── Dependency pins ──────────────────────────────────────────────────────
# The pin SHAs live in ONE manifest, .github/ci-pins.env, loaded into the job
# environment by the early "Load dependency pins" step (below) so this workflow
# and native-build.yml (Coverity) read the same source of truth. To bump a
# dependency, change the SHA in ci-pins.env AND the matching fallback in setup.sh
# (the "Verify pin manifest" step enforces the match — a mismatch errors).
# setup.sh fallbacks keep `make setup` working locally without the workflow;
# HAL CI clones OneWifi/develop and runs its setup.sh, so those fallbacks are the
# cross-repo truth. See ci-README.md §7.
# Stage 1 is deliberately read-only: it builds PR-controlled makefiles, so it
# must hold no write scope. The PR-comment posting (writes) happens only in the
# trusted workflow_run stage (pr-comments.yml).
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-24.04
strategy:
matrix:
platform:
- name: "Raspberry Pi"
id: rpi # has to be stable for the ci-summary-<id> stage
makefile_path: "build/linux/rpi/makefile"
extra_coverage: "0"
extra_cflags: ""
- name: "Banana Pi R4 - MLO"
id: bpi
makefile_path: "build/linux/bpi/makefile"
extra_coverage: "1"
extra_cflags: "-DCONFIG_GENERIC_MLO"
clang_tidy: true # only this leg builds the compile DB + runs clang-tidy
- name: "Platform Mock Unittests"
id: mock
makefile_path: "build/linux/mockplatform/makefile"
extra_coverage: "0"
extra_cflags: ""
fail-fast: false
name: Build for ${{ matrix.platform.name }}
steps:
- name: Checkout current repository
uses: actions/checkout@v6
with:
path: 'OneWifi'
fetch-depth: 0 # clang-tidy diffs changed source against the PR base
persist-credentials: false # nothing here pushes; don't leave the token in .git/config
- name: Clone unified-wifi-mesh repository
run: |
mkdir -p easymesh_project
git clone https://github.com/rdkcentral/unified-wifi-mesh.git easymesh_project/unified-wifi-mesh
mv OneWifi easymesh_project/OneWifi
# ci-pins.env -> $PIN_* with a strict <40hex> gate. Shared with native-build.yml
# via the load-pins composite action (single source of truth for the loader). This
# leg moved OneWifi into easymesh_project/OneWifi, so both the action path and the
# manifest path carry that prefix (native-build.yml checks out at the repo root).
- name: Load dependency pins
uses: ./easymesh_project/OneWifi/.github/actions/load-pins
with:
manifest: easymesh_project/OneWifi/.github/ci-pins.env
- name: Set up dependencies
run: |
sudo apt-get update
# Pruned: removed mariadb-server (100MB, unused — OneWifi doesn't build the
# mesh agent that links -lmariadb), golang (runner has Go in toolcache),
# libmysqlcppconn-dev (0 includes/linkage), vim (CI doesn't need an editor).
# --no-install-recommends avoids pulling unneeded transitive packages.
sudo apt-get install -y --no-install-recommends \
build-essential \
cmake \
python3 \
python3-pip \
git \
libev-dev \
libjansson-dev \
zlib1g-dev \
libnl-3-dev \
libnl-genl-3-dev \
libnl-route-3-dev \
libavro-dev \
libcjson1 libcjson-dev \
libprotobuf-c-dev \
libssl-dev \
uuid-dev \
libreadline-dev \
iptables \
gnupg \
file \
pkg-config \
libperl-dev \
libjson-c-dev \
libgtest-dev \
libgmock-dev \
bear \
clang-tidy-18
# Steps for ucode installation required for hostapd compilation
git clone https://github.com/jow-/ucode.git ucode
cd ucode
git checkout "$PIN_UCODE"
# Assert the tree, not just the SHA: if this pin ever lands on a commit
# that defines the `unused` macro, fail here instead of deep in the HAL build.
if git grep -q 'define unused' HEAD -- include/; then
echo "::error::ucode pin $PIN_UCODE leaks the 'unused' macro (collides with OpenSSL). Check the pin — see the TRAP note in the manifest."
exit 1
fi
mkdir build && cd build
cmake -DUBUS_SUPPORT=OFF -DUCI_SUPPORT=OFF -DULOOP_SUPPORT=OFF ..
make -j"$(nproc)"
sudo make install
sudo ldconfig
# Guard: the manifest pins (ci-pins.env, loaded above) MUST match the setup.sh
# fallbacks. A mismatch means someone bumped only the manifest → the cache key
# (which hashes setup.sh) won't invalidate → CI silently builds the old pinned
# tree. HAL CI runs setup.sh directly (no manifest env vars), so fallback drift
# also means cross-repo divergence. Fail loud at setup time, not deep in a build.
- name: Verify pin manifest matches setup.sh fallbacks
if: matrix.platform.id != 'mock'
working-directory: easymesh_project/OneWifi
run: |
set -uo pipefail
# The pins must have loaded ("Load dependency pins"). An empty value here
# means the manifest failed to load — fail before the grep checks below
# silently pass on an empty needle. All three pins are named symmetrically
# so the SC2153 "possible misspelling" heuristic can't single one out (they
# are assigned via $GITHUB_ENV, which the linter cannot see statically).
if [ -z "${PIN_HOSTAP_2_11:-}" ] || [ -z "${PIN_HOSTAP_2_10:-}" ] || [ -z "${PIN_META_FILOGIC:-}" ]; then
echo "::error::a dependency pin is empty — the pin manifest did not load."
exit 1
fi
ok=true
check() { # usage: check MANIFEST_SHA SETUP_FILE PIN_NAME
local manifest="$1" file="$2" name="$3"
# Match the fallback expansion itself (${PIN_NAME:-<sha>}), not the SHA anywhere:
# swapped fallbacks or a stale SHA in a comment must not pass.
if ! grep -qF "\${${name}:-${manifest}}" "$file"; then
echo "::error::Pin mismatch: $name manifest='$manifest' is not the \${${name}:-...} fallback in $file"
ok=false
fi
}
if [ "${{ matrix.platform.id }}" = "bpi" ]; then
check "$PIN_HOSTAP_2_11" build/linux/bpi/setup.sh PIN_HOSTAP_2_11
check "$PIN_META_FILOGIC" build/linux/bpi/setup.sh PIN_META_FILOGIC
else
check "$PIN_HOSTAP_2_10" build/linux/rpi/setup.sh PIN_HOSTAP_2_10
fi
# native-build.yml (Coverity) must read its hostap pin from the manifest,
# never re-hardcode a SHA — a third copy would drift silently. Assert no
# bare 40-hex literal has crept back into it. Scope: native-build.yml only;
# cov_docker_script/component_config.json is the Coverity setup's own
# dependency list (not covered by ci-pins.env).
if grep -nE '[0-9a-f]{40}' .github/workflows/native-build.yml; then
echo "::error::native-build.yml carries a 40-hex literal — it must load pins from ci-pins.env, not hardcode a SHA."
ok=false
fi
$ok || exit 1
# Perf: each device leg (bpi, rpi) clones + patches hostap from upstream.
# The patched tree is a pure function of the pins in setup.sh plus the patch
# inputs, so the hostap-tree composite action caches it keyed on those files
# (patched-<leg> mode). On a hit, setup.sh's .hostap_patched flag skips the
# clone AND patching entirely; a miss falls back to the normal clone, so this
# can never be slower than today. The key logic (files hash + the one unpinned
# patch source per leg: bpi's meta-cmf-bananapi kernel_6_6 tree SHA, rpi's
# hostap-patches tip) lives in the action so Coverity (native-build.yml, plain
# mode) shares the same cache machinery. mock never clones hostap.
#
# NOTE: the local `uses:` path is ./easymesh_project/OneWifi/… NOT ./.github/… —
# the checkout lands at OneWifi/ then is mv'd under easymesh_project/OneWifi
# (above), so the action only exists at that sub-path (ci-README §10).
- name: Provide patched hostap tree (cached)
if: matrix.platform.id != 'mock'
uses: ./easymesh_project/OneWifi/.github/actions/hostap-tree
with:
mode: patched-${{ matrix.platform.id }}
path: easymesh_project/rdk-wifi-libhostap
github-token: ${{ github.token }}
- name: Setup OneWiFi for ${{ matrix.platform.name }}
working-directory: easymesh_project/OneWifi
run: |
# `make setup` creates local patch commits, so git needs a committer
# identity. Use a generic CI identity (not the PR actor) so no personal
# name ends up echoed in the build logs.
git config --global user.email "onewifi-ci@users.noreply.github.com"
git config --global user.name "OneWifi CI"
make -f ${{ matrix.platform.makefile_path }} setup
- name: Build OneWiFi for ${{ matrix.platform.name }}
id: build
working-directory: easymesh_project/OneWifi
run: |
# pipefail so make's exit status (not tee's) decides the step result.
set -o pipefail
# clang-tidy leg only: wrap make in `bear` so ONE build yields the objects
# AND compile_commands.json (no second build). Other legs run make plain.
${{ matrix.platform.clang_tidy && 'bear --output compile_commands.json --' || '' }} make -f ${{ matrix.platform.makefile_path }} -j"$(nproc)" all 2>&1 | tee build.log
env:
# Coverage tier for the bpi target: "1" enables EXTRA_COV_FLAGS
# (ONEWIFI_DB_SUPPORT + memwraptool). Ignored by rpi/mockplatform.
EXTRA_COVERAGE: ${{ matrix.platform.extra_coverage }}
# Extra defines appended to CFLAGS. bpi appends via 'CFLAGS +=' (so the
# MLO build picks up -DCONFIG_GENERIC_MLO); rpi/mockplatform use
# 'CFLAGS =' and ignore it. -j is safe: PROGRAM depends on libwifihal.a.
CFLAGS: ${{ matrix.platform.extra_cflags }}
- name: Build summary for ${{ matrix.platform.name }}
if: always() # run on failure too, so the red-build reason is surfaced
working-directory: easymesh_project/OneWifi
run: |
# ci-out/ collects the stage-1 artifacts the PR-comment stage posts.
mkdir -p ci-out
LOG=build.log
if [ ! -f "$LOG" ]; then
echo "### ${{ matrix.platform.name }} — no build log captured" \
| tee ci-out/build-summary.md >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# Compiler + linker + make errors: what usually explains a red build,
errs=$(grep -hE ': (error|fatal error):|undefined reference to|Error [0-9]+' "$LOG" \
| sed -E 's#[^ ]*/(OneWifi|rdk-wifi-hal|rdk-wifi-libhostap)/#\1/#; s#//#/#g' \
| sort -u || true)
# Surface the top errors as ANNOTATIONS (the box at the top of the failed
# check, which people actually check) — not only the run-summary page.
if [ -n "$errs" ]; then
head -10 <<< "$errs" | sed 's/%/%25/g; s/\r/%0D/g' \
| while IFS= read -r l; do echo "::error::$l"; done
fi
{
echo "### ${{ matrix.platform.name }} — build ${{ steps.build.outcome }}"
if [ -n "$errs" ]; then
echo "#### ❌ Errors ($(printf '%s\n' "$errs" | wc -l) unique)"
echo '```'
head -100 <<< "$errs"
echo '```'
fi
# Warnings from OneWifi's own sources ONLY, with location.
# HAL/hostapd/mesh errors still break the build, so will be shown too.
# OneWifi runs a Yocto-style clean baseline, so anything here
# is a newly-surfaced bug to triage.
# The leading 'easymesh_project/' gate keeps only in-tree sources; without it the
# sibling-exclusion below would still admit system/toolchain warnings (/usr/include,
# /usr/lib/gcc). Mirrors rdk-wifi-hal's makefile.yml so the two stay in sync.
warns=$(grep -hE ': warning:' "$LOG" \
| grep -E 'easymesh_project/' \
| grep -vE 'rdk-wifi-hal|rdk-wifi-libhostap|unified-wifi-mesh|halinterface|trower-base64' \
| sed -E 's#[^ ]*/OneWifi/+##; s#//#/#g' \
| sort -u || true)
if [ -n "$warns" ]; then
echo "#### ⚠️ OneWifi warnings ($(printf '%s\n' "$warns" | wc -l))"
echo '```'
head -100 <<< "$warns"
echo '```'
else
echo "#### ⚠️ OneWifi warnings: 0"
fi
} | tee ci-out/build-summary.md >> "$GITHUB_STEP_SUMMARY"
- name: clang-tidy (gate + warnings) for ${{ matrix.platform.name }}
if: always() && matrix.platform.clang_tidy
working-directory: easymesh_project/OneWifi
run: |
# clang-tidy over PR-changed .c/.cpp files (see .clang-tidy and EXTRA_CHECKS below):
# * WarningsAsErrors checks -> `error:` -> FATAL (this step fails the job)
# * rest of enabled bugprone-* -> `warning:` -> reported (summary + inline)
# * unreachable code -> `warning:` -> summary + annotations only, never inline
# (reachability depends on the build configuration's #ifdefs)
# clang-diagnostic-* (most false positives and most noisy) is filtered out, except the
# armed unreachable-code group; none of it can gate (it prints `warning:`). The gate is
# changed-LINES-scoped: a PR can only fail on a .c/.cpp LINE it touched (the per-file
# ranges filter below), so pre-existing findings on untouched lines never red it.
mkdir -p ci-out
if [ ! -f compile_commands.json ]; then
echo "#### 🔎 clang-tidy: no compile DB" >> "$GITHUB_STEP_SUMMARY"
# skipped (not clean) so the stage-2 poster leaves inline comments alone.
printf '%s' '{"source":"clang-tidy","status":"skipped","dropped":0,"comments":[]}' > ci-out/inline-tidy.json
exit 0
fi
BASE='${{ github.event.pull_request.base.sha }}'
CHANGED=''
diff_failed=false
DIFF_BASE="$BASE"
if [ -n "$BASE" ]; then
git fetch --no-tags --depth=1 origin "$BASE" >/dev/null 2>&1 || true
# Diff against the merge ref's real base parent (HEAD^1), not the frozen payload
# BASE: this step's checkout has no `ref:`, so HEAD is refs/pull/N/merge and HEAD^1
# is the CURRENT base. On a RE-RUN of an old run the merge ref re-resolves to today's
# base while $BASE stays pinned, so a two-dot $BASE..HEAD would blame post-fork
# base-branch changes on the PR (gate/line-scope fires on lines the author never
# touched). Use HEAD^1 when HEAD is a merge commit AND $BASE is its ancestor (the
# is-ancestor probe makes the parent-order assumption self-verifying); else keep
# $BASE. Mirrors gcc_diff_gate.py effective_base().
if git rev-parse --verify --quiet HEAD^2 >/dev/null \
&& git merge-base --is-ancestor "$BASE" HEAD^1 2>/dev/null; then
DIFF_BASE=HEAD^1
fi
# A changed file not in compile_commands.json (not built for this platform) gets
# flags borrowed from a nearby entry: some compile and are fully checked, most fail
# on includes (clang-diagnostic, filtered below). Counted as best-effort below.
# Exclude only the vendored hostap tree (sibling clone rdk-wifi-libhostap/), never
# first-party sources: '^build/' wrongly dropped build/linux/compat/coverage_stubs.c
# (compiled by the makefile, real DB entry) and bare 'hostap' dropped e.g.
# wifi_hostapd_glue.c.
# Anchored '^': git diff paths are repo-relative and the vendored tree, if ever
# surfaced, sits at the repo root -- so a first-party path that merely *contains*
# the name (e.g. source/.../rdk-wifi-libhostap-notes.c) is never dropped.
# A failed diff (bad or unfetched base) is not "no changed files": record it so the
# summary and the inline set say nothing was analyzed (gcc_diff_gate.py does the same).
if changed_all=$(git diff --name-only --diff-filter=ACM "$DIFF_BASE" HEAD -- '*.c' '*.cpp' 2>/dev/null); then
CHANGED=$(grep -vE '^rdk-wifi-libhostap/' <<< "$changed_all" || true)
else
diff_failed=true
fi
fi
# Extra checks this repo wants on top of the shared .clang-tidy config.
# --warnings-as-errors COMBINES with the config's WAE (proven clang-tidy-18);
# --checks APPENDS to the config's Checks. Both auto-inherit changed-lines
# scope because the awk filter below runs on output, not input.
# See clang-tidy-hal-extra-mechanism memory for the proof + HAL usage.
EXTRA_WAE='' # comma-separated bugprone checks to gate (empty = baseline only)
# Checks to ENABLE that .clang-tidy doesn't. The unreachable-code group is the dead-code
# proxy: clang reports nothing for it unless the warning is on, hence the extra args
# (-aggressive adds dead returns and loops). No compile DB entry carries a bare -Werror
# today; -Wno-error= keeps a future one from turning these into gated `error:` lines.
EXTRA_CHECKS='clang-diagnostic-unreachable-code*'
extra=()
[ -n "$EXTRA_WAE" ] && extra+=(--warnings-as-errors="$EXTRA_WAE")
[ -n "$EXTRA_CHECKS" ] && extra+=(--checks="$EXTRA_CHECKS")
extra+=('--extra-arg=-Wunreachable-code' '--extra-arg=-Wunreachable-code-aggressive'
'--extra-arg=-Wno-error=unreachable-code-aggressive')
: > tidy.log
# Changed files clang-tidy could not fully analyze: a crash or missing binary
# (exit >= 2), or a compile error in a file the build compiled. A file outside
# the compile DB is not built for this platform; clang-tidy borrows flags from
# a nearby entry and most of those fail to compile, which is expected there:
# counted in the best-effort line, not listed. Either kind makes the inline set
# partial, since a compile error can hide a finding that is still there.
tfail_list="$RUNNER_TEMP/tidy-failed.txt"
bfail_list="$RUNNER_TEMP/tidy-besteffort-failed.txt"
: > "$tfail_list"; : > "$bfail_list"
if [ "$diff_failed" = true ]; then
echo "git diff against the PR base failed: changed files unknown, nothing analyzed" >> "$tfail_list"
fi
db_files="$RUNNER_TEMP/tidy-db-files.txt"
dbok=true
python3 -c 'import json, os; print("\n".join(os.path.realpath(os.path.join(e["directory"], e["file"])) for e in json.load(open("compile_commands.json"))))' \
> "$db_files" || dbok=false # unreadable DB -> treat every file as built (stricter)
# clang-tidy itself exits 0 with no output on an unreadable DB (the error goes to
# stderr), so record it: the run cannot read as clean.
if [ "$dbok" = false ] && [ -n "$CHANGED" ]; then
echo "compile_commands.json could not be read: changed files not analyzed with their build flags" >> "$tfail_list"
fi
# A .clang-tidy that does not parse is not fatal either: clang-tidy prints the error to
# stderr, falls back to its default checks and exits 0. --verify-config reports it, but
# not in its exit code, so match the text.
if [ -n "$CHANGED" ]; then
cfg_err=$(clang-tidy-18 --verify-config "${extra[@]}" 2>&1 | grep -m1 ': error: ' \
| sed -E 's#^[^ ]*/OneWifi/+##' | tr -d '`' || true)
if [ -n "$cfg_err" ]; then
echo "clang-tidy configuration invalid, default checks used: $cfg_err" >> "$tfail_list"
fi
fi
besteffort=0 # changed files outside the DB: no gcc gate, best-effort clang-tidy only
for f in $CHANGED; do
[ -f "$f" ] || continue
# New-side line RANGES this PR changed in *this* file (zero-context hunks), one
# "start:end" per hunk in a file. Never expand them into a command-line argument:
# a large added file would overflow it (E2BIG), and the || true below would then
# mask that as "clean".
ranges="$RUNNER_TEMP/tidy-ranges.txt"
if ! git diff -U0 --diff-filter=ACM "$DIFF_BASE" HEAD -- "$f" 2>/dev/null \
| awk '/^@@ /{c=$3;sub(/^\+/,"",c);n=1;if(c~/,/){split(c,a,",");c=a[1];n=a[2]}
if(n>0)print c":"c+n-1}' > "$ranges"; then
echo "$f: git diff of this file failed, not analyzed" >> "$tfail_list"; continue
fi
[ -s "$ranges" ] || continue
indb=true
if [ "$dbok" = true ] && ! grep -qxF "$(realpath "$f")" "$db_files"; then
indb=false; besteffort=$((besteffort + 1))
fi
# Exit 1 = a WarningsAsErrors finding or a compile error, both an "error:" on stdout;
# exit 1 without one is a configuration or invocation failure, told only on stderr
# (e.g. "no checks enabled"). >= 2 = crash / not found.
out="$RUNNER_TEMP/tidy-out.txt"
err="$RUNNER_TEMP/tidy-err.txt"
rc=0
clang-tidy-18 -p . "${extra[@]}" --quiet "$f" > "$out" 2> "$err" || rc=$?
# Unreachable-code findings only from a file that parsed with its own flags: a
# misparse (borrowed flags outside the DB, or a compile error) reports false ones.
dead=$indb
if [ "$rc" -ge 2 ]; then
echo "$f: clang-tidy exit $rc" >> "$tfail_list"
elif grep -q 'clang-diagnostic-error' "$out"; then
dead=false
dest="$tfail_list"; [ "$indb" = true ] || dest="$bfail_list"
grep -m1 'clang-diagnostic-error' "$out" \
| sed -E 's#^[^ ]*/OneWifi/+##; s# \[clang-diagnostic-error\]$##' >> "$dest"
elif [ "$rc" -ne 0 ] && ! grep -qE ':[0-9]+:[0-9]+: error: ' "$out"; then
why=$( { grep -m1 -E '[Ee]rror' "$err" || grep -m1 . "$err"; } \
| sed -E 's#^[^ ]*/OneWifi/+##' | tr -d '`' || true)
echo "$f: clang-tidy exit $rc: ${why:-no message}" >> "$tfail_list"
continue # stdout is not findings here (e.g. the usage text)
fi
# || true: a no-match grep (exit 1) must not abort the loop.
# clang-tidy runs one file at a time -> every finding is this file's, so keep only the
# ones whose LINE the PR changed. Filter on the line number alone: matching the
# sed-normalized display path against git-diff paths is fragile (silently drops all).
grep -E ': (warning|error):' "$out" \
| awk -v dead="$dead" '!/clang-diagnostic/ || (dead == "true" && /\[clang-diagnostic-unreachable-code/)' \
| awk -F: 'NR==FNR{lo[NR]=$1;hi[NR]=$2;r=NR;next}
{l=$2+0; a=1; b=r # ranges are sorted: binary search
while(a<=b){m=int((a+b)/2); if(l<lo[m])b=m-1; else if(l>hi[m])a=m+1; else {print; next}}}' \
"$ranges" - >> tidy.log || true
done
# WarningsAsErrors-promoted checks print as `error:`; everything else as `warning:`,
# with the unreachable-code findings in their own bucket (summary only, never inline).
norm='s#[^ ]*/OneWifi/+##; s#//#/#g'
unreach='\[clang-diagnostic-unreachable-code'
gated=$(grep -E ': error:' tidy.log | sed -E "$norm" | sort -u || true)
advis=$(grep -E ': warning:' tidy.log | grep -vE "$unreach" | sed -E "$norm" | sort -u || true)
deadc=$(grep -E ': warning:' tidy.log | grep -E "$unreach" | sed -E "$norm" | sort -u || true)
tfail=$(sort -u "$tfail_list")
nbfail=$(wc -l < "$bfail_list") # best-effort files that did not compile
if [ -n "$tfail" ]; then
echo "::warning::clang-tidy coverage incomplete ($(wc -l <<< "$tfail") item(s)); see the summary."
fi
if [ -n "$gated" ]; then
head -10 <<< "$gated" | sed 's/%/%25/g; s/\r/%0D/g' \
| while IFS= read -r l; do echo "::error::$l"; done
fi
if [ -n "$advis" ]; then
head -10 <<< "$advis" | sed 's/%/%25/g; s/\r/%0D/g' \
| while IFS= read -r l; do echo "::warning::$l"; done
fi
# Unreachable code is not posted inline, so annotations are its per-line surface.
if [ -n "$deadc" ]; then
head -10 <<< "$deadc" | sed 's/%/%25/g; s/\r/%0D/g' \
| while IFS= read -r l; do echo "::warning::$l"; done
fi
{
if [ -z "$CHANGED$tfail" ]; then echo "#### 🔎 clang-tidy: no changed .c/.cpp to scan"
elif [ -z "$gated$advis$deadc$tfail" ] && [ "$nbfail" -eq 0 ]; then echo "#### 🔎 clang-tidy: clean on changed lines"
elif [ -z "$gated$advis$deadc$tfail" ]; then
# Nothing found, but not every file compiled: not "clean".
echo "#### 🔎 clang-tidy: no findings on changed lines, best-effort only ($nbfail file(s) did not compile)"
else
if [ -n "$gated" ]; then
echo "#### ❌ clang-tidy gate — $(printf '%s\n' "$gated" | wc -l) fatal"
echo '```'; head -100 <<< "$gated"; echo '```'
echo "_Fix the finding, or if it is a deliberate, waive it inline with \`// NOLINT(<check-name>)\` on that line._"
fi
if [ -n "$advis" ]; then
echo "#### ❗ clang-tidy warnings — $(printf '%s\n' "$advis" | wc -l) on changed lines"
echo '```'; head -100 <<< "$advis"; echo '```'
fi
if [ -n "$deadc" ]; then
echo "#### ❗ clang-tidy unreachable code — $(printf '%s\n' "$deadc" | wc -l) on changed lines"
echo "_Not posted inline: whether code is reachable can depend on the build configuration._"
echo '```'; head -100 <<< "$deadc"; echo '```'
fi
if [ -n "$tfail" ]; then
echo "#### ⚠️ clang-tidy: coverage incomplete"
echo '```'; head -100 <<< "$tfail"; echo '```'
fi
fi
if [ "$besteffort" -gt 0 ]; then
# The count goes here only when the heading above does not already carry it.
note=; [ "$nbfail" -gt 0 ] && [ -n "$gated$advis$deadc$tfail" ] && note=" ($nbfail did not compile)"
echo; echo "ℹ️ $besteffort changed file(s) are only covered by a best-effort clang-tidy run$note."
fi
} | tee ci-out/tidy-summary.md >> "$GITHUB_STEP_SUMMARY"
# Inline-review candidates (Commit 5): parse tidy.log (already changed-lines
# scoped) into per-finding comments for the stage-2 'inline' job. Written
# even when the gate fires below — findings post regardless of pass/fail.
# Never reds the step (|| true): a parse hiccup must not fail the build.
cat "$bfail_list" >> "$tfail_list"
python3 .github/scripts/tidy_to_inline.py tidy.log ci-out/inline-tidy.json "$tfail_list" || true
# Gate AFTER the summary + artifact are written, on promoted checks only.
if [ -n "$gated" ]; then
echo "clang-tidy gate: $(printf '%s\n' "$gated" | wc -l) promoted-check violation(s) on changed lines" >&2
exit 1
fi
- name: gcc diff-scoped warning gate for ${{ matrix.platform.name }}
# PROPOSAL (branch ci/gcc-diff-gate). Gate not-yet-promoted gcc classes on the
# PR's changed LINES only, without promoting them tree-wide (the tree still carries a
# backlog: -Wvla 18, -Wreturn-type 11, dead-code family). The script recompiles each changed
# .c/.cpp from compile_commands.json with the candidate warnings enabled (non-fatal) and keeps
# only findings on lines the PR touched that the file's base version did not already have
# -> a PR can only fail on a finding it introduced on a changed line.
# The silent baseline (and the build-summary that relies on it) is unaffected. bpi leg only
# (it owns the compile DB). Runs AFTER clang-tidy so both gate independently.
if: always() && matrix.platform.clang_tidy && github.event_name == 'pull_request'
working-directory: easymesh_project/OneWifi
env:
BASE: ${{ github.event.pull_request.base.sha }}
# ROLLOUT: start in advisory mode. ENFORCE=false renders the same ❌ error
# block but never reds the job, so the mechanism runs on real PRs first.
# FLIP to 'true' (one-word change) once ALL of these hold on the live runner:
# 1. >= a handful of real PRs touching .c/.cpp have processed end-to-end
# with NO "mechanism error" summary line (the try/except fail-open);
# 2. >= 1 advisory finding (the -Wunused-* classes fire on ordinary
# refactors) hand-checked against the diff -> line attribution correct;
# 3. zero false positives on the GATE classes;
# 4. the hidden backlog stays out of the way: makefile.common's -Wno-<class>
# silences several GATE classes (bpi DB, 2026-10: 431 implicit-function-
# declaration, 14 incompatible-pointer-types, 9 return-type, 1 int-conversion).
# The script drops a finding the file had before the PR, so a PR that edits
# such a line for an unrelated reason does not gate; confirm on real PRs.
# Gate on that evidence, not the calendar. Silence of the GATE classes alone
# proves nothing (they're rarely introduced) -- the advisory classes are what
# exercise the recompile + line-scoping machinery.
ENFORCE: 'false'
# Optimization level for the recompile ONLY (not the build product): production
# bpi/rpi are -Os but the compile DB is -O0, so middle-end warnings are dormant.
# -Os wakes them on the changed files at production's level. The script strips the
# DB's own -Werror=<class> promotions (incl. -Werror=array-bounds/-maybe-uninitialized
# that -Os trips) so no class can abort the analysis; a real error still exits nonzero.
OPT: '-Os'
# GATE = fail the job (when ENFORCE=true) if introduced on a changed line.
# ADVISORY = report only. Both are posted inline (❌ / ❗) and in the ci-summary.
# GATE picks: front-end, ~0-FP, crash/RCE/UB classes.
# -Wvla (stack overflow), -Wreturn-type (UB). -Wint-conversion,
# -Wimplicit-function-declaration, -Wimplicit-int, -Wincompatible-pointer-types:
# wrong call ABI / int-pointer confusion, and errors by default from gcc-14.
# makefile.common silences three of them (a -Wno-<class> also turns the gcc-14
# error off), so the tree keeps building but hides a backlog (see 4. above);
# the gate keeps new code from adding to it. -Wsizeof-pointer-memaccess
# (truncated mem*/str* copy), -Wformat-security (uncontrolled format -> leak /
# %n write; armed by the DB's -Wall). The four C-only classes are auto-dropped on
# C++ TUs by the script. gcc-14's other two default errors have no gcc-13 flag:
# -Wreturn-mismatch is covered by -Wreturn-type here (it is what actually breaks
# this build on gcc-14: 5 sites), -Wdeclaration-missing-parameter-type is new.
GATE_WARNINGS: "-Wvla -Wreturn-type -Wint-conversion -Wimplicit-function-declaration -Wimplicit-int -Wincompatible-pointer-types -Wsizeof-pointer-memaccess -Wformat-security"
# ADVISORY: measured quiet over the compile DB at -Os (each 0-4 findings tree-wide,
# so they only fire on something genuinely new). -Wpointer-sign (33) and
# -Wdiscarded-qualifiers (14) were DROPPED as too noisy. Middle-end classes need
# the -Os above to fire.
ADVISORY_WARNINGS: "-Wunused-function -Wunused-but-set-variable -Wunused-variable -Wunused-value -Wunused-label -Waddress -Wstringop-overflow -Wstringop-overread -Warray-bounds -Wdangling-pointer -Wuse-after-free"
# Inline-review candidates (Commit 5) for the stage-2 'inline' job. Lands in
# ci-out/ (uploaded as ci-summary-bpi). Absent/unset -> the script writes none.
INLINE_JSON: "ci-out/inline-gcc.json"
run: |
set -uo pipefail
mkdir -p ci-out
if [ ! -f compile_commands.json ] || [ -z "$BASE" ]; then
echo "#### 🚦 gcc diff-gate: no compile DB or PR base — skipped" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git fetch --no-tags --depth=1 origin "$BASE" >/dev/null 2>&1 || true
# The script prints a markdown summary to stdout and ::error::/::warning:: to stderr;
# it exits 1 iff a GATE class fired on a changed line.
# A missing script (partial cherry-pick / not yet on this branch) must skip,
# not red the job: python3 on an absent file exits 2 and would fail the matrix.
if [ ! -f .github/scripts/gcc_diff_gate.py ]; then
echo "#### 🚦 gcc diff-gate: script not present — skipped" | tee -a ci-out/gcc-gate-summary.md >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
set +e
python3 .github/scripts/gcc_diff_gate.py > gcc-gate.md
rc=$?
set -e
tee -a ci-out/gcc-gate-summary.md < gcc-gate.md >> "$GITHUB_STEP_SUMMARY"
exit "$rc"
- name: Map inline findings to PR-head lines for ${{ matrix.platform.name }}
# clang-tidy and the gcc gate ran on the merge ref (HEAD = refs/pull/N/merge), so
# their line numbers are merge-result lines. Review comments anchor to the PR head,
# where a line moves when the base branch changed the same file above it.
if: always() && matrix.platform.clang_tidy && github.event_name == 'pull_request'
working-directory: easymesh_project/OneWifi
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# A crash must not leave merge-numbered candidates behind: drop both files, which
# the poster reads as "producer skipped" (posts and deletes nothing).
if ! python3 .github/scripts/map_head_lines.py "$HEAD_SHA" ci-out/inline-gcc.json ci-out/inline-tidy.json; then
echo "::warning::Could not map inline findings to PR-head lines; none are posted this run."
rm -f ci-out/inline-gcc.json ci-out/inline-tidy.json
fi
# ---- Hand-off to the trusted PR-comment stage (pr-comments.yml) ----------
# Record the PR identity as passive data for the workflow_run poster to
# validate. Only meaningful on PRs; push builds skip it.
- name: Record PR metadata for the comment stage
if: always() && github.event_name == 'pull_request'
working-directory: easymesh_project/OneWifi
run: |
mkdir -p ci-out
{
echo "pr_number=${{ github.event.pull_request.number }}"
echo "head_sha=${{ github.event.pull_request.head.sha }}"
} > ci-out/pr-meta.env
# Publish the summary artifact the PR-comment stage posts. Uploaded even on build failure
# (if: always()) so a red build still gets its summary posted. tidy-summary.md for now
# exists only on the bananapi build leg.
#
# Scoped to the two device legs (bpi + rpi), NOT mock. Both devices run the Yocto-style
# clean baseline (-Wall -Wextra + explicit -Wno-), so their build-summaries are silent
# *UNLESS* a genuinely new warning *class* appears.
# NOTE: the build-summary greps the whole list of OneWifi warnings — it is NOT PR-diff-scoped
# (unlike the clang-tidy step). It relies on that silent baseline, so any un-suppressed
# residual warning would show on every PR. mock is the unittest leg (no clean-baseline gate)
# -> excluded to avoid exactly that noise.
# The pr-comments.yml 'summary' job shows every ci-summary-* that exists, names an
# expected leg that uploaded none, and anchors trust on ci-summary-bpi (so bpi must
# always upload), so:
# TO ADD mock: drop the `&& matrix.platform.id != 'mock'` clause below, and add mock
# to the expected legs ("bpi rpi") in pr-comments.yml's "Compose CI
# summary comment" step, which names an expected leg that uploaded nothing.
# TO REVERT to bpi-only: change it back to `== 'bpi'`, and drop rpi from that list.
- name: Upload CI summary artifact
if: always() && github.event_name == 'pull_request' && matrix.platform.id != 'mock'
uses: actions/upload-artifact@v6
with:
name: ci-summary-${{ matrix.platform.id }}
path: easymesh_project/OneWifi/ci-out
if-no-files-found: warn
retention-days: 1