From 4a86b65b29947bd15596f5ce459e40ed4fbba8dc Mon Sep 17 00:00:00 2001 From: evmcod <214792911+EVMCOD@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:44:06 +0100 Subject: [PATCH 1/3] ci(linux): start the AppImage and the .deb, not only build them scripts/linux-smoke.sh starts a Linux bundle on a virtual display (Xvfb) with an empty HOME and no session bus, and checks that the local server answers, /api/platform says linux with the browser off, it is a first run with /welcome served, a ConductLoop window exists, and closing the app stops the server. It runs in the bundles job (ubuntu-22.04, after the uploads) and in a new linux-smoke job on ubuntu-latest from the uploaded artifacts; screenshots and logs are kept for 3 days. Publishes nothing. The changelog line of #76 (Windows CI) had landed in the published 1.2.3 section; it moves to Unreleased. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/linux-bundles.yml | 66 +++++++++- CHANGELOG.md | 6 +- scripts/linux-smoke.sh | 175 +++++++++++++++++++++++++++ tests/test_linux_bundles_workflow.py | 78 ++++++++++-- 4 files changed, 312 insertions(+), 13 deletions(-) create mode 100755 scripts/linux-smoke.sh diff --git a/.github/workflows/linux-bundles.yml b/.github/workflows/linux-bundles.yml index da845c1..b777725 100644 --- a/.github/workflows/linux-bundles.yml +++ b/.github/workflows/linux-bundles.yml @@ -2,7 +2,10 @@ name: Linux bundles # Builds the Linux AppImage and .deb of the desktop app with # scripts/build-linux.sh and keeps them as workflow artifacts for 3 days, so a -# Linux user (or a reviewer) can try a build of a pull request. It publishes +# Linux user (or a reviewer) can try a build of a pull request. Then +# scripts/linux-smoke.sh starts both on a virtual display with an empty HOME, +# on the build image (22.04) and on ubuntu-latest, and checks that the app comes +# up as a new user would see it; screenshots and logs are kept for 3 days too. It publishes # nothing: no release, no tag, no secret, no signing (there is no signing and no # updater on Linux). Runs only on GitHub-hosted runners (this repo is public); # never a self-hosted one (tests/test_workflows_guard.py). @@ -17,6 +20,7 @@ on: paths: - "desktop/**" - "scripts/build-linux.sh" + - "scripts/linux-smoke.sh" - ".github/workflows/linux-bundles.yml" permissions: @@ -37,7 +41,8 @@ jobs: run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \ - libayatana-appindicator3-dev librsvg2-dev patchelf + libayatana-appindicator3-dev librsvg2-dev patchelf \ + xvfb x11-utils imagemagick # The sidecar is frozen with Python 3.14, the version the macOS release uses. - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 @@ -87,3 +92,60 @@ jobs: path: desktop/src-tauri/target/release/bundle/deb/*.deb if-no-files-found: error retention-days: 3 + + # After the uploads, so the bundles are there even when a check fails. + - name: Smoke-test the AppImage + run: OUT=smoke scripts/linux-smoke.sh appimage desktop/src-tauri/target/release/bundle/appimage/*.AppImage + + - name: Smoke-test the .deb + if: ${{ !cancelled() }} + run: OUT=smoke scripts/linux-smoke.sh deb desktop/src-tauri/target/release/bundle/deb/*.deb + + - name: Upload the smoke-test screenshots and logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: conductloop-linux-smoke-ubuntu-22.04 + path: smoke/ + if-no-files-found: warn + retention-days: 3 + + # The same bundles on the newest image: they are built on 22.04 for an older + # glibc, so check that they also start (and the .deb installs) on a newer one. + linux-smoke: + needs: linux-bundles + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Display and screenshot tools + run: | + sudo apt-get update + sudo apt-get install -y xvfb x11-utils imagemagick + + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: conductloop-linux-appimage + path: bundles/appimage + + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: conductloop-linux-deb + path: bundles/deb + + - name: Smoke-test the AppImage + run: OUT=smoke scripts/linux-smoke.sh appimage bundles/appimage/*.AppImage + + - name: Smoke-test the .deb + if: ${{ !cancelled() }} + run: OUT=smoke scripts/linux-smoke.sh deb bundles/deb/*.deb + + - name: Upload the smoke-test screenshots and logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: conductloop-linux-smoke-ubuntu-latest + path: smoke/ + if-no-files-found: warn + retention-days: 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index b2726a7..0d38326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ User-facing changes to ConductLoop (the desktop app and its local server), newest first. +## Unreleased + +- CI only: the Linux bundles workflow now **starts the AppImage and the .deb** it builds, on Ubuntu 22.04 (the build image) and on ubuntu-latest, on a virtual display with an empty HOME and no keyring. `scripts/linux-smoke.sh` checks that the local server answers, that `/api/platform` says Linux with the in-app browser off, that it is a first run with the welcome page, that a ConductLoop window is on screen, and that closing the app stops the server; screenshots and logs are kept for 3 days. Until now the Linux packages were only built, never run. The script also works by hand on a Linux machine. +- CI only: a "Windows (compile and tests)" workflow checks on every relevant pull request that the desktop wrapper still compiles for Windows and that the Windows-safe part of the Python tests passes there. It builds no installer, signs nothing, publishes nothing and is not a required check. Windows is still not supported. + ## 1.2.3 — 2026-10-29 — A home for your notes and a Brain that helps without a key The first version that installs from the app: in 1.2.2, "Check for Updates…" @@ -10,7 +15,6 @@ changes is the first part of the list below (the notes home, the Brain without a key, Setup); the Linux and Windows items are groundwork for those systems and leave the Mac app as it was. -- CI only: a "Windows (compile and tests)" workflow checks on every relevant pull request that the desktop wrapper still compiles for Windows and that the Windows-safe part of the Python tests passes there. It builds no installer, signs nothing, publishes nothing and is not a required check. Windows is still not supported. - **A home for your notes.** Without projects (a fresh install has none), the dashboard opened on an empty project canvas and search hid behind ⌘P. It now opens on "Your notes": how many are indexed, a box to ask a question (it goes diff --git a/scripts/linux-smoke.sh b/scripts/linux-smoke.sh new file mode 100755 index 0000000..b9e81c5 --- /dev/null +++ b/scripts/linux-smoke.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Smoke-test a Linux build of the desktop app: start it on a display with an +# empty HOME, and check that it comes up the way a new user would see it. +# +# scripts/linux-smoke.sh deb path/to/ConductLoop__amd64.deb +# scripts/linux-smoke.sh appimage path/to/ConductLoop__amd64.AppImage +# +# Checks, in order (each prints ✓ or ✗; the exit status is 1 if any failed): +# 1. the local server answers /api/health on 127.0.0.1:7777 within $WAIT s; +# 2. /api/platform says Linux, with the in-app browser off; +# 3. it is a first run: /api/onboarding/state is fresh and /welcome is served; +# 4. a ConductLoop window is on the display (needs xwininfo; skipped without); +# 5. closing the app stops its server (the port is free within 15 s). +# A screenshot of the display after start-up goes to $OUT/linux-smoke-.png +# (needs ImageMagick's `import`) and the app's output to linux-smoke-.log. +# +# The .deb is installed with `sudo apt-get install`. The AppImage runs with +# --appimage-extract-and-run, so it needs no FUSE. Without $DISPLAY the script +# starts Xvfb on :99. Everything the app writes goes to a temporary HOME (data, +# config, logs); there is no D-Bus session, so no keyring either: the app must +# say so and keep working. Publishes nothing. +# +# Needs: Linux x86_64, curl, python3, Xvfb when there is no display; optional: +# xwininfo (x11-utils) and import (imagemagick). +set -euo pipefail + +MODE="${1:-}" +PKG="${2:-}" +OUT="${OUT:-$PWD}" +WAIT="${WAIT:-120}" +BASE="http://127.0.0.1:7777" + +usage() { sed -n '2,/^set -euo/p' "${BASH_SOURCE[0]}" | sed '$d' | sed 's/^# \{0,1\}//'; } +case "$MODE" in + deb|appimage) ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; +esac +if [[ -z "$PKG" || ! -f "$PKG" ]]; then + echo "✗ no package at '$PKG'" >&2 + exit 2 +fi +if [[ "$(uname -s)" != "Linux" ]]; then + echo "✗ linux-smoke.sh must run on Linux (this is $(uname -s))" >&2 + exit 2 +fi +PKG="$(realpath "$PKG")" +mkdir -p "$OUT" +LOG="$OUT/linux-smoke-$MODE.log" +SHOT="$OUT/linux-smoke-$MODE.png" +failures=0 +report() { echo "$1"; [[ -n "${GITHUB_STEP_SUMMARY:-}" ]] && echo "- $1" >> "$GITHUB_STEP_SUMMARY"; return 0; } +ok() { report "✓ $1"; } +bad() { report "✗ $1"; failures=$((failures + 1)); } + +if curl -s -m 2 -o /dev/null "$BASE/api/health"; then + echo "✗ something already answers on $BASE: stop it first" >&2 + exit 2 +fi + +case "$MODE" in + deb) + # The log belongs to the user running the script, not to root. + # shellcheck disable=SC2024 + sudo apt-get install -y "$PKG" > "$OUT/linux-smoke-apt.log" 2>&1 || { + tail -20 "$OUT/linux-smoke-apt.log"; echo "✗ apt could not install $PKG" >&2; exit 1; } + BIN="$(dpkg-deb -c "$PKG" | awk '{print $NF}' | sed 's#^\./#/#' | grep '/conductloop-desktop$' | head -1)" + ARGS=() + ;; + appimage) + chmod +x "$PKG" + BIN="$PKG" + ARGS=(--appimage-extract-and-run) + ;; +esac +[[ -n "$BIN" && -x "$BIN" ]] || { echo "✗ no conductloop-desktop binary in $PKG" >&2; exit 1; } +# shellcheck source=/dev/null +[[ -n "${GITHUB_STEP_SUMMARY:-}" ]] && echo "### Linux smoke test: $MODE ($(. /etc/os-release && echo "$PRETTY_NAME"))" >> "$GITHUB_STEP_SUMMARY" + +TMP="$(mktemp -d)" +XVFB_PID="" +APP_PID="" +cleanup() { + [[ -n "$APP_PID" ]] && kill "$APP_PID" 2>/dev/null || true + pkill -f conductloop-desktop 2>/dev/null || true + pkill -f conductloop-server 2>/dev/null || true + [[ -n "$XVFB_PID" ]] && kill "$XVFB_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap cleanup EXIT + +if [[ -z "${DISPLAY:-}" ]]; then + Xvfb :99 -screen 0 1280x800x24 -nolisten tcp > "$OUT/linux-smoke-xvfb.log" 2>&1 & + XVFB_PID=$! + export DISPLAY=:99 + sleep 2 +fi + +# A new user: empty HOME, no session bus (so no Secret Service), nothing set. +# WEBKIT_DISABLE_DMABUF_RENDERER: Xvfb has no GPU device for WebKitGTK's DMA-BUF +# renderer, which otherwise draws an empty window there. +mkdir -p "$TMP/home" +started=$(date +%s) +env -i PATH="/usr/local/bin:/usr/bin:/bin" HOME="$TMP/home" DISPLAY="$DISPLAY" \ + LANG=C.UTF-8 NO_AT_BRIDGE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 \ + TMPDIR="$TMP" "$BIN" "${ARGS[@]}" > "$LOG" 2>&1 & +APP_PID=$! + +# 1. The server comes up. +up="" +for _ in $(seq 1 "$WAIT"); do + if curl -s -m 2 -o /dev/null -w '%{http_code}' "$BASE/api/health" | grep -q '^200$'; then up=1; break; fi + kill -0 "$APP_PID" 2>/dev/null || break + sleep 1 +done +if [[ -n "$up" ]]; then + version="$(curl -s "$BASE/api/health" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version"))')" + ok "server answered in $(( $(date +%s) - started )) s as $version" +else + bad "the server did not answer within $WAIT s (app $(kill -0 "$APP_PID" 2>/dev/null && echo running || echo exited))" + tail -40 "$LOG" || true + exit 1 +fi + +# 2. It knows it is on Linux. +if curl -s "$BASE/api/platform" | python3 -c ' +import json, sys +p = json.load(sys.stdin) +for k in ("browser", "code_execution", "keychain"): + v = p.get(k) or {} + print((" %s: available=%s %s %s" % (k, v.get("available"), v.get("reason") or "", v.get("message") or "")).rstrip()) +sys.exit(0 if p.get("platform") == "linux" and (p.get("browser") or {}).get("available") is False else 1) +'; then + ok "/api/platform: linux, in-app browser off" +else + bad "/api/platform does not say linux with the browser off" +fi + +# 3. A first run. +if curl -s "$BASE/api/onboarding/state" | python3 -c 'import json,sys; sys.exit(0 if json.load(sys.stdin).get("fresh") is True else 1)' \ + && curl -s "$BASE/welcome" | grep -q "ConductLoop"; then + ok "first run: onboarding is fresh and /welcome is served" +else + bad "first run: onboarding not fresh or /welcome missing" +fi + +# 4. A window, and what it shows. +sleep 8 +if command -v xwininfo >/dev/null; then + if xwininfo -root -tree | grep -q '"ConductLoop"'; then ok "a ConductLoop window is on the display"; else bad "no ConductLoop window on the display"; fi +fi +if command -v import >/dev/null; then + import -window root "$SHOT" 2>/dev/null && echo " screenshot: $SHOT" || echo " (no screenshot)" +fi + +# 5. Closing the app stops its server. +pkill -TERM -f conductloop-desktop 2>/dev/null || true +kill "$APP_PID" 2>/dev/null || true +freed="" +for i in $(seq 1 15); do + if ! curl -s -m 1 -o /dev/null "$BASE/api/health"; then freed=$i; break; fi + sleep 1 +done +if [[ -n "$freed" ]] && ! pgrep -f conductloop-server >/dev/null; then + ok "closing the app stopped its server (${freed} s)" +else + bad "the server kept running after the app was closed" +fi +APP_PID="" + +if (( failures )); then + echo "--- last lines of the app's output" + tail -40 "$LOG" || true + exit 1 +fi diff --git a/tests/test_linux_bundles_workflow.py b/tests/test_linux_bundles_workflow.py index 615dff8..7498ccf 100644 --- a/tests/test_linux_bundles_workflow.py +++ b/tests/test_linux_bundles_workflow.py @@ -1,8 +1,9 @@ """The Linux bundles workflow (.github/workflows/linux-bundles.yml). -It builds the AppImage and .deb and uploads them as short-lived artifacts. It -must not publish anything, take a secret, or run anywhere but a GitHub-hosted -runner. (tests/test_workflows_guard.py checks pinning, permissions and runner +It builds the AppImage and .deb, uploads them as short-lived artifacts and +smoke-tests both (scripts/linux-smoke.sh) on the build image and on +ubuntu-latest. It must not publish anything, take a secret, or run anywhere but +a GitHub-hosted runner. (tests/test_workflows_guard.py checks pinning, permissions and runner labels for every workflow; these are the checks specific to this one.) """ from __future__ import annotations @@ -18,11 +19,13 @@ TEXT = PATH.read_text(encoding="utf-8") DOC = yaml.safe_load(TEXT) JOB = DOC["jobs"]["linux-bundles"] +SMOKE_JOB = DOC["jobs"]["linux-smoke"] +SMOKE = ROOT / "scripts" / "linux-smoke.sh" TRIGGERS = DOC.get(True) or DOC.get("on") # PyYAML reads the key `on` as True def test_it_runs_on_ubuntu_22_04_for_an_older_glibc_and_nowhere_else(): - assert list(DOC["jobs"]) == ["linux-bundles"] + assert list(DOC["jobs"]) == ["linux-bundles", "linux-smoke"] assert JOB["runs-on"] == "ubuntu-22.04" # older glibc: the AppImage runs on older distributions assert JOB["timeout-minutes"] <= 90 @@ -30,7 +33,7 @@ def test_it_runs_on_ubuntu_22_04_for_an_older_glibc_and_nowhere_else(): def test_triggers_are_manual_and_pull_requests_that_touch_the_desktop_app_or_the_script(): assert set(TRIGGERS) == {"workflow_dispatch", "pull_request"} paths = TRIGGERS["pull_request"]["paths"] - assert "desktop/**" in paths and "scripts/build-linux.sh" in paths + assert "desktop/**" in paths and "scripts/build-linux.sh" in paths and "scripts/linux-smoke.sh" in paths # Not on push: bundles of main are not wanted on every merge. assert "push" not in TRIGGERS and "schedule" not in TRIGGERS @@ -41,14 +44,69 @@ def test_it_builds_with_the_script_and_installs_webkitgtk_4_1(): assert "libwebkit2gtk-4.1-dev" in steps and "libgtk-3-dev" in steps +def _uploads(job): + return [s for s in job["steps"] if str(s.get("uses", "")).startswith("actions/upload-artifact@")] + + def test_both_bundles_are_uploaded_for_three_days(): - uploads = [s for s in JOB["steps"] if str(s.get("uses", "")).startswith("actions/upload-artifact@")] - assert len(uploads) == 2 - for step in uploads: - assert step["with"]["retention-days"] == 3 + bundles = [s for s in _uploads(JOB) if s["with"]["name"] in ("conductloop-linux-appimage", "conductloop-linux-deb")] + assert len(bundles) == 2 + for step in bundles: assert step["with"]["if-no-files-found"] == "error" - paths = " ".join(s["with"]["path"] for s in uploads) + paths = " ".join(s["with"]["path"] for s in bundles) assert ".AppImage" in paths and ".deb" in paths + for step in _uploads(JOB) + _uploads(SMOKE_JOB): + assert step["with"]["retention-days"] == 3 + + +def _runs(job): + return [str(s.get("run", "")) for s in job["steps"]] + + +def test_both_bundles_are_smoke_tested_on_the_build_image_after_they_are_uploaded(): + names = [s.get("name", "") for s in JOB["steps"]] + last_upload = max(i for i, s in enumerate(JOB["steps"]) + if s.get("with", {}).get("name") in ("conductloop-linux-appimage", "conductloop-linux-deb")) + smoke = [i for i, r in enumerate(_runs(JOB)) if "scripts/linux-smoke.sh" in r] + assert len(smoke) == 2 and min(smoke) > last_upload, names + runs = "\n".join(_runs(JOB)) + assert "linux-smoke.sh appimage" in runs and "linux-smoke.sh deb" in runs + assert "xvfb" in runs and "x11-utils" in runs and "imagemagick" in runs + + +def test_and_again_on_ubuntu_latest_from_the_uploaded_artifacts(): + assert SMOKE_JOB["needs"] == "linux-bundles" + assert SMOKE_JOB["runs-on"] == "ubuntu-latest" + assert SMOKE_JOB["timeout-minutes"] <= 30 + downloads = [s["with"]["name"] for s in SMOKE_JOB["steps"] + if str(s.get("uses", "")).startswith("actions/download-artifact@")] + assert downloads == ["conductloop-linux-appimage", "conductloop-linux-deb"] + runs = "\n".join(_runs(SMOKE_JOB)) + assert "linux-smoke.sh appimage bundles/appimage/" in runs and "linux-smoke.sh deb bundles/deb/" in runs + + +def test_a_failed_check_still_runs_the_other_and_keeps_the_screenshots(): + for job in (JOB, SMOKE_JOB): + steps = job["steps"] + deb = next(s for s in steps if "linux-smoke.sh deb" in str(s.get("run", ""))) + assert deb.get("if") == "${{ !cancelled() }}" + shots = [s for s in _uploads(job) if s["with"]["name"].startswith("conductloop-linux-smoke-")] + assert len(shots) == 1 and shots[0].get("if") == "${{ !cancelled() }}" + + +def test_the_smoke_script_only_reads_the_local_server_and_keeps_to_a_temporary_home(): + code = "\n".join(re.sub(r"(^|\s)#.*$", "", line) for line in SMOKE.read_text(encoding="utf-8").splitlines()) + hosts = set(re.findall(r"https?://([^/\s\"']+)", code)) + assert hosts == {"127.0.0.1:7777"}, hosts + for word in ("-X ", "--data", "-d ", "gh ", "git "): + assert word not in code, word + assert 'HOME="$TMP/home"' in code and "env -i" in code + assert "--appimage-extract-and-run" in code # no FUSE needed + + +@pytest.mark.posix_perms +def test_the_smoke_script_exists_and_is_executable(): + assert SMOKE.is_file() and SMOKE.stat().st_mode & 0o111 def test_it_publishes_nothing_and_takes_no_secret(): From 18b5dd3c3c038c3b02a0c0db8344f996c056b586 Mon Sep 17 00:00:00 2001 From: evmcod <214792911+EVMCOD@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:52:11 +0100 Subject: [PATCH 2/3] ci(linux): find the .deb binary and wait for the server to exit First run on CI: the AppImage came up (server in 2 s, linux, browser off, first run, window on screen). Two script bugs: dpkg-deb lists paths as "usr/bin/x" (no "./"), so the binary path came out relative; and the port closed a moment before the server process exited, which counted as "kept running". Wait up to 15 s for both, and show the process if one is left. Co-Authored-By: Claude Opus 5.5 --- scripts/linux-smoke.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/linux-smoke.sh b/scripts/linux-smoke.sh index b9e81c5..f8bb0d4 100755 --- a/scripts/linux-smoke.sh +++ b/scripts/linux-smoke.sh @@ -64,7 +64,8 @@ case "$MODE" in # shellcheck disable=SC2024 sudo apt-get install -y "$PKG" > "$OUT/linux-smoke-apt.log" 2>&1 || { tail -20 "$OUT/linux-smoke-apt.log"; echo "✗ apt could not install $PKG" >&2; exit 1; } - BIN="$(dpkg-deb -c "$PKG" | awk '{print $NF}' | sed 's#^\./#/#' | grep '/conductloop-desktop$' | head -1)" + # dpkg-deb lists "usr/bin/x" or "./usr/bin/x": make it absolute either way. + BIN="$(dpkg-deb -c "$PKG" | awk '{print $NF}' | sed -E 's#^\.?/?#/#' | grep '/conductloop-desktop$' | head -1)" ARGS=() ;; appimage) @@ -156,15 +157,18 @@ fi # 5. Closing the app stops its server. pkill -TERM -f conductloop-desktop 2>/dev/null || true kill "$APP_PID" 2>/dev/null || true +# The port closes before the process has exited: wait for both. freed="" for i in $(seq 1 15); do - if ! curl -s -m 1 -o /dev/null "$BASE/api/health"; then freed=$i; break; fi + if ! curl -s -m 1 -o /dev/null "$BASE/api/health" && ! pgrep -f conductloop-server >/dev/null; then freed=$i; break; fi sleep 1 done -if [[ -n "$freed" ]] && ! pgrep -f conductloop-server >/dev/null; then +if [[ -n "$freed" ]]; then ok "closing the app stopped its server (${freed} s)" else - bad "the server kept running after the app was closed" + bad "the server kept running 15 s after the app was closed" + ps -o pid,ppid,stat,etime,args -p "$(pgrep -d, -f conductloop-server)" 2>/dev/null || true + curl -s -m 1 -o /dev/null -w ' port 7777: HTTP %{http_code}\n' "$BASE/api/health" || echo " port 7777: closed" fi APP_PID="" From 031723b775fb2c004d2af0054a7879689e974b49 Mon Sep 17 00:00:00 2001 From: evmcod <214792911+EVMCOD@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:28:18 +0100 Subject: [PATCH 3/3] ci(linux): give the ubuntu-latest smoke job a desktop's graphics libraries The AppImage did not start on ubuntu-latest: "libEGL.so.1: cannot open shared object file". AppImages take libEGL/libGL from the system (they are on the AppImage exclude list) and every desktop has them; the runner image is a server without them. The .deb passed there (apt pulls them in), and on 22.04 they came with the build dependencies. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/linux-bundles.yml | 8 ++++++-- tests/test_linux_bundles_workflow.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux-bundles.yml b/.github/workflows/linux-bundles.yml index b777725..db5d8d9 100644 --- a/.github/workflows/linux-bundles.yml +++ b/.github/workflows/linux-bundles.yml @@ -119,10 +119,14 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Display and screenshot tools + # Plus the graphics libraries every Linux desktop has: an AppImage takes + # libEGL/libGL from the system (they are on AppImage's exclude list), and + # this runner image is a server without them ("libEGL.so.1: cannot open + # shared object file" otherwise). The .deb gets them from its dependencies. + - name: Display tools and a desktop's graphics libraries run: | sudo apt-get update - sudo apt-get install -y xvfb x11-utils imagemagick + sudo apt-get install -y xvfb x11-utils imagemagick libegl1 libgl1 libgles2 libgbm1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: diff --git a/tests/test_linux_bundles_workflow.py b/tests/test_linux_bundles_workflow.py index 7498ccf..ac1bf69 100644 --- a/tests/test_linux_bundles_workflow.py +++ b/tests/test_linux_bundles_workflow.py @@ -83,6 +83,8 @@ def test_and_again_on_ubuntu_latest_from_the_uploaded_artifacts(): assert downloads == ["conductloop-linux-appimage", "conductloop-linux-deb"] runs = "\n".join(_runs(SMOKE_JOB)) assert "linux-smoke.sh appimage bundles/appimage/" in runs and "linux-smoke.sh deb bundles/deb/" in runs + # A desktop has libEGL/libGL; the AppImage relies on them like every AppImage. + assert "libegl1" in runs and "libgl1" in runs def test_a_failed_check_still_runs_the_other_and_keeps_the_screenshots():