Repository navigation
Pin reviews: the five held rows go, as #655 brings each pin up to the… #2068
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |