YAML 219 lines
# Per-push CI: host unit tests + rpi4b QEMU boot smoke. The rpi4b boot
# suite takes 5–8 min; it doubles as the link / cross-compile check for
# the rpi4b path (board_asm_defs.inc, mini-uart, BCM2711 timer, armstub,
# …). The faster virt boot smoke that used to run here is frozen as of
# 2026-06-17 (virt board deprioritized — rpi4b + real HW are the live
# gates); revive by restoring the -Dboard=virt test-virt step.
#
# Toolchain note: build.zig hardcodes the `aarch64-elf-` binutils
# prefix (matches the macOS Homebrew naming). Ubuntu's apt ships the
# same tools under `aarch64-linux-gnu-`, so the install step below
# symlinks them into PATH under the expected names.
name: test
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# GitHub switches all JavaScript actions to the Node 24 runtime on
# 2026-06-16. checkout@v5 already declares node24 and codecov-action@v5
# is a composite action (no Node runtime), but mlugg/setup-zig@v2
# (latest release) still declares node20 — upstream has no node24
# release yet. Forcing Node 24 now means every CI run proves the
# pipeline survives the switch, instead of finding out on enforcement
# day. Drop this once setup-zig ships a node24 release.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
test:
# ubuntu-24.04 (noble) ships QEMU 8.2, whose aarch64 system emulator
# carries the `raspi4b` machine type the rpi4b boot smoke needs;
# 22.04's QEMU 6.2 predates it (`unsupported machine type`). kcov is
# absent from noble's repos but that is moot — the coverage step
# already builds kcov v43 from source (jammy's apt v38 is too old to
# read zig's DWARF), so nothing here depends on an apt kcov.
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: install zig 0.16.0
uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: hygiene gate
# Fails on trailing spaces, hard tabs in *.zig, CRLF, or
# lowercase hex literals in src/.
run: zig build check-hygiene
- name: doc-drift gate
# Deterministic doc-drift subset: fails the build only on a dead
# repo-relative path referenced by an active public doc (README,
# DOCUMENTATION, SETUP). Version/target/scenario mismatches are
# printed as advisory warnings; the fuzzy prose review stays with
# the human-driven /doc-drift pass.
run: bash scripts/check_doc_drift.sh
- name: install aarch64 binutils + mtools + qemu build deps
run: |
sudo apt-get update
# mtools (mformat/mcopy/minfo/mdir) builds the rpi4b FAT32 test
# disk in scripts/make_test_disk.sh — needed since rpi4b became
# the sole boot gate (virt did not seed a backing image).
#
# No apt qemu: the `raspi4b` machine type landed in QEMU 9.0
# (Apr 2024); every GitHub-hosted runner's apt qemu predates it
# (noble 8.2, jammy 6.2), so the next steps build qemu-system-
# aarch64 from a pinned source release. The libs below are both
# its build and (via the runtime .so they pull in) its run deps,
# so this always-run step keeps a cache-hit qemu linkable.
sudo apt-get install -y binutils-aarch64-linux-gnu mtools \
ninja-build pkg-config python3-pip python3-venv \
libglib2.0-dev libpixman-1-dev flex bison
# build.zig invokes `aarch64-elf-objcopy` and `aarch64-elf-nm`;
# Ubuntu's apt ships them as `aarch64-linux-gnu-*`. Symlinks
# bridge the prefix without touching build.zig (Pi byte-identity
# is sensitive to build.zig changes).
sudo ln -sf /usr/bin/aarch64-linux-gnu-objcopy /usr/local/bin/aarch64-elf-objcopy
sudo ln -sf /usr/bin/aarch64-linux-gnu-nm /usr/local/bin/aarch64-elf-nm
# The rpi4b boot smoke needs QEMU >= 9.0 for the `raspi4b` machine.
# Pin 11.0.1 — the version the boot contract in run_qemu_test.sh was
# captured against locally — so CI emulates byte-for-byte what the
# operator validates by hand (free-page checkpoints, scenario tally).
# Built once per version, cached on the version key, then instant.
- name: cache qemu-system-aarch64 (pinned source build)
id: qemu
uses: actions/cache@v4
with:
path: ~/qemu-prefix
key: qemu-aarch64-11.0.1-${{ runner.os }}
- name: build qemu-system-aarch64 from pinned source
if: steps.qemu.outputs.cache-hit != 'true'
# Single-target build (aarch64-softmmu only) keeps it to a few
# minutes; we drive QEMU headless (-display none), so the UI/tools
# frontends are disabled.
run: |
ver=11.0.1
curl -fsSL -o /tmp/qemu.tar.xz "https://download.qemu.org/qemu-$ver.tar.xz"
mkdir -p /tmp/qemu-src
tar -xJf /tmp/qemu.tar.xz -C /tmp/qemu-src --strip-components=1
cd /tmp/qemu-src
./configure --prefix="$HOME/qemu-prefix" --target-list=aarch64-softmmu \
--disable-docs --disable-tools --disable-gtk --disable-sdl \
--disable-vnc --disable-curses --disable-werror
make -j"$(nproc)"
make install
- name: put pinned qemu on PATH
# GITHUB_PATH prepends for all later steps, so the boot smoke's
# bare `qemu-system-aarch64` resolves to the pinned build, not apt.
run: |
echo "$HOME/qemu-prefix/bin" >> "$GITHUB_PATH"
"$HOME/qemu-prefix/bin/qemu-system-aarch64" --version
"$HOME/qemu-prefix/bin/qemu-system-aarch64" -machine help | grep -i raspi4b
# FlashOS source modules are written in Flash (*.flash) and transpiled
# to Zig at build time by flashc. build.zig resolves the compiler at
# ~/Flash/zig-out/bin/flashc-stage1 by default; the pinned revision
# lives in flash-toolchain.lock. Flash ships no prebuilt binaries, so
# build the self-hosted stage1 from source at the pinned commit. The
# binary is cached on the lock hash, so it only rebuilds when the pin
# moves.
- name: cache flashc (pinned Flash toolchain)
id: flashc
uses: actions/cache@v4
with:
path: ~/Flash/zig-out/bin/flashc-stage1
key: flashc-stage1-${{ runner.os }}-${{ hashFiles('flash-toolchain.lock') }}
- name: build flashc from the pinned Flash commit
if: steps.flashc.outputs.cache-hit != 'true'
# `zig build stage1` (not the bare `zig build`, which emits only the
# stage0 bootstrap seed `flashc`) produces the self-hosted
# `flashc-stage1` that flash-toolchain.lock pins.
run: |
commit=$(grep -oE '^flash-commit[[:space:]]*=[[:space:]]*[0-9a-f]{40}' flash-toolchain.lock | grep -oE '[0-9a-f]{40}')
git clone https://github.com/ajhahnde/Flash.git ~/Flash
git -C ~/Flash checkout "$commit"
( cd ~/Flash && zig build stage1 )
test -x ~/Flash/zig-out/bin/flashc-stage1
- name: host unit tests
run: zig build test
- name: build kcov v43 from source
# Ubuntu jammy ships kcov v38 (2020). Its DWARF and breakpoint
# handling predates two fixes that zig binaries need: addresses
# that map to multiple lines (v39) and BFD state clearing
# between files (v41) — without them kcov silently drops
# coverage for most of the test binaries. Ubuntu 24.04 dropped
# the kcov package entirely, so build the current release from
# source instead of using apt.
run: |
sudo apt-get install -y binutils-dev libssl-dev libelf-dev zlib1g-dev libdw-dev libiberty-dev libcurl4-openssl-dev
git clone --depth 1 --branch v43 https://github.com/SimonKagstrom/kcov /tmp/kcov-src
cmake -S /tmp/kcov-src -B /tmp/kcov-build -DCMAKE_BUILD_TYPE=Release
make -C /tmp/kcov-build -j"$(nproc)"
sudo make -C /tmp/kcov-build install
kcov --version
- name: host unit tests with coverage
# Coverage pass. Four things make this report trustworthy:
#
# - -Dcoverage builds the test binaries with the LLVM backend;
# zig's self-hosted x86_64 backend (Debug default on
# x86_64-linux) emits DWARF that kcov cannot read.
# - Both zig cache tiers point at fresh run-local dirs. The
# shared .zig-cache is restored from the Actions cache, and
# zig satisfying test roots from stale artifacts there
# silently shrinks the report.
# - The binary count is checked against the addHostTest
# registrations in build.zig, so a missing test root fails
# the job instead of shrinking the badge.
# - kcov output dirs are numbered (every zig test binary is
# named `test`, so basenames collide) and merged into a
# single report; the include filter is anchored to the
# workspace so zig's own standard library stays out.
#
# The `zig build test` step above stays the green/red
# correctness gate; kcov failures on individual binaries stay
# non-fatal (|| true).
run: |
export ZIG_LOCAL_CACHE_DIR="$RUNNER_TEMP/cov-local"
export ZIG_GLOBAL_CACHE_DIR="$RUNNER_TEMP/cov-global"
zig build test -Dcoverage
bins=$(find "$ZIG_LOCAL_CACHE_DIR" "$ZIG_GLOBAL_CACHE_DIR" -type f -name 'test*' -perm -111 2>/dev/null)
expected=$(grep -c 'addHostTest(b, test_step' build.zig)
found=$(echo "$bins" | grep -c .)
echo "coverage: found $found test binaries, expected $expected"
test "$found" -eq "$expected" || { echo "coverage: binary count mismatch" >&2; exit 1; }
mkdir -p coverage/raw
i=0
for bin in $bins; do
i=$((i+1))
kcov --include-path="$GITHUB_WORKSPACE/src,$GITHUB_WORKSPACE/lib,$GITHUB_WORKSPACE/user_space" "coverage/raw/$i" "$bin" || true
done
kcov --merge coverage/merged coverage/raw/*
echo "coverage: merged report contains:"
find coverage/merged -name 'cobertura.xml' -o -name 'codecov.json'
- name: upload coverage to codecov
uses: codecov/codecov-action@v5
with:
directory: ./coverage/merged
fail_ci_if_error: false
- name: rpi4b QEMU boot smoke
# -Dci-login-seed seeds flash/flash before /bin/login so the
# unattended boot authenticates with no typist. -Dboot-selftest runs
# the in-kernel [TEST] harness (the 28-scenario / 32-checkpoint
# boot-as-test path run_qemu_test.sh asserts). Both default OFF so
# hardware deploys boot clean straight to a real login: prompt; the
# watchdog needs both ON.
run: zig build -Dboard=rpi4b -Dci-login-seed=true -Dboot-selftest=true test-rpi4b