From 7f54d18375c90dbe377a9ae071317312e676456e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Wed, 28 Oct 2026 04:55:03 +0000 Subject: [PATCH] CI on dev, the app started on X11 and Wayland; no silent re-release ci.yml runs on dev too, adds the hygiene check, check-ui.sh (the modules parsed as modules; nothing sets HTML) and the vendor pin, and a start job: a debug build with the bundled page, started under Xvfb and under a headless Weston with GTK's Wayland backend, which must still be running after 25 seconds. release.yml refuses a tag that already has a release unless run by hand with replace, marks betas as pre-releases, and gives a beta's .deb a ~beta version so it sorts below its release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 47 +++++++++++++++++++---- .github/workflows/release.yml | 48 +++++++++++++++++++---- scripts/check-ui.sh | 19 ++++++++++ scripts/ci-start.sh | 71 +++++++++++++++++++++++++++++++++++ scripts/hygiene.sh | 29 ++++++++++++++ 5 files changed, 200 insertions(+), 14 deletions(-) create mode 100755 scripts/check-ui.sh create mode 100755 scripts/ci-start.sh create mode 100755 scripts/hygiene.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d681a4..95aca3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,11 @@ -# Checks on every push to main and every pull request: formatting, lints, tests, and the page's -# JavaScript. The app is built for real only by release.yml, from a tag. +# Checks on every push to main and dev and every pull request: formatting, lints, tests, the +# page's scripts, the pins (tokens, CodeMirror), the public-repository hygiene -- and the app +# started for real, on X11 and on Wayland, to prove it gets as far as its window. The packages +# are built only by release.yml, from a tag. name: ci on: push: - branches: [main] + branches: [main, dev] pull_request: permissions: @@ -23,6 +25,8 @@ jobs: with: components: rustfmt, clippy cache-workspaces: src-tauri + - name: Nothing private in a public repository + run: scripts/hygiene.sh - name: The bundled LLeMbas CLI, verified run: scripts/fetch-cli.sh - name: Formatting @@ -34,8 +38,37 @@ jobs: - name: Tests working-directory: src-tauri run: cargo test --locked - - name: The pages' JavaScript, and the script the content view runs + - name: The page's scripts parse, and none sets HTML + run: scripts/check-ui.sh + - name: The design tokens and CodeMirror are the server's, as pinned run: | - for f in ui/*.js src-tauri/src/*.js; do node --check "$f"; done - - name: The design tokens are the server's, as pinned - run: scripts/fetch-tokens.sh + scripts/fetch-tokens.sh + scripts/fetch-vendor.sh + + # The app started for real: it must still be running after 25 seconds (timeout's 124), and its + # log must not say it stopped. Once under Xvfb (X11) and once under a headless Weston (Wayland, + # GTK's Wayland backend forced), each with a session bus (the tray and the notifications want + # one) and an empty home, so it starts at its first-run page. Built with the custom protocol on, + # as a release is, so the page is the bundled ui/ and not a development server. + start: + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: System libraries, and an X server and a Wayland compositor without a screen + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \ + xvfb weston dbus libgl1-mesa-dri + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache-workspaces: src-tauri + - name: The bundled LLeMbas CLI, verified + run: scripts/fetch-cli.sh + - name: Build (debug, with the bundled page) + working-directory: src-tauri + run: cargo build --locked --features tauri/custom-protocol + - name: Starts on X11 + run: scripts/ci-start.sh x11 + - name: Starts on Wayland + run: scripts/ci-start.sh wayland diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e404cfb..5df7d0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ -# A release, from a signed tag vX.Y.Z on main: the Linux packages (.deb and AppImage) built with -# the LLeMbas CLI inside, SHA256SUMS over them, and the GitHub Release with the tag message as its -# notes. LLeMbas Desktop is developed on Gitea, which mirrors its commits and tags here; the builds +# A release, from a signed tag vX.Y.Z (stable, main) or vX.Y.Z-beta.N (a beta, marked as a +# pre-release): the Linux packages (.deb and AppImage) built with the LLeMbas CLI inside, +# SHA256SUMS over them, and the GitHub Release with the tag message as its notes. LLeMbas Desktop is developed on Gitea, which mirrors its commits and tags here; the builds # happen here because they need a full Rust and WebKitGTK toolchain. name: release on: @@ -11,6 +11,10 @@ on: tag: description: "The tag, e.g. v0.1.0" required: true + replace: + description: "Replace a release that already exists for this tag (people who installed it get no update)" + type: boolean + default: false permissions: contents: write @@ -48,6 +52,26 @@ jobs: run: cargo install tauri-cli --version "^2" --locked - name: Build run: cargo tauri build --bundles deb,appimage + # Debian sorts 1.0.0-beta.1 *above* 1.0.0 (what follows the dash is a package revision), so a + # beta's package would make the release after it look like a downgrade, and apt would keep the + # beta. A beta's package version says ~beta, which sorts below. + - name: A beta's .deb sorts below its release + run: | + deb="$(ls src-tauri/target/release/bundle/deb/*.deb)" + have="$(dpkg-deb -f "$deb" Version)" + want="${VERSION/-/\~}" + if [ "$have" != "$want" ]; then + echo "repacking $deb: Version $have -> $want" + work="$(mktemp -d)" + dpkg-deb -R "$deb" "$work/pkg" + sed -i "s/^Version: .*/Version: $want/" "$work/pkg/DEBIAN/control" + dpkg-deb --root-owner-group -b "$work/pkg" "$deb" + fi + base="${VERSION%%-*}" + if [ "$VERSION" != "$base" ]; then + dpkg --compare-versions "$(dpkg-deb -f "$deb" Version)" lt "$base" \ + || { echo "the beta's package does not sort below $base" >&2; exit 1; } + fi - name: Collect the files and SHA256SUMS run: | mkdir dist @@ -62,8 +86,18 @@ jobs: GH_TOKEN: ${{ github.token }} run: | git tag -l --format='%(contents)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' > notes.md - # A tag moved to a fixed commit and pushed again replaces its release: - # the old one, and the files built from the old commit, go first. - gh release delete "$TAG" --repo "$GITHUB_REPOSITORY" --yes 2>/dev/null || true - gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \ + # A version is released once. Installing the same version again over a broken one is a + # no-op for apt ("already the newest version") and the updater offers nothing, so a fix + # is a new version -- never the old one re-released. Replacing is a deliberate act: a + # run by hand with `replace`. + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + if [ "${{ inputs.replace || false }}" != "true" ]; then + echo "A release for $TAG exists already. Release the fix as a new version." >&2 + exit 1 + fi + gh release delete "$TAG" --repo "$GITHUB_REPOSITORY" --yes + fi + pre=() + case "$VERSION" in *-*) pre=(--prerelease) ;; esac + gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag "${pre[@]}" \ --title "LLeMbas Desktop $VERSION" --notes-file notes.md dist/* diff --git a/scripts/check-ui.sh b/scripts/check-ui.sh new file mode 100755 index 0000000..7848ba7 --- /dev/null +++ b/scripts/check-ui.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# The page's JavaScript parses: the app's modules and the vendored ones as ES modules (a plain +# `node --check file.js` treats a .js file as CommonJS and passes a module with a syntax error in +# it), the generated i18n.js as the classic script it is. +set -euo pipefail +cd "$(dirname "$0")/.." + +bad=0 +for f in ui/js/*.js ui/vendor/*.mjs; do + node --input-type=module --check < "$f" || { echo "check-ui: $f does not parse" >&2; bad=1; } +done +node --check ui/i18n.js || { echo "check-ui: ui/i18n.js does not parse" >&2; bad=1; } +# Nothing the page draws is set as HTML: text goes in as text (ui/js/dom.js). +if grep -nE '\.(innerHTML|outerHTML)\s*=|insertAdjacentHTML|document\.write' ui/js/*.js; then + echo "check-ui: the lines above set HTML from a string" >&2 + bad=1 +fi +[ "$bad" -eq 0 ] && echo "check-ui: the page's scripts parse, and none sets HTML" +exit "$bad" diff --git a/scripts/ci-start.sh b/scripts/ci-start.sh new file mode 100755 index 0000000..9fa405a --- /dev/null +++ b/scripts/ci-start.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Start the built app on a display without a screen and check it is still running 25 seconds +# later, with nothing in its log saying it stopped. For CI (ci.yml, job `start`): +# +# scripts/ci-start.sh x11 under Xvfb +# scripts/ci-start.sh wayland under a headless Weston, GTK's Wayland backend forced +# +# The app starts with an empty home, so it shows its first-run page and talks to no server. +set -euo pipefail +cd "$(dirname "$0")/.." + +APP="${LLEMBAS_DESKTOP_BIN:-src-tauri/target/debug/llembas-desktop}" +[ -x "$APP" ] || { echo "ci-start: no app at $APP; build it first" >&2; exit 1; } +how="${1:?usage: ci-start.sh x11|wayland}" + +home="$(mktemp -d)" +runtime="$(mktemp -d)" +chmod 700 "$runtime" +pids=() +cleanup() { + for p in "${pids[@]}"; do kill "$p" 2>/dev/null || true; done + rm -rf "$home" "$runtime" +} +trap cleanup EXIT + +export HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_DATA_HOME="$home/.local/share" \ + XDG_STATE_HOME="$home/.local/state" XDG_CACHE_HOME="$home/.cache" XDG_RUNTIME_DIR="$runtime" +# No GPU on a runner: Mesa's software renderer. +export LIBGL_ALWAYS_SOFTWARE=1 +log="$XDG_DATA_HOME/eu.llembas.desktop/logs/desktop.log" + +case "$how" in + x11) + run=(xvfb-run -a -s "-screen 0 1280x840x24") + ;; + wayland) + weston --backend=headless-backend.so --socket=wayland-ci --idle-time=0 --use-pixman \ + > "$runtime/weston.log" 2>&1 & + pids+=("$!") + for _ in $(seq 1 50); do + [ -S "$runtime/wayland-ci" ] && break + sleep 0.2 + done + [ -S "$runtime/wayland-ci" ] || { echo "ci-start: weston did not start" >&2; cat "$runtime/weston.log" >&2; exit 1; } + export WAYLAND_DISPLAY=wayland-ci GDK_BACKEND=wayland + unset DISPLAY + run=() + ;; + *) + echo "ci-start: x11 or wayland, not $how" >&2 + exit 2 + ;; +esac + +code=0 +dbus-run-session -- "${run[@]}" timeout 25 "$APP" > "$runtime/app.log" 2>&1 || code=$? +echo "--- the app's output ($how)" +cat "$runtime/app.log" +if [ -f "$log" ]; then + echo "--- desktop.log" + cat "$log" +fi +if [ "$code" -ne 124 ]; then + echo "ci-start: on $how the app ended by itself within 25 seconds (exit $code)" >&2 + exit 1 +fi +if [ -f "$log" ] && grep -q 'stopped:\|could not start\|could not open the window' "$log"; then + echo "ci-start: on $how the app logged a failure" >&2 + exit 1 +fi +echo "ci-start: on $how the app was still running after 25 seconds" diff --git a/scripts/hygiene.sh b/scripts/hygiene.sh new file mode 100755 index 0000000..8a9d92d --- /dev/null +++ b/scripts/hygiene.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# The repository is public: nothing in it may name the private estate it was written on. +# The LLeMbas section rule ("everything public is current-state and estate-free"), as a check +# that runs in CI on every push rather than a sentence somebody has to remember. +# +# Patterns are the kinds of thing that leak: a private domain, an internal `.lan` name, a +# container number, a private address range, a machine's name. This file is the one place that +# may spell them, and is skipped. +set -euo pipefail +cd "$(dirname "$0")/.." + +patterns=( + 'houmeres' + '\b[a-z0-9-]+\.lan\b' + '\bCT ?2[0-9]{2}\b' + '\b10\.0\.0\.[0-9]+' + '\b192\.168\.[0-9]+\.[0-9]+' + '\bgamebox\b' + '\bthor\b' +) +found=0 +for pattern in "${patterns[@]}"; do + if git ls-files -z | grep -zv '^scripts/hygiene.sh$' | xargs -0 grep -nIEi -- "$pattern" ; then + echo "hygiene: the lines above match /$pattern/" >&2 + found=1 + fi +done +[ "$found" -eq 0 ] && echo "hygiene: nothing private in the tracked files" +exit "$found"