diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 70669fb..7b1246a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,6 +1,8 @@ # CI and releases (docs/milestones/R1.md). -# Any push: host tests, then the release firmware and the Debug Build. -# A tag v*: the same, then a Gitea release with the signed Update File. +# A push: the host tests, with their coverage of lib/. On main, the README's badges +# are published to the branch `badges`. +# A pull request: the same, then the release firmware and the Debug Build. +# A tag v*: all of it, then a Gitea release with the signed Update File. # Run by hand: the release of a tag that exists already (the ones from before CI). # # The job runs in a plain Python image, as scripts/ci.sh does on a developer's machine, with the @@ -9,8 +11,9 @@ name: CI on: push: - branches: ['**'] + branches: ['**', '!badges'] # badges holds what CI itself publishes tags: ['v*'] + pull_request: workflow_dispatch: inputs: tag: @@ -20,6 +23,8 @@ on: jobs: build: runs-on: ubuntu + # A pull request from a fork would run someone else's code on our runner: not without us (Q154). + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository container: image: python:3.12-slim volumes: @@ -32,7 +37,7 @@ jobs: run: | apt-get update -qq apt-get install -y -qq --no-install-recommends git build-essential openssl >/dev/null - pip install -q --no-cache-dir --root-user-action=ignore platformio + pip install -q --no-cache-dir --root-user-action=ignore platformio gcovr pio --version; df -h /pio | tail -1; ls /pio | head - name: Check out @@ -41,13 +46,32 @@ jobs: git config --global --add safe.directory '*' git init -q . git remote add origin "${{ github.server_url }}/${{ github.repository }}.git" - git fetch -q --tags origin '+refs/heads/*:refs/remotes/origin/*' + git fetch -q --tags origin '+refs/heads/*:refs/remotes/origin/*' '+refs/pull/*/head:refs/remotes/pull/*' git checkout -q --detach "${{ github.sha }}" git describe --tags --always - - name: Host tests and both builds - if: github.event_name == 'push' - run: scripts/ci.sh + - name: Host tests, and their coverage of lib/ + if: github.event_name != 'workflow_dispatch' + run: scripts/coverage.sh + + - name: The release firmware and the Debug Build + if: github.event_name == 'pull_request' || github.ref_type == 'tag' + run: scripts/ci.sh builds + + # The README's badges are files on a branch of their own, replaced at each push to main. + - name: Publish the badges + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + rm -rf /tmp/badges && mkdir /tmp/badges + cp .pio/coverage/coverage.svg .pio/coverage/summary.json /tmp/badges/ + scripts/coverage_badge.py --plain release "$(git describe --tags --abbrev=0)" /tmp/badges/release.svg + cd /tmp/badges + git init -q -b badges . + git add . + git -c user.name="roro9stack CI" -c user.email="ci@git.twis.la" commit -q -m "Coverage of ${{ github.ref_name }} at ${{ github.sha }}" + git push -q --force "$(echo "${{ github.server_url }}" | sed "s#://#://ci:${GITEA_TOKEN}@#")/${{ github.repository }}.git" badges - name: Which release id: release diff --git a/README.md b/README.md index 0270dce..3f28204 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # roro9stack +[![CI](https://git.twis.la/twisla/roro9stack/actions/workflows/ci.yml/badge.svg?branch=main)](https://git.twis.la/twisla/roro9stack/actions?workflow=ci.yml) [![Coverage of lib/ by the host tests](https://git.twis.la/twisla/roro9stack/raw/branch/badges/coverage.svg)](#build-and-test-local-ci) [![Latest release](https://git.twis.la/twisla/roro9stack/raw/branch/badges/release.svg)](https://git.twis.la/twisla/roro9stack/releases/latest) + A multi-app firmware for the **M5Stack Cardputer ADV** with the **Cap LoRa-1262**. It's a Meshtastic-compatible mesh messenger, plus Wi-Fi tools, IRC, GNSS and more. Licensed GPL-3.0. - Domain language: [CONTEXT.md](CONTEXT.md) @@ -18,11 +20,13 @@ scripts/ci.sh This runs the host-side unit tests (`test/`, `native` environment), then builds the firmware. The output is `.pio/build/cardputer-adv/firmware.factory.bin`. +`scripts/coverage.sh` runs the same tests with coverage counters and writes a line-by-line report to `.pio/coverage/index.html`. The badge above is its figure for `main`: the share of the lines of `lib/` that the host tests run. `lib/` is the logic that compiles on a PC; `lib/SD` (the card's driver) and `src/` (the Apps, the Services, everything that needs the device) have no host tests and aren't in that figure. + The framework is rebuilt with the TLS settings in `platformio.ini` (`custom_sdkconfig`, ADR 0006), so the first build after a fresh checkout takes about 4 minutes; later builds take under a minute. ## CI and releases -Gitea Actions runs the same thing on every push (`.gitea/workflows/ci.yml`, docs/milestones/R1.md). Pushing a tag `v*` also publishes a release on Gitea with: +Gitea Actions (`.gitea/workflows/ci.yml`, docs/milestones/R1.md) runs the host tests on every push, and on a pull request also builds the release firmware and the Debug Build: changes reach `main` through pull requests. Pushing a tag `v*` runs all of it and publishes a release on Gitea with: - `roro9stack-.ota`, the signed Update File; - `roro9stack--factory.bin`, the whole flash image for a first install over USB; diff --git a/docker/Dockerfile b/docker/Dockerfile index b67e2b4..7c3220b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends git build-essential \ && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir platformio +RUN pip install --no-cache-dir platformio gcovr # Toolchains and libraries are cached in a named volume mounted here. RUN mkdir -p /pio && chmod 777 /pio diff --git a/docs/milestones/R1.md b/docs/milestones/R1.md index 9447cf7..fe9bfa8 100644 --- a/docs/milestones/R1.md +++ b/docs/milestones/R1.md @@ -12,7 +12,7 @@ Until now the tests, the builds, the signing and the flashing all happened on on | # | Decision | |---|---| -| Q151 | Every push, to any branch: the host tests and both builds. A tag `v*`: the same, then a release. | +| Q151 | A push, to any branch: the host tests (with their coverage). A pull request: the same and both builds; changes reach `main` through pull requests. A tag `v*`: all of it, then a release. (First: everything on every push, which rebuilt the firmware far more often than anyone looked at it.) | | Q152 | **CI signs.** The signing key is the repository secret `OTA_SIGNING_KEY`; a tag push makes a complete, signed release with no manual step (ADR 0008). | | Q153 | The Debug Build is built in CI with a token of the runner's own, to prove it compiles, and **isn't published**: it would hand everyone its Debug Console token. | | Q154 | Pull requests from forks don't start a run. | @@ -22,6 +22,7 @@ Until now the tests, the builds, the signing and the flashing all happened on on | Q158 | Reproducible builds aren't needed for signing any more (Q152); not pursued here. | | Q159 | **The tags from before CI get their releases too**, v0.1.0 to v0.10.0, built from each tag's own sources by running the workflow by hand. | | Q160 | Actions is switched on for the repository. | +| Q161 | **Changes reach `main` through pull requests, merged as "rebase, then a merge commit"**, the only style the repository allows: the branch's commits keep their messages, the merge commit marks the pull request, and what CI tested is what lands. No squash, no fast-forward. | ### As built diff --git a/platformio.ini b/platformio.ini index 0a10b9a..2b3c541 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,3 +46,14 @@ build_flags = platform = native lib_ldf_mode = deep+ build_flags = -std=gnu++17 + +; The same tests, built to count which lines of lib/ they run (scripts/coverage.sh). +[env:native-coverage] +extends = env:native +build_flags = + ${env:native.build_flags} + --coverage + -O0 +extra_scripts = + ${env.extra_scripts} + pre:scripts/coverage_link.py diff --git a/scripts/ci.sh b/scripts/ci.sh index 4f289b7..34ea0b1 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -1,7 +1,14 @@ #!/usr/bin/env bash # Local CI: run host unit tests, then build the firmware. +# Usage: scripts/ci.sh [tests|builds] one half only; both by default set -euo pipefail source "$(dirname "$0")/_docker.sh" DOCKER_EXTRA=() -run_in_container bash -c 'git config --global --add safe.directory "$PWD" && pio test -e native && pio run -e cardputer-adv -e cardputer-adv-debug' +case "${1:-all}" in + tests) STEPS='pio test -e native' ;; + builds) STEPS='pio run -e cardputer-adv -e cardputer-adv-debug' ;; + all) STEPS='pio test -e native && pio run -e cardputer-adv -e cardputer-adv-debug' ;; + *) echo "Usage: scripts/ci.sh [tests|builds]" >&2; exit 1 ;; +esac +run_in_container bash -c 'git config --global --add safe.directory "$PWD" && '"$STEPS" diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 0000000..01ab695 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Runs the host tests and says how much of lib/ they run: they're built with coverage counters. +# Usage: scripts/coverage.sh [out folder, default .pio/coverage] +# Out: summary.json (gcovr), coverage.svg (the README's badge), index.html (line by line). +# What it measures: the lines of lib/ that compile on a PC. Not lib/SD (the card's driver) and not +# src/ (the Apps, the Services, everything that needs the device): those have no host tests. +set -euo pipefail +source "$(dirname "$0")/_docker.sh" +DOCKER_EXTRA=() +OUT="${1:-.pio/coverage}" + +run_in_container bash -c ' + set -eo pipefail # a failing test fails this script, tail or not + git config --global --add safe.directory "$PWD" + rm -rf .pio/build/native-coverage "'"$OUT"'" + mkdir -p "'"$OUT"'" + pio test -e native-coverage | tail -1 + gcovr -r . --filter "lib/" --object-directory .pio/build/native-coverage \ + --json-summary "'"$OUT"'/summary.json" --html-details "'"$OUT"'/index.html" --print-summary | tail -4 + python3 scripts/coverage_badge.py "'"$OUT"'/summary.json" "'"$OUT"'/coverage.svg" +' diff --git a/scripts/coverage_badge.py b/scripts/coverage_badge.py new file mode 100755 index 0000000..eb08c7b --- /dev/null +++ b/scripts/coverage_badge.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Draws the README's coverage badge from gcovr's summary. + +Usage: scripts/coverage_badge.py + scripts/coverage_badge.py --plain