From 4f34ce0a7c927d3c174559bd0f644e437be8bc82 Mon Sep 17 00:00:00 2001 From: Dmitrii Okunev Date: Wed, 14 Oct 2026 15:16:53 +0100 Subject: [PATCH 1/3] ci: run the checks of the Makefile on GitHub Actions Nothing runs the checks of the Makefile on a change before it lands. Add a workflow that runs them on ubuntu-24.04 for every pull request and every push to main: - lint-and-test: make setup, then make -k -o qemu, which runs every check but the QEMU boots. - qemu: the packages of tests/qemu/ubuntu-packages, the udev rule that GitHub documents to open /dev/kvm to the runner user, then make qemu. The kernel RPMs and DriverSample.efi are cached under a key of the files that pin them and saved only when the job passes, as run.sh reuses DriverSample.efi unchecked. The logs of failed boots are uploaded. The actions are pinned to commit SHAs, and the token can only read the repository. Signed-off-by: Dmitrii Okunev --- .github/workflows/checks.yml | 79 ++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 81 insertions(+) create mode 100644 .github/workflows/checks.yml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..d845f48 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,79 @@ +# The checks of the Makefile; the QEMU boots, which need KVM and apt packages, in +# a job of their own. +name: ci + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +# PRs: a new push cancels the previous run. main: one group per run, as a shared +# group drops pending runs. +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - run: make setup + + # After setup: the key covers installed toolchains. + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + + # Every check but the boots. + - run: make -k -o qemu + + qemu: + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - run: | + sudo apt-get update + xargs --arg-file=tests/qemu/ubuntu-packages sudo apt-get install --yes + + # Opens /dev/kvm to the runner user, per + # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ + - run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | + sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + # No rust-cache: its cleanup takes run.sh's initramfs/tests for a cargo + # profile and logs errors; the builds take about a minute without it. + - run: make setup + + # Kernel RPMs and DriverSample.efi. Saved only on success, as run.sh reuses + # DriverSample.efi unchecked. + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + target/qemu-test/kernel-rpms + target/qemu-test/driver-sample + key: qemu-inputs-${{ hashFiles('tests/qemu/kernel.sha256', 'tests/qemu/build-driver-sample.sh') }} + + - run: make qemu + + - if: failure() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: qemu-test-logs + path: | + target/qemu-test/*/*/*.log + target/qemu-test/*/executables + if-no-files-found: ignore + retention-days: 14 diff --git a/README.md b/README.md index 82363e0..ad313e7 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,8 @@ make help # lists the checks `make qemu`, the QEMU tests, also needs an x86_64 host with `/dev/kvm` and the packages that [`tests/qemu/README.md`](tests/qemu/README.md) lists. +GitHub Actions runs the checks on every pull request and every push to main, as [`.github/workflows/checks.yml`](.github/workflows/checks.yml) describes. + ## QEMU tests End-to-end tests boot QEMU guests and run `uefisettings` against emulated firmware; [`tests/qemu/README.md`](tests/qemu/README.md) explains how to run them. From fb95ac5bb4c66cd2699f85c04383085af285c9ed Mon Sep 17 00:00:00 2001 From: Dmitrii Okunev Date: Thu, 15 Oct 2026 13:53:16 +0100 Subject: [PATCH 2/3] test(qemu): judge only the test in the health checks On some GitHub runners every boot failed its health checks although all tests passed: run.sh boots the guest with -cpu host, and on Intel hosts that hide IBRS from the guest, Linux 6.17 logs a RETBleed error while it boots (arch/x86/kernel/cpu/bugs.c, retbleed_update_mitigation). init failed on any kernel error since boot, so what the host's CPU makes the kernel log decided the result. init now writes a start record to /dev/kmsg and snapshots the taint and the warning count right before the test, then fails on a kernel error after the start record, on a changed taint or warning count, or when the start record is gone. The warning count catches a WARN whose taint bit the boot already set. The values are read with cat, as busybox read gets only the first digit of a sysctl. run.sh prints the host's CPU model with the tool versions, which the logs of the failing runner lacked. Signed-off-by: Dmitrii Okunev --- tests/qemu/init | 27 ++++++++++++++++++--------- tests/qemu/run.sh | 1 + 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/qemu/init b/tests/qemu/init index bfb386a..ac175a4 100755 --- a/tests/qemu/init +++ b/tests/qemu/init @@ -39,10 +39,18 @@ done # start and exit lines begin with a newline. run.sh prints the second port from the start # line on. printf '\nuefisettings-qemu-test: start %s %s\n' "$test" "$filter" > /dev/ttyS1 +# The health checks judge only the test: the kernel log and taint of the boot depend on +# the host, e.g. a RETBleed error on Intel hosts that hide IBRS from the guest. +read -r start < /proc/sys/kernel/random/uuid +echo "uefisettings-qemu-test: start of test $start" > /dev/kmsg +# cat, not read: busybox read reads a byte at a time, and numeric sysctls answer only a +# read at offset 0, so read would get only the first digit. +taint=$(cat /proc/sys/kernel/tainted /sys/kernel/warn_count) "/tests/$test" --test-threads=1 ${filter:+"$filter"} > /dev/ttyS1 2>&1 test_status=$? -# The kernel is not tainted (TAINT_* in include/linux/panic.h), kept its whole log (the -# first /dev/kmsg record has sequence number 0) and logged no error: each /dev/kmsg record +# The test added no taint (TAINT_* in include/linux/panic.h) or warning (a repeated WARN +# adds no taint bit), and the kernel kept its log +# (/dev/kmsg still has the start record) and logged no error since: each /dev/kmsg record # starts with facility << 3 | level, and the kernel's own facility is 0. /dev/kmsg keeps # the records that a log clear hides from dmesg. A read of it returns one record, fails if # the record does not fit and waits at the end of the log, so dd copies the records into @@ -50,19 +58,20 @@ test_status=$? # opens the FIFO for dd, so awk also stops if dd cannot open /dev/kmsg. health_status=0 { - read -r taint < /proc/sys/kernel/tainted - [ "$taint" = 0 ] || { echo "the kernel is tainted: $taint"; health_status=1; } - dd if=/dev/kmsg bs=8192 count=1 2> /dev/null | grep -q '^[0-9]*,0,' || { - echo "the first kernel log record is gone, or /dev/kmsg cannot be read" + tainted=$(cat /proc/sys/kernel/tainted /sys/kernel/warn_count) + set -- $taint + [ $# = 2 ] && [ "$tainted" = "$taint" ] || { + echo "the taint or warning count changed or is missing:" $taint, then $tainted health_status=1 } read -r end < /proc/sys/kernel/random/uuid echo "uefisettings-qemu-test: end of log $end" > /dev/kmsg mkfifo /tmp/kmsg dd if=/dev/kmsg bs=8192 2> /dev/null > /tmp/kmsg & - awk -v end="$end" ' - index($0, end) { found = 1; exit } - /^[0-3],/ { print; error = 1 } + awk -v start="$start" -v end="$end" ' + index($0, end) { found = started; exit } + started && /^[0-3],/ { print; error = 1 } + index($0, start) { started = 1 } END { if (!found) print "cannot read the kernel log"; exit !found || error }' /tmp/kmsg || health_status=1 kill "$!" 2> /dev/null diff --git a/tests/qemu/run.sh b/tests/qemu/run.sh index d1bd529..c5223e7 100755 --- a/tests/qemu/run.sh +++ b/tests/qemu/run.sh @@ -62,6 +62,7 @@ mapfile -t named < <(awk -F '\t' '$1 !~ /^(#|$)/ { echo "run.sh: start $(date -u +%FT%TZ), logs in $run" mkdir -p "$run/initramfs/bin" "$run/initramfs/tests" "$run/esp" +grep --max-count=1 '^model name' /proc/cpuinfo "$qemu" --version "$busybox" --help | sed -n 1p From 0475842e83b3f9d3d9236f6307fe481f25c5d30b Mon Sep 17 00:00:00 2001 From: Dmitrii Okunev Date: Thu, 15 Oct 2026 14:23:47 +0100 Subject: [PATCH 3/3] build: name the checks lint, test and test-e2e-qemu make lint test did not work, as the Makefile had no lint target, and the QEMU tests were called qemu, so CI ran make -k -o qemu to run every check but the boots. Name the checks after what they do: - lint runs fmt, clippy-x86_64, clippy-aarch64, clippy-uefi and doc. qemu-lint goes: the root cargo fmt --check already covers the QEMU test crates, as they are workspace members; doc now documents the whole workspace, publish-hiidb included; and the UEFI clippy of publish-hiidb becomes clippy-uefi. - test-e2e-qemu, formerly qemu, boots the scenarios; it no longer runs the lints first, as test does not either. CI runs make -k lint test and make test-e2e-qemu, in jobs of those names. Signed-off-by: Dmitrii Okunev --- .github/workflows/checks.yml | 6 +++--- Makefile | 35 ++++++++++++++++++++--------------- README.md | 5 +++-- tests/qemu/README.md | 4 ++-- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d845f48..8f002e7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -31,9 +31,9 @@ jobs: - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 # Every check but the boots. - - run: make -k -o qemu + - run: make -k lint test - qemu: + test-e2e-qemu: runs-on: ubuntu-24.04 timeout-minutes: 120 steps: @@ -66,7 +66,7 @@ jobs: target/qemu-test/driver-sample key: qemu-inputs-${{ hashFiles('tests/qemu/kernel.sha256', 'tests/qemu/build-driver-sample.sh') }} - - run: make qemu + - run: make test-e2e-qemu - if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 diff --git a/Makefile b/Makefile index a5a6bb3..bb4c5c1 100644 --- a/Makefile +++ b/Makefile @@ -11,18 +11,19 @@ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # The checks of uefisettings. They need GNU make and bash, rustup while -# RUST_TOOLCHAIN is set, as it is by default, and for qemu an x86_64 host with -# /dev/kvm and what tests/qemu/README.md lists: +# RUST_TOOLCHAIN is set, as it is by default, and for test-e2e-qemu an x86_64 +# host with /dev/kvm and what tests/qemu/README.md lists: # make every check of CHECKS, stopping at the first that fails # make -k every check of CHECKS, even after one fails # make ... the named checks, e.g. make clippy-aarch64 doc +# make lint the checks of LINTS +# make lint test every check but test-e2e-qemu # make RUST_TOOLCHAIN= the checks on the cargo of PATH # make setup installs, through rustup, the toolchain, components and # targets that the checks use # make help the checks # make -n prints the commands of a check without running them. The -# checks install nothing themselves. qemu needs qemu-lint, so under make -k a -# qemu-lint failure skips the qemu boots. +# checks install nothing themselves. # # A CI host runs make setup && make -k under timeout, as nothing bounds the time # that cargo takes: timeout -k 60 bash -c 'make setup && make -k'. It @@ -60,15 +61,21 @@ unexport CARGO_ENCODED_RUSTFLAGS TARGETS := x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,x86_64-unknown-uefi comma := , -CHECKS := fmt clippy-x86_64 clippy-aarch64 test doc qemu-lint qemu +# CI runs lint test and test-e2e-qemu: a new check joins LINTS or gets a CI job. +LINTS := fmt clippy-x86_64 clippy-aarch64 clippy-uefi doc +CHECKS := $(LINTS) test test-e2e-qemu -.PHONY: check help setup toolchain $(CHECKS) +.PHONY: check lint help setup toolchain $(CHECKS) check: $(CHECKS) +lint: $(LINTS) + help: @echo "checks: $(CHECKS)" @echo "make runs every check; make ... runs the named ones." + @echo "make lint runs $(LINTS)." + @echo "make lint test runs every check but test-e2e-qemu." @echo "make -k runs every check, even after one fails." @echo "make -n prints the commands of a check without running them." @echo "make RUST_TOOLCHAIN= runs the checks on the cargo of PATH." @@ -90,26 +97,24 @@ endif toolchain: @scripts/check-toolchain.sh '$(RUST_TOOLCHAIN)' '$(TARGETS)' +# Covers the workspace's QEMU test crates too. fmt: toolchain cargo fmt --check clippy-x86_64 clippy-aarch64: clippy-%: toolchain cargo clippy --locked --workspace --all-targets --all-features --target $*-unknown-linux-gnu -- -D warnings +# publish-hiidb, the one crate built for UEFI. +clippy-uefi: toolchain + cargo clippy --locked --release --target x86_64-unknown-uefi --manifest-path tests/qemu/publish-hiidb/Cargo.toml -- -D warnings + test: toolchain scripts/assert-tests-ran.sh cargo test --locked doc: toolchain - RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items --locked - -# The checks of the QEMU test crates that boot no guest. -qemu-lint: toolchain - cargo fmt --check --manifest-path tests/qemu/guest/Cargo.toml - RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items --locked --manifest-path tests/qemu/guest/Cargo.toml - cargo fmt --check --manifest-path tests/qemu/publish-hiidb/Cargo.toml - cargo clippy --locked --release --target x86_64-unknown-uefi --manifest-path tests/qemu/publish-hiidb/Cargo.toml -- -D warnings + RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps --document-private-items --locked # Boots the scenarios of tests/qemu/scenarios.tsv under KVM, with logs in # target/qemu-test//; see tests/qemu/README.md. -qemu: qemu-lint +test-e2e-qemu: toolchain tests/qemu/run.sh diff --git a/README.md b/README.md index ad313e7..0358088 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ mv lib.rs thrift/rust/uefisettings_spellings_db_thrift/uefisettings_spellings_db ## Checks -The `Makefile` in the root of the repository runs the checks that a change should pass: rustfmt, clippy for x86_64 and aarch64, the tests, the docs and the QEMU tests. +The `Makefile` in the root of the repository runs the checks that a change should pass: the lints (`make lint`: rustfmt, clippy for x86_64, aarch64 and UEFI, and the docs), the tests (`make test`) and the QEMU tests (`make test-e2e-qemu`). The checks need GNU make and bash, and rustup unless `make RUST_TOOLCHAIN=` runs them on the cargo of `PATH`. @@ -153,11 +153,12 @@ make setup # once: installs, through rustup, the Rust toolchain, compo make # every check, stopping at the first that fails make -k # every check, even after one fails make ... # the named checks, e.g. make clippy-aarch64 doc +make lint test # every check but the QEMU tests make RUST_TOOLCHAIN= # the checks on the cargo of PATH make help # lists the checks ``` -`make qemu`, the QEMU tests, also needs an x86_64 host with `/dev/kvm` and the packages that [`tests/qemu/README.md`](tests/qemu/README.md) lists. +`make test-e2e-qemu`, the QEMU tests, also needs an x86_64 host with `/dev/kvm` and the packages that [`tests/qemu/README.md`](tests/qemu/README.md) lists. GitHub Actions runs the checks on every pull request and every push to main, as [`.github/workflows/checks.yml`](.github/workflows/checks.yml) describes. diff --git a/tests/qemu/README.md b/tests/qemu/README.md index f0b1d25..aa90607 100644 --- a/tests/qemu/README.md +++ b/tests/qemu/README.md @@ -6,13 +6,13 @@ machine that runs them. ## Running -`make qemu` runs the checks of the QEMU test crates and then every scenario, on the Rust +`make test-e2e-qemu` runs every scenario, on the Rust toolchain and targets that `make setup` installs (see the Checks section of the repository's README): ```sh make setup # once -make qemu +make test-e2e-qemu ``` It needs an x86_64 host with `/dev/kvm` and, on Ubuntu 24.04, the packages of