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 <noreply@anthropic.com>
This commit is contained in:
5 files changed
+200
-14
No files matched your search
@@ -1,9 +1,11 @@
|
|||||||
# Checks on every push to main and every pull request: formatting, lints, tests, and the page's
|
# Checks on every push to main and dev and every pull request: formatting, lints, tests, the
|
||||||
# JavaScript. The app is built for real only by release.yml, from a tag.
|
# 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
|
name: ci
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -23,6 +25,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
cache-workspaces: src-tauri
|
cache-workspaces: src-tauri
|
||||||
|
- name: Nothing private in a public repository
|
||||||
|
run: scripts/hygiene.sh
|
||||||
- name: The bundled LLeMbas CLI, verified
|
- name: The bundled LLeMbas CLI, verified
|
||||||
run: scripts/fetch-cli.sh
|
run: scripts/fetch-cli.sh
|
||||||
- name: Formatting
|
- name: Formatting
|
||||||
@@ -34,8 +38,37 @@ jobs:
|
|||||||
- name: Tests
|
- name: Tests
|
||||||
working-directory: src-tauri
|
working-directory: src-tauri
|
||||||
run: cargo test --locked
|
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: |
|
run: |
|
||||||
for f in ui/*.js src-tauri/src/*.js; do node --check "$f"; done
|
scripts/fetch-tokens.sh
|
||||||
- name: The design tokens are the server's, as pinned
|
scripts/fetch-vendor.sh
|
||||||
run: scripts/fetch-tokens.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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# A release, from a signed tag vX.Y.Z on main: the Linux packages (.deb and AppImage) built with
|
# A release, from a signed tag vX.Y.Z (stable, main) or vX.Y.Z-beta.N (a beta, marked as a
|
||||||
# the LLeMbas CLI inside, SHA256SUMS over them, and the GitHub Release with the tag message as its
|
# pre-release): the Linux packages (.deb and AppImage) built with the LLeMbas CLI inside,
|
||||||
# notes. LLeMbas Desktop is developed on Gitea, which mirrors its commits and tags here; the builds
|
# 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.
|
# happen here because they need a full Rust and WebKitGTK toolchain.
|
||||||
name: release
|
name: release
|
||||||
on:
|
on:
|
||||||
@@ -11,6 +11,10 @@ on:
|
|||||||
tag:
|
tag:
|
||||||
description: "The tag, e.g. v0.1.0"
|
description: "The tag, e.g. v0.1.0"
|
||||||
required: true
|
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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -48,6 +52,26 @@ jobs:
|
|||||||
run: cargo install tauri-cli --version "^2" --locked
|
run: cargo install tauri-cli --version "^2" --locked
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo tauri build --bundles deb,appimage
|
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
|
- name: Collect the files and SHA256SUMS
|
||||||
run: |
|
run: |
|
||||||
mkdir dist
|
mkdir dist
|
||||||
@@ -62,8 +86,18 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
git tag -l --format='%(contents)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' > notes.md
|
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:
|
# A version is released once. Installing the same version again over a broken one is a
|
||||||
# the old one, and the files built from the old commit, go first.
|
# no-op for apt ("already the newest version") and the updater offers nothing, so a fix
|
||||||
gh release delete "$TAG" --repo "$GITHUB_REPOSITORY" --yes 2>/dev/null || true
|
# is a new version -- never the old one re-released. Replacing is a deliberate act: a
|
||||||
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \
|
# 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/*
|
--title "LLeMbas Desktop $VERSION" --notes-file notes.md dist/*
|
||||||
Executable
+19
@@ -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"
|
||||||
Executable
+71
@@ -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"
|
||||||
Executable
+29
@@ -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"
|
||||||
Reference in new issue
Block a user