Skip to content

Verify the platforms this project claims, and support more of them #677

Verify the platforms this project claims, and support more of them

Verify the platforms this project claims, and support more of them #677

Workflow file for this run

name: AutoControl Docker CI
on:
push:
branches: [ "dev", "main" ]
paths:
- "docker/**"
- "je_auto_control/**"
- "pyproject.toml"
- ".github/workflows/docker.yml"
pull_request:
branches: [ "dev", "main" ]
paths:
- "docker/**"
- "je_auto_control/**"
- "pyproject.toml"
- ".github/workflows/docker.yml"
permissions:
contents: read
jobs:
build-image:
name: Build AutoControl container
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build image (no push)
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile
tags: autocontrol:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image size
run: docker image inspect autocontrol:ci --format='size={{.Size}} bytes'
headless-tests:
name: Headless pytest inside the image
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Rebuild image (cached)
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile
tags: autocontrol:ci
load: true
cache-from: type=gha
# Mount the repo so pytest can read tests + write the artifact.
- name: Run headless tests under Xvfb
run: |
docker run --rm \
--user root \
-v "$PWD:/work" -w /work \
--entrypoint /bin/sh \
autocontrol:ci -c "
pip install --no-cache-dir -r dev_requirements.txt &&
xvfb-run -a -s '-screen 0 1280x800x24' \
python -m pytest -q --tb=short
"
- name: Smoke test the entrypoint (rest mode)
run: |
# Run without --rm so a quick crash leaves the container in
# place for ``docker logs`` to inspect afterwards. Final
# ``docker rm -f`` cleans up regardless of exit state.
docker run -d --name ac-rest -p 9939:9939 \
-e AC_TOKEN=ci-token autocontrol:ci rest
ok=0
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS -H "Authorization: Bearer ci-token" \
http://127.0.0.1:9939/health; then
echo "REST API is up"
ok=1
break
fi
sleep 2
done
echo "::group::docker logs ac-rest"
docker logs ac-rest || true
echo "::endgroup::"
echo "::group::docker inspect (state)"
docker inspect --format '{{json .State}}' ac-rest || true
echo "::endgroup::"
docker rm -f ac-rest >/dev/null 2>&1 || true
if [ "$ok" -ne 1 ]; then
echo "REST health probe never succeeded" >&2
exit 1
fi
wayland-verification:
name: Wayland backend against a real compositor
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build the Wayland verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.wayland
tags: autocontrol-wayland:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
# Two halves, both of which mocks structurally cannot cover.
#
# Capture: sway's headless backend needs no GPU, no seat and no display,
# so a plain ubuntu runner can host a genuine wlroots session. grim's
# argv and -g geometry, wlr-randr's undocumented output format, and the
# whole screenshot -> screen_grabber -> capture -> grab_image chain are
# checked against pixels the compositor actually painted. Twice: once
# with the outputs side by side from the origin, and once with the
# left-hand one at x=-1280, which is the layout of any desktop with a
# monitor left of the primary. The whole-screen capture then starts at
# a negative coordinate, and a size, a crop or a located hit that
# assumes (0, 0) is wrong by the width of that monitor.
#
# libei: every entry point the ctypes binding names is resolved against
# the real libei.so — a misspelled symbol passes every mock and fails
# only on a user's machine — and the fail-closed chain is driven end to
# end. It also re-checks whether ei_unref still segfaults upstream, so
# the workaround in LibeiBackend._teardown gets removed once it can be.
#
# The container exits with the number of failed checks.
- name: Verify against headless sway and the real libei
run: docker run --rm autocontrol-wayland:ci
eis-verification:
name: libei sender against a real EIS server
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build the EIS verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.eis
tags: autocontrol-eis:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
# The other half of the input path, and the half no mock can reach: a
# wrong capability value, a mis-marshalled variadic bind or a wrong
# scroll unit is *accepted* by a fake symbol table and only rejected by
# something that speaks the protocol. libeis is that something — the
# server side of libei's own protocol, packaged by Debian — so
# docker/eis_server.py runs a real EIS implementation on a Unix socket
# and records what AutoControl's real sender does to it. No compositor
# and no desktop session are involved.
#
# It also re-checks whether ei_unref is still safe on a live context,
# which is what lets _teardown release instead of leaking.
#
# The container exits with the number of failed checks.
- name: Verify the libei sender against libeis
run: docker run --rm autocontrol-eis:ci
portal-verification:
name: RemoteDesktop portal handshake against a real liboeffis
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build the portal verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.portal
tags: autocontrol-portal:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
# How a client gets an EIS socket on GNOME and KDE: not a path on disk
# but a file descriptor handed over D-Bus at the end of the
# org.freedesktop.portal.RemoteDesktop dance. That was recorded as
# needing a GNOME VM because xdg-desktop-portal-wlr has no RemoteDesktop
# interface — but the portal is a D-Bus interface, not a compositor
# feature, so docker/portal_server.py owns the well-known name on a
# private session bus and answers the four calls for real.
#
# Its ConnectToEIS hands back a live connection to the same real libeis
# server the eis job uses, so the whole chain is checked at once: the
# call order and predicted request paths, the device mask the user would
# be consenting to, that the descriptor carries a real EI session, and
# that input emitted through it is recorded by an independent
# implementation.
#
# And every way a portal says no — a dismissed dialog, a dialog left
# open, a withheld descriptor, a closed session, a portal too old to
# have ConnectToEIS, no portal at all — has to come back as a refusal on
# this project's own clock rather than a hang or a silent downgrade.
#
# The container exits with the number of failed checks.
- name: Verify the portal handshake against liboeffis
run: docker run --rm autocontrol-portal:ci
x11-verification:
name: X11 backend against a real X server
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build the X11 verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.x11
tags: autocontrol-x11:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
# Wayland ended up with five jobs that read back what reached a real
# peer. X11 — the older and more widely deployed of the two Linux
# paths — had none: every X11 assertion in the suite is made against a
# mock of python-Xlib, so nothing had confirmed that an injected event
# reaches a client at all.
#
# Ground truth deliberately comes from other codebases than the one
# under test. xev is a real X client that prints every event delivered
# to its window, so a click is read back the way the ydotool job reads
# its events off /dev/input/eventN — including `synthetic NO`, which is
# what separates real server input from XSendEvent traffic that
# toolkits discard. ImageMagick's `import` is an independent grabber,
# in the role grim plays for Wayland, against a root window painted two
# asymmetric colours so a wrong rectangle cannot look right.
#
# It runs twice: one monitor over the whole screen, then two RANDR
# monitors side by side. There is deliberately no negative-origin pass
# — on X11 the root window is the union of every monitor and always
# starts at (0, 0), so the Wayland job's second layout has no analogue
# here. That is a protocol difference, not an untested case.
#
# The container exits with the number of failed checks.
- name: Verify the X11 backend against a real X server
run: docker run --rm autocontrol-x11:ci
seat-verification:
name: ydotool absolute move against a seat that consumes it
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build the seat verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.seat
tags: autocontrol-seat:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Load the uinput and evdev kernel modules
run: |
sudo modprobe uinput
sudo modprobe evdev || true
test -e /dev/uinput || {
echo "::error::/dev/uinput is missing after modprobe; this runner's"
echo "::error::kernel cannot host the seat verification."
exit 1
}
# The join between the two images above, and the one every earlier note
# in this file said needed a VM. The wayland job runs a compositor that
# consumes no input; the ydotool job reads ydotool's events off the
# kernel with no compositor. Neither can say where the cursor ends up.
#
# wlroots can: WLR_BACKENDS=headless,libinput keeps the outputs virtual
# while running the real libinput backend, libseat's builtin backend
# opens the device without logind, and SEATD_VTBOUND=0 stops it
# reaching for a VT no container owns. ydotoold's device is then an
# ordinary seat device, and grim -c draws the cursor into a screenshot.
#
# That settles what --absolute is absolute *to* — the top-left of the
# output layout, not layout (0, 0), which is the translation
# linux_wayland/mouse.py now applies — and what pointer acceleration
# does to it, which is double the distance asked for under libinput's
# default profile. It runs over the same two layouts as the wayland
# job, and the negative-origin one is where an untranslated request
# lands on the wrong monitor entirely.
#
# The container exits with the number of failed checks.
- name: Verify the absolute move against a real seat
run: |
docker run --rm --device /dev/uinput --device-cgroup-rule 'c 13:* rmw' autocontrol-seat:ci
ydotool-verification:
name: ydotool argv against a real uinput device
needs: build-image
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
- name: Build the ydotool verification image
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
with:
context: .
file: docker/Dockerfile.ydotool
tags: autocontrol-ydotool:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
# uinput is what ydotoold writes into; evdev is the handler that turns
# the device it creates into the /dev/input/eventN this verification
# reads back. Both ship with the runner's kernel as modules. The
# explicit check is here so a kernel without them fails saying so,
# rather than the container reporting an empty device list.
- name: Load the uinput and evdev kernel modules
run: |
sudo modprobe uinput
sudo modprobe evdev || true
test -e /dev/uinput || {
echo "::error::/dev/uinput is missing after modprobe; this runner's"
echo "::error::kernel cannot host the ydotool verification."
exit 1
}
ls -l /dev/uinput
# The half neither other image can reach. sway's headless backend
# consumes no libinput devices, so an injected event has nowhere to
# arrive there — but arriving is not what is being checked. ydotoold
# creates an ordinary uinput device and the kernel publishes it as an
# evdev node, so reading that node returns the exact input_event structs
# ydotool wrote, with no compositor and no seat in the picture.
#
# That settles the click bitmasks, the split press / release edges drag
# depends on, what --absolute really puts on the wire, and the wheel
# signs this project had assumed from the kernel's REL_WHEEL convention
# and never measured. The last check drives the backend's own functions
# rather than a hand-written argv, so the two halves meet.
#
# --device covers /dev/uinput, which exists before the container starts.
# The input node does not — ydotoold creates it afterwards — so the
# cgroup rule grants character major 13 and nothing else, which is much
# narrower than --privileged.
#
# The container exits with the number of failed checks.
- name: Verify the ydotool argv against the kernel
run: |
docker run --rm \
--device /dev/uinput \
--device-cgroup-rule 'c 13:* rmw' \
autocontrol-ydotool:ci