diff --git a/.github/badges/quality.svg b/.github/badges/quality.svg new file mode 100644 index 0000000..731eec5 --- /dev/null +++ b/.github/badges/quality.svg @@ -0,0 +1,23 @@ + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da124a8..e607325 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,8 +257,71 @@ jobs: # 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] + needs: [test, quality] if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 15 @@ -266,26 +329,37 @@ jobs: contents: write steps: - uses: actions/checkout@v4 - - name: install cloc - run: sudo apt-get update -qq && sudo apt-get install -y -qq cloc + - 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=orange; [ "${COVERAGE:-0}" -ge 60 ] && col=yellow; [ "${COVERAGE:-0}" -ge 80 ] && col=brightgreen - curl -fsSL "https://img.shields.io/badge/lines%20of%20code-${loc}-blue" -o .github/badges/loc.svg - curl -fsSL "https://img.shields.io/badge/tests-${PASSED}%20passing-brightgreen" -o .github/badges/tests.svg - curl -fsSL "https://img.shields.io/badge/coverage-${COVERAGE}%25-${col}" -o .github/badges/coverage.svg + 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/loc.svg .github/badges/tests.svg .github/badges/coverage.svg + git add .github/badges/*.svg if ! git diff --cached --quiet; then - git commit -m "ci: update loc/tests/coverage badges [skip ci]" + git commit -m "ci: update badges [skip ci]" git push fi diff --git a/README.md b/README.md index 31ae408..d7a91bf 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@    + A modern, open development environment for the **Luckfox Pico 86 Panel** (Rockchip RV1106), replacing the vendor SDK, and honest about what runs on diff --git a/docs/ci-cd.md b/docs/ci-cd.md index ebb278d..4208c91 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -14,6 +14,7 @@ execute code on private infrastructure (ADR-0007). | `bench` | ubuntu-latest | Smoke-runs the sim + rs485-bridge micro-benchmarks; emits ns/op trend JSON. | | `patches-apply` | ubuntu-latest | Fetches pristine linux-6.18.46 (cached, sha256-verified) and applies `patches/*` in order. | | `qemu-tools` | ubuntu-latest | shellcheck on `qemu/**.sh`; builds the initramfs (pinned busybox) and the A/B disk image. | +| `quality` | ubuntu-latest | Codacy-style grade computed in-pipeline: clippy, cppcheck, shellcheck, ruff, lizard, jscpd, cargo-audit feed `tools/quality/score.py` (SQALE debt ratio + a separate worst-of security axis; SonarQube's published thresholds). Uploads `quality.json`; fails if the security grade is worse than C. | | `kernel-build` | ubuntu-latest, **dispatch-only** | apt-installs the cross toolchain + qemu, `build/build-kernel.sh` -> `zImage` + `rv1106-warden.dtb`, QEMU `-M virt` boot smoke (fail-closed), artifact upload (best-effort). Trigger: `gh workflow run ci.yml`. | | `prune-artifacts` | ubuntu-latest, dispatch-only | Deletes `kernel-rv1106` artifacts beyond the newest 3. | | `badges` | ubuntu-latest | Renders loc/tests/coverage shields on push to `main` (`[skip ci]` + `paths-ignore` loop guard). | @@ -28,5 +29,8 @@ deployment log, not here. ## Badges -Static shields SVGs are committed by the `badges` job. The GitHub-native -`ci.yml` status badge works live regardless. +SVGs are rendered in-runner with anybadge and committed by the `badges` +job: no external badge or assessment service at render or view time. The +quality letter comes from the `quality` job; `tools/quality/score.py` +documents the scoring model and thresholds. The GitHub-native `ci.yml` +status badge works live regardless. diff --git a/tools/flowgen.py b/tools/flowgen.py old mode 100644 new mode 100755 index 71d4d98..f5eeeef --- a/tools/flowgen.py +++ b/tools/flowgen.py @@ -150,8 +150,8 @@ def main(): os.makedirs(OUT, exist_ok=True) index = ["# Workflow Flowcharts", "", "Generated by `tools/flowgen.py` from the modelled decision paths.", - "Each is an outcome-first flowchart of a workflow the SDK tests, with its" - " benchmark or MC/DC metric.", ""] + ("Each is an outcome-first flowchart of a workflow the SDK tests," + " with its benchmark or MC/DC metric."), ""] for w in WORKFLOWS: body = (f"# {w['title']}\n\n" f"> **Outcome tested:** {w['outcome']}\n\n" diff --git a/tools/quality/score.py b/tools/quality/score.py new file mode 100755 index 0000000..a696f7d --- /dev/null +++ b/tools/quality/score.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Self-hosted code-quality grade: SQALE debt ratio plus a security axis. + +Reads the linter outputs collected by the CI quality job from one directory +and prints a letter grade. No external service is involved at any point; +every threshold below traces to a published number (SonarQube's default +30 min/line development cost and its maintainability grid, which +Code Climate/Qlty publish almost verbatim). + + score.py