fix(ci): run desktop smoke headless on rpm roots without Xvfb (#16211)

* fix(ci): give the RHEL 10 desktop smoke a headless display

RHEL 10 removed xorg-x11-server-Xvfb from every repository: BaseOS,
AppStream, CRB, extras and EPEL 10 contain no such build, so the rpm
build-root dependency step failed one step after #16154 fixed the EPEL
bootstrap (`No match for argument: xorg-x11-server-Xvfb`, run 41642580745,
job 125940965630).

Keep Xvfb for the Fedora 44 leg and give the RHEL 10 smoke a real display
instead: a weston headless compositor with rootless Xwayland on top, since
GTK3 has no headless backend and WebKitGTK always needs a display server.
Export WAYLAND_DISPLAY before Xwayland is launched, wait for both sockets
with a hard timeout, and pin the app to X11 so the bridge is actually used.

Verified end to end on a Desktop Checks dispatch of this head (run
41652880267): the RHEL 10 rpm lane passes dependency installation and the
packaged smoke, and the Fedora 44 lane keeps its xvfb-run path. The three
workflow meta-tests that parse this file pass (161 passed, 1 skipped).

* fix(ci): key the rpm display arm on the matrix and harden its meta-test

Review round 2 findings on #16211:

- R2-1: the display arm keyed on an artifact_suffix literal while the EPEL
  bootstrap it depends on keys on matrix.epel. Declare matrix.display
  ('xvfb' on Fedora 44, 'weston' on RHEL 10) and branch on that, so a leg
  added later picks a server instead of inheriting an arm by accident.
- R2-2: the xvfb-run arm shipped a shell function through declare -f into a
  child bash without -e -u -o pipefail. Run the same npm command in both
  arms so both keep the step's error regime; assert no declare -f remains.
- R2-3 / R1-4: bound the shared package list by the conditional that follows
  it instead of a prose comment, ignore comments when checking it, and assert
  the xvfb/weston arms, the weston socket name against WAYLAND_DISPLAY, and
  the Xwayland display number against DISPLAY and the X11 socket.

* fix(ci): fail the rpm display arm closed and pin the smoke's wiring

R2-1: the install arm keyed on `matrix.display` but defaulted every unknown
value to the weston arm, which only works where the EPEL bootstrap above has
run. Turn it into an explicit `case` with an error arm, and assert from the
matrix that a non-Fedora root taking the weston arm has EPEL enabled, so a new
leg with `display: 'weston'` and `epel: 'false'` turns the meta-test red
instead of dying at `dnf install` in a release run.

R1-4: restore the two pins the previous rewrite dropped
(`weston --backend=headless-backend.so`, `XDG_RUNTIME_DIR`) and bind the rest
to the step's structure rather than to bare substrings: the weston readiness
wait must sit between the two launches, the helper must `return 1` on timeout,
and the EXIT trap must reap both daemons. Each added assertion was checked
against the mutation it names (all five turn red; the unmutated tree stays
green).

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw1dkg4zbx

---------

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
易良andQwen-Coder authored and GitHub committed 2026-11-04 10:40:34 +08:00
1 parent b5975157d1
commit 4593a3b8f2
2 files changed
+200 -2

No files matched your search

+67 -2
View File
@@ -381,6 +381,7 @@ jobs:
glibc_floor: '2.43'
rpm: 'true'
epel: 'false'
display: 'xvfb'
legacy_arch: ''
rust_target: 'x86_64-unknown-linux-gnu'
tauri_config: 'src-tauri/rpm-fedora44.conf.json'
@@ -394,6 +395,7 @@ jobs:
glibc_floor: '2.39'
rpm: 'true'
epel: 'true'
display: 'weston'
legacy_arch: ''
rust_target: 'x86_64-unknown-linux-gnu'
tauri_config: 'src-tauri/rpm-rhel10.conf.json'
@@ -484,7 +486,29 @@ jobs:
rpm cpio \
libappindicator-gtk3-devel librsvg2-devel \
gtk3 mesa-libgbm libdrm at-spi2-core dbus-x11 xdg-utils \
xorg-x11-server-Xvfb webkit2gtk4.1-devel
webkit2gtk4.1-devel
# Fedora 44 still ships Xvfb; RHEL 10 removed it from every
# repository (BaseOS, AppStream, CRB, extras and EPEL 10 carry no
# such build). `matrix.display` names the server each rpm root can
# install, so a leg added later picks one instead of inheriting an
# arm by accident. An unknown value fails closed: the weston arm
# needs the EPEL bootstrap above, so guessing it would be wrong on
# every root that has no EPEL.
case '${{ matrix.display }}' in
xvfb)
dnf install -y --setopt=install_weak_deps=False xorg-x11-server-Xvfb
;;
weston)
# weston (EPEL) provides a headless Wayland display and Xwayland
# (AppStream) bridges it to X11 for WebKitGTK. The smoke step
# below starts both.
dnf install -y --setopt=install_weak_deps=False weston xorg-x11-server-Xwayland
;;
*)
echo "matrix.display must be 'xvfb' or 'weston', got '${{ matrix.display }}'" >&2
exit 1
;;
esac
- name: 'Install Qwen Code dependencies'
shell: 'bash'
@@ -813,7 +837,48 @@ jobs:
shell: 'bash'
run: |
set -euo pipefail
xvfb-run -a npm run smoke:packaged -- src-tauri/target/${{ matrix.rust_target }}/release/qwen-code-desktop
executable='src-tauri/target/${{ matrix.rust_target }}/release/qwen-code-desktop'
# A timed-out wait must fail here: `[ -S ... ] && break` is exempt
# from `set -e`, so falling through would blame the app instead.
wait_for_socket() {
for _ in $(seq 1 50); do
[ -S "$1" ] && return 0
sleep 0.2
done
echo "::error::$2 never created $1 within 10s" >&2
return 1
}
# WebKitGTK needs a real display: GTK3 has no headless backend.
# Fedora keeps Xvfb; RHEL 10 removed it, so there the display is a
# headless weston compositor with Xwayland on top.
if command -v xvfb-run >/dev/null 2>&1; then
xvfb-run -a npm run smoke:packaged -- "$executable"
else
command -v weston >/dev/null 2>&1 || {
echo '::error::weston is not installed'; exit 1;
}
command -v Xwayland >/dev/null 2>&1 || {
echo '::error::Xwayland is not installed'; exit 1;
}
export XDG_RUNTIME_DIR="$RUNNER_TEMP/xdg-runtime"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
# Xwayland reads its compositor from its own environment, so the
# socket name has to be exported before it is launched.
export WAYLAND_DISPLAY=qwen-smoke
weston --backend=headless-backend.so --socket=qwen-smoke --idle-time=0 &
weston_pid=$!
trap 'kill ${xwayland_pid:-} $weston_pid 2>/dev/null || true' EXIT
wait_for_socket "$XDG_RUNTIME_DIR/qwen-smoke" weston
Xwayland :33 -rootless -wm -1 &
xwayland_pid=$!
wait_for_socket /tmp/.X11-unix/X33 Xwayland
export DISPLAY=:33
# GTK3 prefers Wayland when both are exported, which would leave
# the X11 bridge above unused; pin the app to that display.
export GDK_BACKEND=x11
npm run smoke:packaged -- "$executable"
fi
- name: 'Collect artifacts'
shell: 'bash'
+133
View File
@@ -105,6 +105,139 @@ describe('desktop rpm build-root dependencies', () => {
source.indexOf('sha256sum -c -'),
);
});
it('keeps the smoke runnable on rpm roots without Xvfb', () => {
const install = release.jobs.build.steps.find(
(step) => step.name === 'Install rpm build-root dependencies',
);
// The display server a leg can install is a matrix property: EPEL, which
// supplies weston, is only enabled on the leg that has no Xvfb.
const displays = Object.fromEntries(
release.jobs.build.strategy.matrix.include.map((leg) => [
leg.name,
leg.display,
]),
);
expect(displays['Linux x64 (rpm, Fedora 44)']).toBe('xvfb');
expect(displays['Linux x64 (rpm, RHEL 10)']).toBe('weston');
// Fedora 44 ships weston from its own updates repo, so "weston ⇒ EPEL" is
// not the rule; the rule is that a non-Fedora root taking the weston arm
// has the EPEL bootstrap above enabled. Read the coupling from the matrix
// so a new leg with `display: 'weston'` and `epel: 'false'` turns this red
// instead of dying at `dnf install` in a release run.
const containerLegs = release.jobs.build.strategy.matrix.include.filter(
(leg) => leg.container,
);
expect(containerLegs.length).toBeGreaterThan(0);
for (const leg of containerLegs) {
expect(['xvfb', 'weston']).toContain(leg.display);
if (
leg.display !== 'xvfb' &&
!String(leg.artifact_suffix).includes('fedora')
) {
expect(leg.epel).toBe('true');
}
}
// Bound the shared package list by the arm switch that follows it, not by
// a prose comment: rewording the comment must not move the bound.
const switchAt = install.run.indexOf("case '${{ matrix.display }}' in");
expect(switchAt).toBeGreaterThan(-1);
const sharedInstalls = install.run
.slice(0, switchAt)
.split('\n')
.filter((line) => !line.trim().startsWith('#'))
.join('\n');
expect(sharedInstalls).not.toContain('Xvfb');
// Each arm is bounded by its own pattern and terminating `;;`, so another
// `case`/`if` above cannot re-point the slice.
const installArm = (pattern) => {
const start = install.run.indexOf(`\n ${pattern})`, switchAt);
expect(start).toBeGreaterThan(switchAt);
const end = install.run.indexOf(';;', start);
expect(end).toBeGreaterThan(start);
return install.run.slice(start, end);
};
const xvfbArm = installArm('xvfb');
const westonArm = installArm('weston');
expect(xvfbArm).toContain('xorg-x11-server-Xvfb');
expect(xvfbArm).not.toContain('weston');
expect(westonArm).toContain('weston xorg-x11-server-Xwayland');
expect(westonArm).not.toContain('xorg-x11-server-Xvfb');
// An unrecognised `matrix.display` must fail closed: defaulting it to the
// weston arm installs packages the root may have no repository for.
const fallbackArm = installArm('*');
expect(fallbackArm).toContain('exit 1');
const smoke = release.jobs.build.steps.find(
(step) =>
step.name === 'Smoke packaged application' &&
String(step.if).includes("runner.os == 'Linux'"),
);
expect(smoke.run).toContain('command -v xvfb-run');
expect(smoke.run).not.toContain('GDK_BACKEND=dummy');
// Both arms must run the same smoke under the step's own -e -u -o pipefail;
// a function shipped through `declare -f` into a child shell drops them.
expect(smoke.run).not.toContain('declare -f');
expect(
smoke.run.match(/npm run smoke:packaged -- "\$executable"/gu),
).toHaveLength(2);
// The compositor socket the step exports has to be the one weston opens,
// and the export has to precede the launch: Xwayland reads its own
// environment at startup.
const socket = /--socket=(\S+)/u.exec(smoke.run)?.[1];
expect(socket).toBeTruthy();
// Without this flag weston finds no DRM device in the container and the
// leg dies in a release run, where nothing else reads this step.
expect(smoke.run).toContain(
`weston --backend=headless-backend.so --socket=${socket}`,
);
expect(smoke.run).toContain('XDG_RUNTIME_DIR');
const waylandExport = `export WAYLAND_DISPLAY=${socket}`;
expect(smoke.run).toContain(waylandExport);
expect(smoke.run.indexOf(waylandExport)).toBeLessThan(
smoke.run.indexOf('Xwayland :'),
);
// Xwayland must not start before its compositor exists, so the weston
// readiness wait has to sit between the two launches.
const westonWait = `wait_for_socket "$XDG_RUNTIME_DIR/${socket}" weston`;
expect(smoke.run).toContain(westonWait);
expect(smoke.run.indexOf(westonWait)).toBeGreaterThan(
smoke.run.indexOf('weston_pid=$!'),
);
expect(smoke.run.indexOf(westonWait)).toBeLessThan(
smoke.run.indexOf('Xwayland :'),
);
// Both daemons have to be reaped, or the runner hangs on the sockets.
expect(smoke.run).toContain("trap 'kill ${xwayland_pid:-} $weston_pid");
// A timed-out wait must fail the step: `[ -S ... ] && break` is exempt
// from `set -e`, so a helper that only prints would blame the app.
const helper = /wait_for_socket\(\) \{([\s\S]*?)\n\}/u.exec(smoke.run);
expect(helper).not.toBeNull();
expect(helper[1]).toContain('return 1');
// The display Xwayland serves, the socket the step waits on and the
// DISPLAY the app receives must all name the same number.
const display = /Xwayland :(\d+)/u.exec(smoke.run)?.[1];
expect(display).toBeTruthy();
expect(smoke.run).toContain(`wait_for_socket /tmp/.X11-unix/X${display}`);
const displayExport = `export DISPLAY=:${display}`;
expect(smoke.run).toContain(displayExport);
expect(smoke.run.indexOf(displayExport)).toBeGreaterThan(
smoke.run.indexOf('Xwayland :'),
);
// With both displays exported GTK3 prefers Wayland, which would leave the
// X11 bridge above unused; the app has to be pinned to it.
expect(smoke.run.indexOf('export GDK_BACKEND=x11')).toBeGreaterThan(
smoke.run.indexOf('Xwayland :'),
);
});
});
describe('desktop isolation guard — nativePrefixes coverage', () => {