Skip to content

Pin reviews: the five held rows go, as #655 brings each pin up to the… #2068

Pin reviews: the five held rows go, as #655 brings each pin up to the…

Pin reviews: the five held rows go, as #655 brings each pin up to the… #2068

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
schedule:
# Weekly: a pinned kernel's support status changes even when the pin does not (ADR-009), and
# a host can stop serving a pinned file the same way.
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
identity:
name: Commit identity
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
fetch-depth: 0
- name: Every commit is DevomB <Devom.hb@yahoo.com>
run: |
# A pull request's checkout is a synthetic merge commit; check the branch head instead.
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/check-commit-identity.sh "${{ github.event.pull_request.head.sha }}"
else
./tools/check-commit-identity.sh --all
fi
lint:
name: Shell lint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
# alsa-lib's headers for tools/tests/sound-tools.sh, which compiles the sound's C programs, and
# dosfstools, mtools and xorriso for media-same.sh, which stage 06's media functions need.
- name: Install shellcheck, alsa-lib's headers and the media tools
run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck libasound2-dev dosfstools mtools xorriso
- name: Syntax check every script
run: |
fail=0
while IFS= read -r f; do
if bash -n "$f"; then
echo " ok $f"
else
echo " FAIL $f"
fail=1
fi
done < <(find . -name '*.sh' -not -path './build/work/*')
exit $fail
- name: Tool fixture suites
run: |
# Exit 77 means the suite cannot run here: it is listed as skipped, never counted as a pass.
fail=0
skipped=""
for t in tools/tests/*.sh tools/tests/*.py; do
echo "=== $t"
# The step runs under bash -e; capture the status without tripping it.
"$t" && rc=0 || rc=$?
if [ "$rc" -eq 77 ]; then
echo " SKIPPED: $t"
skipped="$skipped $t"
elif [ "$rc" -ne 0 ]; then
echo " FAILED: $t"
fail=1
fi
done
if [ -n "$skipped" ]; then
echo
echo "did not run (not passes):$skipped"
fi
exit $fail
- name: Shellcheck
# Every tracked file whose first line names a shell.
run: |
mapfile -t scripts < <(git ls-files | xargs awk 'FNR==1 { if ($0 ~ /^#! ?\/(usr\/)?bin\/(env +)?(ba|da)?sh/) print FILENAME; nextfile }')
echo "${#scripts[@]} shell scripts"
[ "${#scripts[@]}" -ge 100 ] || { echo "the search for scripts found too few: it is broken"; exit 1; }
shellcheck -S warning -x "${scripts[@]}"
- name: Executable bits are recorded
run: |
# A commit from Windows records 100644; a Linux clone then fails with "Permission denied".
fail=0
for f in build/stages/*.sh tools/*.sh tools/tests/*.sh tools/tests/*.py \
tools/image/*.sh tools/image/*.py \
compartments/tests/*.sh tools/git-hooks/*; do
mode=$(git ls-files -s "$f" | awk '{print $1}')
if [ "$mode" != "100755" ]; then
echo " NOT EXECUTABLE ($mode): $f"
fail=1
fi
done
exit $fail
- name: No CRLF in scripts
run: |
# By name or by shebang: tools/kryptik and the run files have no extension.
git ls-files | while IFS= read -r f; do
case "$f" in *.sh|*.rs|*.py|Makefile|*/Makefile) ;; *) [ "$(head -c 2 "$f" 2>/dev/null)" = '#!' ] || continue ;; esac
if grep -qU $'\r' "$f"; then echo "$f"; fi
done > /tmp/crlf
if [ -s /tmp/crlf ]; then
cat /tmp/crlf
echo "CRLF found - scripts will fail on Linux with 'bad interpreter'"
exit 1
fi
echo "no CRLF"
manifest:
name: Source manifest
runs-on: ubuntu-24.04
# The Distro workflow's path: a cache's version hashes its path, so any other misses main's copy.
env:
KRYPTIK_SOURCES: /mnt/kryptik/sources
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Manifest resolves
run: |
sudo mkdir -p /mnt/kryptik && sudo chown "$(id -u):$(id -g)" /mnt/kryptik
./tools/fetch-sources.sh --list
# Restores the last sets; `make sources` checks them against the locks and fetches what is missing.
# The software images' sources, under images/, are a cache of their own (the prepare action's),
# set aside while the root's is saved.
- name: Upstream sources (cached by sources.lock)
id: sources
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: /mnt/kryptik/sources
key: sources-v3-${{ hashFiles('sources.lock') }}
restore-keys: |
sources-
# With the signatures, which verify-signatures keeps beside the root's: when an image's pin alone
# moves, only this cache is saved again.
- name: The software images' sources (cached by build/images/sources.lock)
id: image_sources
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: |
/mnt/kryptik/sources/images
/mnt/kryptik/sources/.signatures
key: image-sources-v1-${{ hashFiles('build/images/sources.lock') }}
restore-keys: |
image-sources-v1-
# Runs on two hits as well: the root's set restored may be one saved without the links that name
# each image source in sources/ (an older set's), and only this pass makes them. On a miss it
# fetches the signatures into the cached set for the gate below; that pass is not the verdict.
- name: Fetch and verify the sources
env:
MISSED: ${{ steps.sources.outputs.cache-hit != 'true' || steps.image_sources.outputs.cache-hit != 'true' }}
run: |
make sources
./tools/prune-sources.sh
[[ "$MISSED" != true ]] || ./tools/verify-signatures.sh > /dev/null 2>&1 || true
# From main alone, as the Distro jobs save it: a pull request's cache is scoped to its merge
# ref and would duplicate the set.
- name: Set the software images' sources aside
if: ${{ github.ref == 'refs/heads/main' && steps.sources.outputs.cache-hit != 'true' }}
run: if [[ -d /mnt/kryptik/sources/images ]]; then mv /mnt/kryptik/sources/images /mnt/kryptik/images-aside; fi
- name: Save the sources
if: ${{ github.ref == 'refs/heads/main' && steps.sources.outputs.cache-hit != 'true' }}
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: /mnt/kryptik/sources
key: ${{ steps.sources.outputs.cache-primary-key }}
- name: The software images' sources back in place
if: always()
run: if [[ -d /mnt/kryptik/images-aside ]]; then mv /mnt/kryptik/images-aside /mnt/kryptik/sources/images; fi
- name: Save the software images' sources
if: ${{ github.event_name != 'pull_request' && steps.image_sources.outputs.cache-hit != 'true' }}
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: |
/mnt/kryptik/sources/images
/mnt/kryptik/sources/.signatures
key: ${{ steps.image_sources.outputs.cache-primary-key }}
# Informational, as hosts go down: every place each source is kept, asked as a fetch asks them.
# The weekly Source reachability job is the one that fails on a source no place serves.
- name: Every source is served somewhere
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
continue-on-error: true
# It takes about three minutes; bounded, so a host that answers slowly holds up the
# signature gate after it for no longer than this.
timeout-minutes: 8
run: ./tools/fetch-sources.sh --probe
- name: Provenance of unsigned sources
# Strict on push and schedule; informational on pull requests, where a fork gets no token.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "pull request: informational (no token for fork PRs)"
./tools/verify-provenance.sh
else
./tools/verify-provenance.sh --strict
fi
- name: Provenance inventory
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# One class per source and no total: the classes are different strengths of evidence.
./tools/provenance-inventory.sh
# Each signature must verify against its publisher's key, or tools/source-notes.tsv says why not.
# On a pull request a key's published copy that does not answer is a warning, checked against the
# key held: a host being down is not the change's fault, and the push to main reads every copy.
- name: Signatures, strict
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/verify-signatures.sh --strict --unread-warns
else
./tools/verify-signatures.sh --strict
fi
# The root's sources in sources.lock, the software images' in build/images/sources.lock.
- name: sources.lock covers the manifest
run: |
fail=0
check() {
while read -r name ver url _; do
file=$(basename "$url")
if ! grep -q " $file\$" "$1"; then
echo " MISSING FROM $1: $file"
fail=1
fi
done
}
check sources.lock < <(./tools/fetch-sources.sh --list-root)
check build/images/sources.lock < <(./tools/fetch-sources.sh --list | grep -vxF -f <(./tools/fetch-sources.sh --list-root))
[ $fail -eq 0 ] && echo "sources.lock and build/images/sources.lock cover every manifest entry"
exit $fail
compartments:
name: Compartment layer
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: kryptikd unit tests
run: cd compartments/kryptikd && cargo test
# wlproxy and zoneid tests, and the shipped zone files against the identity invariant.
- name: Compositor tests
run: tools/tests/compositor.sh
# The static proxy under strace: every call it makes is on its seccomp list.
- name: The proxy's calls are on its filter's list
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq strace
rustup target add x86_64-unknown-linux-musl
tools/tests/proxy-calls.sh
- name: Zone definitions validate
run: |
cd compartments/kryptikd && cargo build --quiet
./target/debug/kryptikd check --zones ../zones
- name: Allow unprivileged user namespaces
run: |
# Ubuntu 24.04 restricts unprivileged user namespaces, which the zone suites need here.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
echo "userns check:"
unshare --user --map-root-user id -u
- name: kryptikd builds for musl
run: |
# The shipped binary is static musl; this catches glibc-only assumptions.
rustup target add x86_64-unknown-linux-musl
cd compartments/kryptikd
cargo build --release --target x86_64-unknown-linux-musl
cargo test --target x86_64-unknown-linux-musl
# The zone's reader of USB sticks (ADR-025), as it ships: static musl, its crates as locked.
- name: kryptik-stick tests
run: |
cd tools/stick
cargo test --locked
cargo build --locked --release --target x86_64-unknown-linux-musl
# exFAT as a camera leaves it: a card mkfs.exfat made, which fsck.exfat must find sound.
- name: kryptik-stick reads a card mkfs.exfat made
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq exfatprogs
tools/tests/exfat-card.sh
- name: Isolation exit test (the primitives)
run: |
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/adversarial.sh
- name: Real-launcher suite (the launch path itself)
run: |
# Unprivileged here, so groups K (privileged launch) and M (cgroup limits) report NOT RUN.
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/launcher.sh | tee /tmp/launcher.out
exit "${PIPESTATUS[0]}"
- name: The launcher suite reports its gaps
run: |
# Groups K and M need root: they run in the Distro workflow's zones-test, never here.
if ! grep -q 'PASSED WITH GAPS' /tmp/launcher.out; then
echo "::error::The launcher suite passed cleanly on a runner, where groups K and M cannot run."
echo "They either ran vacuously or were dropped; neither is a pass."
grep -E 'passed,|not run' /tmp/launcher.out || true
exit 1
fi
echo "The suite reported its gaps, as it must on a runner:"
sed -n '/not run (mandatory gaps/,/^$/p' /tmp/launcher.out
- name: What this CI run did and did not establish
if: always()
run: |
cat >> "$GITHUB_STEP_SUMMARY" <<'SUMMARY'
### What a green run here means
| tier | established here? |
|---|---|
| component (unit tests, tool fixtures) | yes |
| developer host (this runner's kernel, unprivileged) | yes |
| **target kernel (root, installed system, userns restricted)** | **no — the Distro workflow's acceptance run** |
The privileged launch path, the cgroup limits, the zone lifecycle
under root and the boot are tested by `make acceptance` on the built
media under KVM, not here.
SUMMARY
- name: The user-facing command
run: |
./compartments/tests/cli.sh
- name: The launch daemon, over its socket
run: |
# A runner has no XDG_RUNTIME_DIR, which the registry and proxy-socket checks need.
uid="$(id -u)"
sudo install -d -m 0700 -o "$uid" -g "$(id -g)" "/run/user/$uid"
export XDG_RUNTIME_DIR="/run/user/$uid"
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/serve.sh
- name: The zone boundary's regression probes
run: |
# Exits with the number of failures; root-only checks say NOT RUN and run in zones-test.
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/kryptikd/probes/boundary-checks.sh
# A job of its own, as it fetches pinned sources: a host that does not answer holds up no
# Distro dispatch, which waits on the compartment layer alone.
compositor:
name: Compositor build
runs-on: ubuntu-latest
# A fetch is bounded by curl's --max-time, but apt-get on a runner has hung before.
timeout-minutes: 20
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: The compositor's sources, keyed by their sources.lock lines
id: sources
run: |
echo "key=$(grep -E ' (wlroots-|wayland-protocols-|wayland-[0-9]|dwl-v|pixman-|libdrm-|libxkbcommon-|meson-)' sources.lock | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: ~/.cache/kryptik-compositor-sources
key: compositor-sources-${{ steps.sources.outputs.key }}
- name: The host's headers
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq libwayland-dev libwayland-bin libxkbcommon-dev libinput-dev \
libpixman-1-dev libdrm-dev libudev-dev libseat-dev pkg-config ninja-build bison libffi-dev \
libexpat1-dev xkb-data strace
# Kryptik's patches and edits to dwl, compiled against the pinned wlroots' headers: a C
# error in them shows here, not hours into a Distro build.
- name: dwl compiles with Kryptik's edits
run: KRYPTIK_TEST_SOURCES="$HOME/.cache/kryptik-compositor-sources" tools/tests/dwl-compile.sh
# The same dwl built over the pinned libraries and run headless: it starts, serves a client a
# window, and stops on SIGTERM.
- name: dwl runs headless
run: KRYPTIK_TEST_SOURCES="$HOME/.cache/kryptik-compositor-sources" tools/tests/dwl-headless.sh
kernel:
name: Kernel currency
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Pinned series are supported upstream
run: ./tools/check-support-status.sh --strict
# Survey, then gate; informational on a pull request, since an upstream release is not its fault.
- name: Pins behind upstream are reviewed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./tools/check-source-currency.sh --tsv > pin-survey.tsv
echo "survey: $(wc -l < pin-survey.tsv) sources"
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/check-pin-reviews.sh --survey pin-survey.tsv || echo "pull request: informational"
else
./tools/check-pin-reviews.sh --survey pin-survey.tsv
fi
- name: Pinned kernel is longterm and not EOL
run: ./tools/check-kernel-eol.sh
- name: Kernel config fragments validate against pinned source
run: |
# merge_config.sh drops unknown symbols silently; check each against the pinned source.
mkdir -p sources
# Exact names: '^linux ' would also match linux-hardened.
./tools/fetch-sources.sh --list \
| awk '$1 == "linux" || $1 == "linux-hardened" || $1 == "kernel-hardening-checker" {print $3}' \
| while read -r u; do
f="$(basename "$u")"
echo "fetching $f"
curl -fL --no-progress-meter -o "sources/$f" "$u"
# Before anything unpacks it: resolving the config runs the kernel tree's own make and scripts.
awk -v f="$f" '$2 == f' sources.lock | (cd sources && sha256sum --check --strict -)
done
./tools/validate-kernel-config.sh
./tools/validate-kernel-config.sh --hardened
./tools/validate-kernel-config.sh --boot
- name: Resolved kernel config keeps every fragment line and passes the hardening checker
run: |
# The GCC plugin options exist only with the plugin headers installed.
sudo apt-get install -y --no-install-recommends \
"gcc-$(gcc -dumpversion | cut -d. -f1)-plugin-dev" flex bison
make check-kernel-hardening
reachability:
name: Source reachability
# Weekly, or by hand: no push of ours moves a host, and a pull request should not fail for one.
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
# Fails on a source no place serves, which a build from an empty cache, as a release tag's
# is, would stop at; a source one place alone serves is a warning, a mirror row's cue.
- name: Every place each source is kept, asked for its file
run: ./tools/fetch-sources.sh --probe
# Each mirror row's file, downloaded and held to the lock: a row serving other bytes, or one
# for no source, fails; one that no longer serves the pin is a warning. Its downloads stay
# on the runner.
- name: Every mirror row has the lock's bytes
if: ${{ !cancelled() }}
run: ./tools/fetch-sources.sh --check-mirrors