Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions .github/workflows/ci-distro-compat-cua-driver.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
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 "")
Comment on lines +82 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -type f -name "ci-distro-compat-cua-driver.yml" 2>/dev/null | head -5

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

git ls-files | grep -i "ci-distro-compat-cua-driver.yml"

Repository: trycua/cua

Length of output: 104


🏁 Script executed:

cat -n .github/workflows/ci-distro-compat-cua-driver.yml | head -100

Repository: trycua/cua

Length of output: 5197


🏁 Script executed:

cat -n .github/workflows/ci-distro-compat-cua-driver.yml | sed -n '98,150p'

Repository: trycua/cua

Length of output: 3310


🏁 Script executed:

cat -n .github/workflows/ci-distro-compat-cua-driver.yml | tail -80

Repository: trycua/cua

Length of output: 4427


🏁 Script executed:

# Check GitHub CLI releases for trycua/cua to understand release distribution
gh api repos/trycua/cua/releases --jq '.[] | {tag_name, draft}' 2>/dev/null | head -40

Repository: trycua/cua

Length of output: 1545


🏁 Script executed:

# Count total releases to understand pagination risk
gh api repos/trycua/cua/releases --jq 'length' 2>/dev/null

Repository: trycua/cua

Length of output: 57


🏁 Script executed:

# Check if there are more releases beyond page 1 (need to use --paginate to see)
gh api --paginate repos/trycua/cua/releases --jq '.[] | .tag_name' 2>/dev/null | wc -l

Repository: trycua/cua

Length of output: 58


🏁 Script executed:

# Check what the current query actually returns (without pagination)
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

Repository: trycua/cua

Length of output: 60


🏁 Script executed:

# Check if the proposed fix works correctly and would catch more releases
gh api --paginate repos/trycua/cua/releases --jq '.[] | select((.draft | not) and (.tag_name | startswith("cua-driver-rs-v"))) | .tag_name' 2>/dev/null | head -5

Repository: trycua/cua

Length of output: 159


🏁 Script executed:

# Test the proposed fix
VERSION=$(gh api --paginate repos/trycua/cua/releases \
  --jq '.[] | select((.draft | not) and (.tag_name | startswith("cua-driver-rs-v"))) | .tag_name' \
  2>/dev/null | sed -n 's/^cua-driver-rs-v//p' | head -n1)
VERSION="${VERSION:-}"
echo "Proposed fix result: $VERSION"

Repository: trycua/cua

Length of output: 81


🏁 Script executed:

# Verify the proposed fix matches current code output
CURRENT=$(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 "")
echo "Current code result: $CURRENT"

PROPOSED=$(gh api --paginate repos/trycua/cua/releases \
  --jq '.[] | select((.draft | not) and (.tag_name | startswith("cua-driver-rs-v"))) | .tag_name' \
  2>/dev/null | sed -n 's/^cua-driver-rs-v//p' | head -n1)
PROPOSED="${PROPOSED:-}"
echo "Proposed fix result: $PROPOSED"

if [[ "$CURRENT" == "$PROPOSED" ]]; then
  echo "✓ Results match"
else
  echo "✗ Results differ"
fi

Repository: trycua/cua

Length of output: 124


Release auto-detection can silently fail when matching tags are off page 1.

Line 82 queries only the first /releases page. With 528 total releases in this repo (default 30 per page), future release patterns could push all cua-driver-rs-v* tags beyond page 1, causing VERSION to become empty. The workflow then reports false-green by skipping the matrix (jobs are conditional on version != 'none' and continue-on-error: true masks the silent failure).

Proposed fix
-            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 "")
+            VERSION=$(gh api --paginate repos/trycua/cua/releases \
+              --jq '.[] | select((.draft | not) and (.tag_name | startswith("cua-driver-rs-v"))) | .tag_name' \
+              2>/dev/null | sed -n 's/^cua-driver-rs-v//p' | head -n1)
+            VERSION="${VERSION:-}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 "")
VERSION=$(gh api --paginate repos/trycua/cua/releases \
--jq '.[] | select((.draft | not) and (.tag_name | startswith("cua-driver-rs-v"))) | .tag_name' \
2>/dev/null | sed -n 's/^cua-driver-rs-v//p' | head -n1)
VERSION="${VERSION:-}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-distro-compat-cua-driver.yml around lines 82 - 84, The
GitHub API query for the cua-driver-rs release version only retrieves the first
page of releases (30 per page by default), but with 528 total releases in the
repository, cua-driver-rs-v* tags could exist on later pages. Add pagination to
the gh api call by including the --paginate flag to retrieve all releases across
multiple pages before filtering with jq. This ensures the VERSION variable gets
populated correctly even when matching tags are beyond the first page,
preventing the workflow from silently skipping the matrix job.

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
# X11 runtime libs (libx11-6 libxi6 libxtst6 libxext6) and the
# Wayland client lib (libwayland-client0) are required because
# cua-driver is dynamically linked against the X11 input stack and
# the native Wayland backend (added in #1910). They are the runtime
# counterparts of the build-time deps
# (libx11-dev libxi-dev libxtst-dev libxext-dev libwayland-dev) used
# in the CD workflow. Without them the dynamic linker fails before
# main() and --version exits 127, which the smoke-test correctly
# treats as an ABI error. Installing only curl+ca-certificates is not
# enough.
- 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 libx11-6 libxi6 libxtst6 libxext6 libwayland-client0"
- 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 libx11-6 libxi6 libxtst6 libxext6 libwayland-client0"
- 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 libx11-6 libxi6 libxtst6 libxext6 libwayland-client0"
# RPM family
# Rocky Linux 9 ships curl-minimal in the base image which conflicts
# with the full curl package. Use --allowerasing to let dnf replace
# curl-minimal with curl, or skip curl and use curl-minimal (already
# present). We use --allowerasing so the install is explicit and
# consistent with what a user would do on a fresh Rocky install.
- distro: "rockylinux:9"
image: "rockylinux:9"
glibc_version: "2.34"
pkg_install: "dnf install -y --setopt=install_weak_deps=False --allowerasing curl ca-certificates libX11 libXi libXtst libXext libwayland-client"
- distro: "fedora:41"
image: "fedora:41"
glibc_version: "2.40"
pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates libX11 libXi libXtst libXext libwayland-client"

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 runtime deps (curl, ca-certificates, X11 libs)
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 manifest 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"
Comment on lines +231 to +237

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd .github/workflows && cat -n ci-distro-compat-cua-driver.yml | sed -n '210,240p'

Repository: trycua/cua

Length of output: 1815


🏁 Script executed:

# Check if there are other test/verification steps in the workflow
rg "EXIT_CODE" .github/workflows/ci-distro-compat-cua-driver.yml

# Check if there are other doctor-related checks elsewhere
rg -i "doctor" .github/workflows/ -A 2 -B 2 | head -80

Repository: trycua/cua

Length of output: 4726


doctor smoke gate accepts non-zero exits outside 126/127 as success, weakening ABI/runtime validation.

The current check on line 223 only rejects exit codes 126 and 127. This allows crash exits like 132 (SIGILL), 134 (SIGABRT), and 139 (SIGSEGV) to pass, even though the test intent (lines 210–216) is to verify the binary loads and exits with a parseable status—not a crash. The comment on lines 227–228 claims success for exits 0 or 1 only, but the code never validates EXIT_CODE is actually one of those values.

Proposed fix
          # 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.
+         # Treat only 0 (all capabilities present) or 1 (capabilities missing
+         # but doctor ran) as success.
+         if [[ $EXIT_CODE -ne 0 && $EXIT_CODE -ne 1 ]]; then
+           echo "ERROR: cua-driver doctor exited unexpectedly ($EXIT_CODE)"
+           exit 1
+         fi
          echo "PASS: doctor ran without ABI error"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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"
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 only 0 (all capabilities present) or 1 (capabilities missing
# but doctor ran) as success.
if [[ $EXIT_CODE -ne 0 && $EXIT_CODE -ne 1 ]]; then
echo "ERROR: cua-driver doctor exited unexpectedly ($EXIT_CODE)"
exit 1
fi
echo "PASS: doctor ran without ABI error"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-distro-compat-cua-driver.yml around lines 223 - 229,
The exit code validation in the smoke gate test is incomplete. Currently, the
condition starting with `if [[ $EXIT_CODE -eq 127 || $EXIT_CODE -eq 126 ]]` only
rejects those two specific exit codes, but allows other non-zero exits (like
132, 134, 139 from signal terminations) to pass as success. You need to add an
additional validation after the existing check to ensure that only exit codes 0
or 1 are treated as success, explicitly rejecting all other non-zero exit codes.
This will align the code behavior with the stated intent in the comment that
follows.


# ── 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."
Loading