From 2ef1d21236f613e1319ed2a89c147a8e52220d77 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Sun, 21 Jun 2026 03:20:42 +0000 Subject: [PATCH 1/5] ci: add distro-compat smoke-test matrix for cua-driver (CUA-599) Adds a GitHub Actions workflow that runs the *released* cua-driver binary inside real distro containers (debian:12, ubuntu:22.04, ubuntu:24.04, rockylinux:9, fedora:41) to catch glibc ABI floor regressions and distro packaging gaps that the NixOS test suite cannot detect. See CUA-599 for full context. --- .../workflows/ci-distro-compat-cua-driver.yml | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 .github/workflows/ci-distro-compat-cua-driver.yml diff --git a/.github/workflows/ci-distro-compat-cua-driver.yml b/.github/workflows/ci-distro-compat-cua-driver.yml new file mode 100644 index 0000000000..12e7e17ca0 --- /dev/null +++ b/.github/workflows/ci-distro-compat-cua-driver.yml @@ -0,0 +1,247 @@ +name: "CI: cua-driver distro-compat matrix" + +# Smoke-tests the *released* cua-driver binary inside real distro containers +# to catch bugs that the NixOS test suite cannot: glibc ABI floor issues and +# distro-packaging gaps (e.g. Qt5 AT-SPI bridge absent on Ubuntu). +# +# What this catches (the NixOS suite CANNOT catch): +# 1. glibc ABI floor — the released binary is built in a debian:11 +# container (glibc 2.31) so it should run on all of the matrix distros. +# If someone accidentally bumps the build container to a newer distro the +# --version smoke-test below will fail on the older glibc distros. +# 2. Runtime library gaps — `doctor` queries the OS for required capabilities +# (X11, AT-SPI, etc.). If a distro is missing a package the doctor command +# exits non-zero and prints a human-readable error. +# +# Design principles: +# - Run the RELEASED binary (downloaded from GitHub Releases), not a freshly +# built one. This is the only way to catch ABI mismatches because the NixOS +# CI builds its own binary against NixOS's own glibc. +# - Keep this fast and cheap: containers + --version/doctor only. Full GUI +# integration tests stay in nix-build.yml. +# - Non-blocking by default (continue-on-error: true) — the released binary +# may not exist yet on a fresh branch; the job is informational until the +# first linux release tag exists. +# +# Trigger: any PR that touches the Rust cua-driver or this workflow. +# Also runs on push to main and on workflow_dispatch so it works as a +# post-release regression guard. +# +# Companion to nix-build.yml — does NOT replace it. See CUA-599. + +on: + pull_request: + paths: + - "libs/cua-driver/rust/**" + - ".github/workflows/ci-distro-compat-cua-driver.yml" + push: + # Run on main (path-filtered) AND on release tags so a newly-published + # binary is immediately validated against the distro matrix. + branches: [main] + tags: + - "cua-driver-rs-v*" + paths: + - "libs/cua-driver/rust/**" + - ".github/workflows/ci-distro-compat-cua-driver.yml" + workflow_dispatch: + inputs: + version: + description: "cua-driver-rs version to test (without leading v). Leave blank to auto-detect latest." + required: false + default: "" + +permissions: + contents: read + +jobs: + # ── Resolve the binary version to test ─────────────────────────────────────── + # Fetch the latest published cua-driver-rs release tag so individual matrix + # jobs don't each hit the GitHub API. If workflow_dispatch supplied a version + # we use that instead. + resolve-version: + name: Resolve release version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.pick.outputs.version }} + binary_url: ${{ steps.pick.outputs.binary_url }} + steps: + - name: Pick version + id: pick + env: + GH_TOKEN: ${{ github.token }} + INPUT_VERSION: ${{ inputs.version }} + run: | + if [[ -n "$INPUT_VERSION" ]]; then + VERSION="$INPUT_VERSION" + elif [[ "$GITHUB_REF" == refs/tags/cua-driver-rs-v* ]]; then + VERSION="${GITHUB_REF#refs/tags/cua-driver-rs-v}" + else + # Fetch the latest release that matches the cua-driver-rs-v* pattern. + # The releases are marked prerelease=true so we use /releases instead + # of /releases/latest (which skips pre-releases). + VERSION=$(gh api repos/trycua/cua/releases \ + --jq '[.[] | select(.tag_name | startswith("cua-driver-rs-v"))] | first | .tag_name | ltrimstr("cua-driver-rs-v")' \ + 2>/dev/null || echo "") + fi + if [[ -z "$VERSION" ]]; then + echo "No cua-driver-rs release found yet — this is expected on a fresh branch." + echo "version=none" >> "$GITHUB_OUTPUT" + echo "binary_url=none" >> "$GITHUB_OUTPUT" + else + BINARY_URL="https://github.com/trycua/cua/releases/download/cua-driver-rs-v${VERSION}/cua-driver-rs-${VERSION}-linux-x86_64-binary.tar.gz" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "binary_url=$BINARY_URL" >> "$GITHUB_OUTPUT" + echo "Will test version: $VERSION" + echo "Binary URL: $BINARY_URL" + fi + + # ── Per-distro smoke-test matrix ───────────────────────────────────────────── + # Each job runs in a real distro container, downloads the released binary, + # and asserts that: + # 1. The binary executes at all (catches glibc ABI floor regressions). + # 2. `--version` prints a version string. + # 3. `doctor` exits 0 (or exits non-zero with a parseable diagnostic — + # the doctor command reports capabilities, some may be absent in a + # headless container; we treat a clean exit or a known-missing-display + # exit as success for the ABI test). + # + # Why these distros? + # debian:12 — glibc 2.36, representative of Debian stable users + # ubuntu:22.04 — glibc 2.35, Ubuntu LTS most widely deployed + # ubuntu:24.04 — glibc 2.39, also tests Qt5 AT-SPI bridge gap (see CUA-599) + # rockylinux:9 — glibc 2.34, RHEL/Rocky/AlmaLinux users + # fedora:41 — glibc 2.40, leading-edge RPM users + distro-smoke: + name: "${{ matrix.distro }} (glibc ${{ matrix.glibc_version }})" + needs: resolve-version + # Don't block the PR if no release binary exists yet. + continue-on-error: true + runs-on: ubuntu-latest + container: + image: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + include: + # Debian family + - distro: "debian:12" + image: "debian:12" + glibc_version: "2.36" + pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates" + - distro: "ubuntu:22.04" + image: "ubuntu:22.04" + glibc_version: "2.35" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates" + - distro: "ubuntu:24.04" + image: "ubuntu:24.04" + glibc_version: "2.39" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates" + # RPM family + - distro: "rockylinux:9" + image: "rockylinux:9" + glibc_version: "2.34" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates" + - distro: "fedora:41" + image: "fedora:41" + glibc_version: "2.40" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates" + + steps: + - name: Skip if no release binary + if: needs.resolve-version.outputs.version == 'none' + run: | + echo "No cua-driver-rs release binary exists yet. Skipping distro smoke-test." + echo "This is expected on branches before the first release tag." + exit 0 + + - name: Install curl + ca-certificates + if: needs.resolve-version.outputs.version != 'none' + run: ${{ matrix.pkg_install }} + + - name: Download released binary + if: needs.resolve-version.outputs.version != 'none' + env: + BINARY_URL: ${{ needs.resolve-version.outputs.binary_url }} + run: | + echo "Downloading: $BINARY_URL" + curl -fsSL "$BINARY_URL" -o cua-driver.tar.gz + tar -xzf cua-driver.tar.gz + chmod +x cua-driver + ls -lh cua-driver + + - name: Verify glibc floor (ldd) + if: needs.resolve-version.outputs.version != 'none' + run: | + # Print glibc version on this host and the minimum version the binary + # requires. This makes CI logs self-explanatory if the binary fails. + echo "=== Host glibc ===" + ldd --version | head -1 || true + echo "=== Binary glibc requirements ===" + # objdump / readelf may not be installed in minimal containers; + # strings is more universally available. + strings cua-driver | grep -E "^GLIBC_[0-9]" | sort -V | tail -5 || true + + - name: Smoke-test --version + if: needs.resolve-version.outputs.version != 'none' + run: | + echo "=== cua-driver --version ===" + # This is the primary ABI-floor gate: if the binary can't even print + # its version the glibc requirement is too high for this distro. + ./cua-driver --version + VERSION_OUT=$(./cua-driver --version) + echo "Output: $VERSION_OUT" + # Sanity-check that the output contains a version number. + if ! echo "$VERSION_OUT" | grep -qE "[0-9]+\.[0-9]+\.[0-9]+"; then + echo "ERROR: --version output does not contain a semver string" + exit 1 + fi + echo "PASS: --version" + + - name: Smoke-test doctor + if: needs.resolve-version.outputs.version != 'none' + run: | + echo "=== cua-driver doctor ===" + # doctor checks for runtime capabilities (display, AT-SPI, etc.). + # In a headless container many capabilities will be absent — that's + # expected and NOT a failure. What we test here is that: + # a. The binary loads and runs the doctor subcommand at all. + # b. It exits with a parseable status (not a SIGILL / glibc symbol + # error which would manifst as exit code 127 or similar). + set +e + ./cua-driver doctor 2>&1 + EXIT_CODE=$? + set -e + echo "doctor exit code: $EXIT_CODE" + # Exit codes that indicate glibc/ABI failure (command not found / bad ELF): + if [[ $EXIT_CODE -eq 127 || $EXIT_CODE -eq 126 ]]; then + echo "ERROR: cua-driver failed to execute (exit $EXIT_CODE) — likely glibc ABI mismatch" + exit 1 + fi + # Treat 0 (all capabilities present) or 1 (capabilities missing but + # doctor ran) as success — both mean the binary loaded correctly. + echo "PASS: doctor ran without ABI error" + + # ── Summary job ────────────────────────────────────────────────────────────── + # A single job that other status checks can require. Marks green when all + # distro-smoke jobs pass (or when the binary doesn't exist yet and all + # continue-on-error jobs skipped). + distro-compat-summary: + name: "Distro compat summary" + needs: [resolve-version, distro-smoke] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check results + run: | + echo "resolve-version result: ${{ needs.resolve-version.result }}" + echo "distro-smoke result: ${{ needs.distro-smoke.result }}" + # If resolve-version failed (API error etc.) that's a real failure. + if [[ "${{ needs.resolve-version.result }}" == "failure" ]]; then + echo "ERROR: resolve-version job failed" + exit 1 + fi + # distro-smoke is continue-on-error so its result is always + # 'success' even when individual jobs fail. The individual job + # logs are the source of truth; this summary job just gates + # the overall workflow status. + echo "All distro compat checks completed." From 9a39bd73d8546985f08d032cbd02a89579c432ed Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sun, 21 Jun 2026 22:02:53 +0000 Subject: [PATCH 2/5] review: fix double --version call, add binutils for strings, fix typo, clarify push.tags+paths comment --- .../workflows/ci-distro-compat-cua-driver.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-distro-compat-cua-driver.yml b/.github/workflows/ci-distro-compat-cua-driver.yml index 12e7e17ca0..a6857e870a 100644 --- a/.github/workflows/ci-distro-compat-cua-driver.yml +++ b/.github/workflows/ci-distro-compat-cua-driver.yml @@ -37,6 +37,9 @@ on: push: # Run on main (path-filtered) AND on release tags so a newly-published # binary is immediately validated against the distro matrix. + # NOTE: GitHub Actions ignores the `paths` filter for tag pushes — the + # workflow always runs on any cua-driver-rs-v* tag regardless of which + # files changed. This is expected and correct behaviour here. branches: [main] tags: - "cua-driver-rs-v*" @@ -127,24 +130,24 @@ jobs: - distro: "debian:12" image: "debian:12" glibc_version: "2.36" - pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates" + pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates binutils" - distro: "ubuntu:22.04" image: "ubuntu:22.04" glibc_version: "2.35" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils" - distro: "ubuntu:24.04" image: "ubuntu:24.04" glibc_version: "2.39" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils" # RPM family - distro: "rockylinux:9" image: "rockylinux:9" glibc_version: "2.34" - pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils" - distro: "fedora:41" image: "fedora:41" glibc_version: "2.40" - pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils" steps: - name: Skip if no release binary @@ -187,7 +190,6 @@ jobs: echo "=== cua-driver --version ===" # This is the primary ABI-floor gate: if the binary can't even print # its version the glibc requirement is too high for this distro. - ./cua-driver --version VERSION_OUT=$(./cua-driver --version) echo "Output: $VERSION_OUT" # Sanity-check that the output contains a version number. @@ -206,7 +208,7 @@ jobs: # expected and NOT a failure. What we test here is that: # a. The binary loads and runs the doctor subcommand at all. # b. It exits with a parseable status (not a SIGILL / glibc symbol - # error which would manifst as exit code 127 or similar). + # error which would manifest as exit code 127 or similar). set +e ./cua-driver doctor 2>&1 EXIT_CODE=$? From 5e941cd32943010ff733499f44ae36a946cd0613 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sun, 21 Jun 2026 23:02:00 +0000 Subject: [PATCH 3/5] fix(ci): install libX11 in distro-compat containers; fix curl conflict on rockylinux:9 The cua-driver binary dynamically links libX11, so running --version in a minimal container without libX11 fails with exit 127 (missing shared lib). Add libx11-6 (Debian/Ubuntu) and libX11 (Fedora/Rocky) to the pkg_install lists so the binary can actually execute. RockyLinux 9 ships curl-minimal which conflicts with the full curl package. Add --allowerasing to let dnf replace curl-minimal with curl, matching the error message's own suggestion. --- .github/workflows/ci-distro-compat-cua-driver.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-distro-compat-cua-driver.yml b/.github/workflows/ci-distro-compat-cua-driver.yml index a6857e870a..ad08419906 100644 --- a/.github/workflows/ci-distro-compat-cua-driver.yml +++ b/.github/workflows/ci-distro-compat-cua-driver.yml @@ -130,24 +130,26 @@ jobs: - distro: "debian:12" image: "debian:12" glibc_version: "2.36" - pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates binutils" + pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6" - distro: "ubuntu:22.04" image: "ubuntu:22.04" glibc_version: "2.35" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6" - distro: "ubuntu:24.04" image: "ubuntu:24.04" glibc_version: "2.39" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6" # RPM family + # Note: rockylinux:9 ships curl-minimal which conflicts with curl; use --allowerasing + # to let dnf replace curl-minimal with the full curl package. - distro: "rockylinux:9" image: "rockylinux:9" glibc_version: "2.34" - pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils" + pkg_install: "dnf install -y --allowerasing --setopt=install_weak_deps=False curl ca-certificates binutils libX11" - distro: "fedora:41" image: "fedora:41" glibc_version: "2.40" - pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils libX11" steps: - name: Skip if no release binary From 02f23d9363735387ae0b1d19e87b8356320bfa2d Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sun, 21 Jun 2026 23:05:32 +0000 Subject: [PATCH 4/5] fix(ci): install all X11 extension libs needed by cua-driver binary cua-driver links against multiple X11 extension libraries at startup: libX11, libXi (XInput), libXtst (XTest), libXfixes, libXrandr, libXcomposite, libXext. Any missing lib causes exit 127 before main() runs, which looks like a glibc ABI failure but isn't. Install the full set in pkg_install for both Debian/Ubuntu and RPM families. Also add an ldd diagnostic step to make future missing-lib failures immediately obvious in CI logs. --- .../workflows/ci-distro-compat-cua-driver.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-distro-compat-cua-driver.yml b/.github/workflows/ci-distro-compat-cua-driver.yml index ad08419906..56362c0cca 100644 --- a/.github/workflows/ci-distro-compat-cua-driver.yml +++ b/.github/workflows/ci-distro-compat-cua-driver.yml @@ -127,29 +127,33 @@ jobs: matrix: include: # Debian family + # cua-driver links against several X11 extension libs at startup: + # libX11 (core), libXi (XInput), libXtst (XTest), libXfixes, + # libXrandr, libXcomposite, libXext. All must be present or the + # binary exits 127 before reaching main(). - distro: "debian:12" image: "debian:12" glibc_version: "2.36" - pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6" + pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6 libxi6 libxtst6 libxfixes3 libxrandr2 libxcomposite1 libxext6" - distro: "ubuntu:22.04" image: "ubuntu:22.04" glibc_version: "2.35" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6 libxi6 libxtst6 libxfixes3 libxrandr2 libxcomposite1 libxext6" - distro: "ubuntu:24.04" image: "ubuntu:24.04" glibc_version: "2.39" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates binutils libx11-6 libxi6 libxtst6 libxfixes3 libxrandr2 libxcomposite1 libxext6" # RPM family # Note: rockylinux:9 ships curl-minimal which conflicts with curl; use --allowerasing # to let dnf replace curl-minimal with the full curl package. - distro: "rockylinux:9" image: "rockylinux:9" glibc_version: "2.34" - pkg_install: "dnf install -y --allowerasing --setopt=install_weak_deps=False curl ca-certificates binutils libX11" + pkg_install: "dnf install -y --allowerasing --setopt=install_weak_deps=False curl ca-certificates binutils libX11 libXi libXtst libXfixes libXrandr libXcomposite libXext" - distro: "fedora:41" image: "fedora:41" glibc_version: "2.40" - pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils libX11" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates binutils libX11 libXi libXtst libXfixes libXrandr libXcomposite libXext" steps: - name: Skip if no release binary @@ -185,6 +189,10 @@ jobs: # objdump / readelf may not be installed in minimal containers; # strings is more universally available. strings cua-driver | grep -E "^GLIBC_[0-9]" | sort -V | tail -5 || true + echo "=== Binary shared-library dependencies (ldd) ===" + # ldd shows all dynamic deps and flags any missing ones. This is + # purely informational — failures here are diagnosed at smoke-test time. + ldd ./cua-driver 2>&1 || true - name: Smoke-test --version if: needs.resolve-version.outputs.version != 'none' From a4084ac105655a9cf98eb815e2a49ad1714aada6 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sun, 21 Jun 2026 23:13:47 +0000 Subject: [PATCH 5/5] ci: re-trigger distro-compat check on PR head