mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
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:
1 parent
b5975157d1
commit
4593a3b8f2
2 files changed
+200
-2
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user