A new quality job runs the linter battery (clippy, cppcheck, shellcheck, ruff, lizard, jscpd, cargo-audit per crate, scc for the LOC denominator) and feeds tools/quality/score.py: findings convert to SQALE remediation minutes, debt ratio grades A-F on SonarQube's published grid, and a separate worst-of security axis can only drag the overall grade down. The job uploads the full quality.json breakdown and fails when the security grade is worse than C. The badges job now renders all four SVGs in-runner with anybadge (shields hex palette); the previous img.shields.io curls were the one external-service dependency left in the pipeline. quality.svg is seeded at the current locally-computed grade (A, debt ratio 0.42%). Also fixes the two ruff findings the battery surfaced in flowgen.py.
366 lines
16 KiB
YAML
366 lines
16 KiB
YAML
# bfe-core1106-sdk CI.
|
|
#
|
|
# Policy (mirrors flare-edge): only GitHub-owned actions get the repo token; the one
|
|
# third-party helper (taiki-e/install-action) is pinned and never handed a token.
|
|
# Every job runs on GitHub-hosted runners: no self-hosted runner may be reachable
|
|
# from this repo's workflows (ADR-0007: public-repo fork PRs would otherwise be
|
|
# able to run code on private infrastructure). kernel-build is dispatch-only.
|
|
name: ci
|
|
|
|
on:
|
|
push:
|
|
paths-ignore: ['.github/badges/**']
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Least privilege by default: every job gets a read-only token; only `badges`
|
|
# (which commits rendered SVGs) overrides this with contents: write below.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
outputs:
|
|
passed: ${{ steps.result.outputs.passed }}
|
|
coverage: ${{ steps.result.outputs.coverage }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: cargo test (all crates)
|
|
run: |
|
|
set -o pipefail
|
|
: > /tmp/test.log
|
|
for d in sim tools/config-lint qemu/rs485-bridge qemu/tests/clockprobe; do
|
|
echo "== cargo test in $d ==" | tee -a /tmp/test.log
|
|
( cd "$d" && cargo test --locked ) 2>&1 | tee -a /tmp/test.log
|
|
done
|
|
- name: coverage (cargo-llvm-cov on sim)
|
|
run: rustup component add llvm-tools-preview
|
|
- uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7
|
|
with:
|
|
tool: cargo-llvm-cov
|
|
- name: run coverage
|
|
working-directory: sim
|
|
run: cargo llvm-cov --locked --json --summary-only --output-path /tmp/cov.json
|
|
- name: parse results
|
|
id: result
|
|
run: |
|
|
passed=$(grep -oE '[0-9]+ passed' /tmp/test.log | awk '{s+=$1} END{print s+0}')
|
|
pct=$(python3 -c 'import json;print("%.0f"%json.load(open("/tmp/cov.json"))["data"][0]["totals"]["lines"]["percent"])')
|
|
echo "passed=$passed" >> "$GITHUB_OUTPUT"
|
|
echo "coverage=$pct" >> "$GITHUB_OUTPUT"
|
|
echo "tests passed: $passed | sim line coverage: ${pct}%"
|
|
|
|
mcdc:
|
|
# 100% MC/DC (condition coverage) enforced on every Tier-1 driver harness.
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: install gcc-14
|
|
run: sudo apt-get update -qq && sudo apt-get install -y -qq gcc-14
|
|
- name: enforce 100% MC/DC on drivers/*/test
|
|
run: |
|
|
fail=0; ran=0
|
|
for t in drivers/*/test; do
|
|
[ -f "$t/Makefile" ] || continue
|
|
ran=1
|
|
echo "== MC/DC: $t =="
|
|
make -C "$t" check CC=gcc-14 GCOV=gcov-14 || fail=1
|
|
done
|
|
[ "$ran" = 1 ] || { echo "no driver MC/DC harnesses found"; exit 1; }
|
|
exit $fail
|
|
|
|
bench:
|
|
# Smoke-run the sim micro-benchmarks and emit the ns/op trend JSON. Regression
|
|
# gating against stored history is future work (no flare-edge pattern to copy).
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: run sim benchmarks
|
|
working-directory: sim
|
|
run: |
|
|
cargo bench --locked --bench sim_bench 1> bench.txt 2> bench.json
|
|
echo "== timings =="; cat bench.txt
|
|
echo "== trend json =="; grep '"bench"' bench.json
|
|
- name: run rs485-bridge benchmarks
|
|
working-directory: qemu/rs485-bridge
|
|
run: |
|
|
cargo bench --locked --bench bridge_bench 1> bench.txt 2> bench.json
|
|
echo "== timings =="; cat bench.txt
|
|
echo "== trend json =="; grep '"bench"' bench.json
|
|
|
|
qemu-tools:
|
|
# The qemu/ device-sim build tooling must stay healthy on a plain hosted
|
|
# runner: shellcheck the scripts, build the initramfs (pinned busybox,
|
|
# fail-closed sha), and build the A/B disk image (unprivileged mkfs -d).
|
|
# Booting needs a zImage and therefore lives in kernel-build's smoke step.
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: shellcheck qemu scripts
|
|
run: |
|
|
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
|
|
shellcheck -x qemu/*.sh qemu/tests/*.sh build/*.sh \
|
|
qemu/rootfs/etc/warden-lib.sh qemu/rootfs/etc/rc \
|
|
qemu/rootfs/sbin/init qemu/rootfs/init
|
|
- name: cache pinned busybox
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: qemu/out/busybox-armv7l
|
|
key: busybox-armv7l-${{ hashFiles('qemu/busybox.sha256') }}
|
|
- name: build initramfs
|
|
run: bash qemu/mkinitramfs.sh
|
|
- name: build A/B disk image
|
|
run: bash qemu/mkimage.sh
|
|
|
|
patches-apply:
|
|
# The RV1106 series must apply cleanly onto pristine linux-6.18.46.
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: cache pristine kernel tarball
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/linux-6.18.46.tar.xz
|
|
key: linux-6.18.46-tarball
|
|
- name: fetch + verify pristine (shared fail-closed fetcher)
|
|
run: bash build/fetch-kernel-tarball.sh "$HOME/linux-6.18.46.tar.xz"
|
|
- name: apply the series in order
|
|
run: |
|
|
tar -C /tmp -xf ~/linux-6.18.46.tar.xz
|
|
for p in patches/*.patch; do
|
|
git -C /tmp/linux-6.18.46 apply --whitespace=nowarn "$GITHUB_WORKSPACE/$p" \
|
|
&& echo "applied $p" || { echo "FAILED to apply $p"; exit 1; }
|
|
done
|
|
echo "full series applied cleanly onto pristine 6.18.46"
|
|
|
|
# Automatic artifact GC (mirrors flare-edge). An exceeded account-wide Actions
|
|
# storage quota blocks ALL new runs (startup_failure), not just uploads, so
|
|
# before a new kernel artifact is uploaded, drop older kernel-rv1106 artifacts
|
|
# beyond the newest few: storage stays bounded across dispatches. gh + the
|
|
# built-in token (no third-party action); needs actions:write to delete.
|
|
# Best-effort: it never fails the run, so it can't block the build that needs it.
|
|
prune-artifacts:
|
|
if: github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
actions: write
|
|
steps:
|
|
- name: Keep only the newest few kernel artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
KEEP: "3"
|
|
run: |
|
|
ids=$(gh api --paginate "repos/$REPO/actions/artifacts?per_page=100" \
|
|
--jq "[.artifacts[] | select(.name==\"kernel-rv1106\")]
|
|
| sort_by(.created_at) | reverse | .[$KEEP:] | .[].id" \
|
|
2>/dev/null) || { echo "list failed; skipping"; exit 0; }
|
|
n=0
|
|
for id in $ids; do
|
|
if gh api -X DELETE "repos/$REPO/actions/artifacts/$id" 2>/dev/null; then
|
|
n=$((n + 1)); echo "pruned artifact $id"
|
|
fi
|
|
done
|
|
echo "pruned $n old kernel artifact(s); kept newest $KEEP"
|
|
exit 0
|
|
|
|
kernel-build:
|
|
# Full hermetic build on a GitHub-hosted runner (ADR-0007; supersedes the
|
|
# self-hosted half of ADR-0004: a self-hosted runner must never be reachable
|
|
# from a public repo's workflows). Manual-dispatch by design: a full kernel
|
|
# build is heavy; trigger via `gh workflow run ci.yml` / the Actions UI.
|
|
if: github.event_name == 'workflow_dispatch'
|
|
needs: [prune-artifacts]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: install cross toolchain + kernel build deps + qemu
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq gcc-arm-linux-gnueabihf qemu-system-arm \
|
|
cpio bc bison flex libssl-dev ccache
|
|
- name: provision `python` (SDK quirk, the build calls bare python)
|
|
run: |
|
|
mkdir -p "$RUNNER_TEMP/bin"
|
|
ln -sf "$(command -v python3)" "$RUNNER_TEMP/bin/python"
|
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
|
- name: cache pristine kernel tarball
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/linux-6.18.46.tar.xz
|
|
key: linux-6.18.46-tarball
|
|
# Ephemeral runners rebuild the whole tree every dispatch (~9 min of
|
|
# compile); ccache recovers most of it for an unchanged/lightly-changed
|
|
# series. Keyed on the config + patches so a real change misses cleanly.
|
|
- name: cache ccache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.ccache
|
|
key: kbuild-ccache-${{ hashFiles('build/warden_defconfig', 'patches/*.patch') }}
|
|
restore-keys: kbuild-ccache-
|
|
- name: build zImage + rv1106-warden.dtb
|
|
env:
|
|
# WORK must be OUTSIDE the repo checkout: build-kernel.sh applies the patch
|
|
# series with `git apply`, which silently ignores out-of-subdir paths when
|
|
# run inside another git repo (issue #1). $RUNNER_TEMP is outside the checkout.
|
|
WORK: ${{ runner.temp }}/kbuild-out
|
|
# The kernel is freestanding; the generic arm cross toolchain links it.
|
|
CROSS_COMPILE: arm-linux-gnueabihf-
|
|
WARDEN_CCACHE: 1
|
|
CCACHE_DIR: /home/runner/.ccache
|
|
# Matched module set for the out-of-tree wifi/BT drivers (issue #4:
|
|
# a stale 5.10 .ko was all the panel had). Full `make modules` runs
|
|
# (zImage alone emits no Module.symvers); only these dirs' .ko files
|
|
# are collected/uploaded.
|
|
WARDEN_MODULES_COLLECT: drivers/net/wireless/aic8800
|
|
# KERNEL_TARBALL is exported from the SHELL so $HOME expands. A literal
|
|
# `~` in a YAML env: value is never tilde-expanded and broke every
|
|
# dispatch until caught in review.
|
|
run: |
|
|
export KERNEL_TARBALL="$HOME/linux-6.18.46.tar.xz"
|
|
bash build/build-kernel.sh
|
|
ccache -s | head -4
|
|
# Boot smoke under QEMU: the zImage this job just built must reach the
|
|
# initramfs sentinel on -M virt (verified 2026-08-29: the canonical
|
|
# config boots virt as-is). FAIL-CLOSED on a missing qemu-system-arm.
|
|
- name: boot smoke (qemu-system-arm -M virt)
|
|
run: |
|
|
bash qemu/mkinitramfs.sh
|
|
bash qemu/tests/boot-smoke.sh \
|
|
"$RUNNER_TEMP/kbuild-out/linux-6.18.46/arch/arm/boot/zImage"
|
|
# Best-effort: the build IS the gate. Uploading the zImage/dtb to GitHub
|
|
# artifact storage can fail on an account-wide storage-quota hit (recalculated
|
|
# every 6-12h) that has nothing to do with this build. Don't red-X a good
|
|
# kernel build over it.
|
|
- uses: actions/upload-artifact@v4
|
|
continue-on-error: true
|
|
with:
|
|
name: kernel-rv1106
|
|
path: |
|
|
${{ runner.temp }}/kbuild-out/linux-6.18.46/arch/arm/boot/zImage
|
|
${{ runner.temp }}/kbuild-out/linux-6.18.46/arch/arm/boot/dts/rockchip/rv1106-warden.dtb
|
|
${{ runner.temp }}/kbuild-out/modules-out/*.ko
|
|
# Short so kernel images self-expire instead of piling into the
|
|
# account-wide storage quota; the prune-artifacts job above is the
|
|
# active bound, this is the backstop.
|
|
retention-days: 5
|
|
|
|
quality:
|
|
# Self-hosted Codacy-style grade: a linter battery feeds
|
|
# tools/quality/score.py (SQALE debt ratio + a separate security axis;
|
|
# thresholds documented in the script). No external assessment service;
|
|
# the badge is rendered and committed by the badges job.
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
outputs:
|
|
grade: ${{ steps.score.outputs.grade }}
|
|
color: ${{ steps.score.outputs.color }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: install analyzers
|
|
run: |
|
|
sudo apt-get update -qq && sudo apt-get install -y -qq cppcheck shellcheck
|
|
pip install --quiet lizard ruff
|
|
sudo npm install --silent -g jscpd
|
|
curl -fsSL -o /tmp/scc.tar.gz \
|
|
https://github.com/boyter/scc/releases/download/v3.6.0/scc_Linux_x86_64.tar.gz
|
|
sudo tar -C /usr/local/bin -xzf /tmp/scc.tar.gz scc
|
|
rustup component add clippy
|
|
- uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7
|
|
with:
|
|
tool: cargo-audit
|
|
- name: collect linter outputs
|
|
run: |
|
|
Q="$RUNNER_TEMP/qual"; mkdir -p "$Q"
|
|
scc --format json \
|
|
--exclude-dir .git,target,patches,kernel,docs/workflows,.github/badges \
|
|
. > "$Q/scc.json"
|
|
: > "$Q/clippy.jsonl"
|
|
for d in sim tools/config-lint qemu/rs485-bridge qemu/tests/clockprobe; do
|
|
( cd "$d" && cargo clippy --locked --all-targets --message-format=json \
|
|
2>/dev/null >> "$Q/clippy.jsonl" )
|
|
( cd "$d" && cargo audit --json -q > "$Q/audit-$(basename "$d").json" )
|
|
done
|
|
shellcheck -f json1 qemu/*.sh qemu/tests/*.sh build/*.sh \
|
|
qemu/rootfs/init qemu/rootfs/etc/rc qemu/rootfs/sbin/init \
|
|
> "$Q/shellcheck.json" || true
|
|
cppcheck --enable=warning,style,performance,portability --inline-suppr \
|
|
--xml drivers/ 2> "$Q/cppcheck.xml"
|
|
lizard -C 10 --csv sim/src qemu/rs485-bridge/src tools/config-lint/src \
|
|
drivers/ tools/flowgen.py qemu/tests/clockprobe/src > "$Q/lizard.csv"
|
|
ruff check --output-format=json tools/ qemu/ > "$Q/ruff.json" || true
|
|
jscpd --silent --reporters json --output "$Q" \
|
|
--pattern '**/*.{rs,c,h,sh,py}' \
|
|
--ignore '**/target/**,**/patches/**,**/kernel/**' .
|
|
- name: score
|
|
id: score
|
|
run: |
|
|
python3 tools/quality/score.py "$RUNNER_TEMP/qual" \
|
|
--out "$RUNNER_TEMP/qual/quality.json"
|
|
J="$RUNNER_TEMP/qual/quality.json"
|
|
echo "grade=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['grade'])" "$J")" >> "$GITHUB_OUTPUT"
|
|
echo "color=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['badge_color'])" "$J")" >> "$GITHUB_OUTPUT"
|
|
- name: security gate
|
|
run: python3 tools/quality/score.py "$RUNNER_TEMP/qual" --gate-security C
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: quality-report
|
|
path: ${{ runner.temp }}/qual/quality.json
|
|
retention-days: 30
|
|
|
|
badges:
|
|
needs: [test, quality]
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: install tools
|
|
run: |
|
|
sudo apt-get update -qq && sudo apt-get install -y -qq cloc
|
|
pip install --quiet anybadge
|
|
- name: render badges
|
|
# Rendered locally with anybadge: the committed SVG must not depend
|
|
# on any external service, at view time or at render time.
|
|
env:
|
|
PASSED: ${{ needs.test.outputs.passed }}
|
|
COVERAGE: ${{ needs.test.outputs.coverage }}
|
|
GRADE: ${{ needs.quality.outputs.grade }}
|
|
QCOLOR: ${{ needs.quality.outputs.color }}
|
|
run: |
|
|
mkdir -p .github/badges
|
|
loc=$(cloc --quiet --json --exclude-dir=target,build,build-target,patches,data,docs . \
|
|
| python3 -c 'import sys,json; print(json.load(sys.stdin)["SUM"]["code"])')
|
|
col='#fe7d37'; [ "${COVERAGE:-0}" -ge 60 ] && col='#dfb317'; [ "${COVERAGE:-0}" -ge 80 ] && col='#4c1'
|
|
anybadge --overwrite --label="lines of code" --value="$loc" --color='#007ec6' \
|
|
--file=.github/badges/loc.svg
|
|
anybadge --overwrite --label=tests --value="${PASSED} passing" --color='#4c1' \
|
|
--file=.github/badges/tests.svg
|
|
anybadge --overwrite --label=coverage --value="${COVERAGE}%" --color="$col" \
|
|
--file=.github/badges/coverage.svg
|
|
anybadge --overwrite --label="code quality" --value="$GRADE" --color="$QCOLOR" \
|
|
--file=.github/badges/quality.svg
|
|
- name: commit badges
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git add .github/badges/*.svg
|
|
if ! git diff --cached --quiet; then
|
|
git commit -m "ci: update badges [skip ci]"
|
|
git push
|
|
fi
|