diff --git a/.github/workflows/linux-appimage-quality.yml b/.github/workflows/linux-appimage-quality.yml new file mode 100644 index 0000000..13c688b --- /dev/null +++ b/.github/workflows/linux-appimage-quality.yml @@ -0,0 +1,159 @@ +name: Linux AppImage Quality + +on: + pull_request: + branches: [main] + paths: + - ".github/workflows/linux-appimage-quality.yml" + - ".github/workflows/main.yml" + - "package.json" + - "package-lock.json" + - "scripts/validate-linux-appimage.sh" + - "src-tauri/Cargo.toml" + - "src-tauri/Cargo.lock" + - "src-tauri/tauri.conf.json" + - "src-tauri/icons/**" + push: + branches: [main] + paths: + - ".github/workflows/linux-appimage-quality.yml" + - ".github/workflows/main.yml" + - "package.json" + - "package-lock.json" + - "scripts/validate-linux-appimage.sh" + - "src-tauri/Cargo.toml" + - "src-tauri/Cargo.lock" + - "src-tauri/tauri.conf.json" + - "src-tauri/icons/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: appimage-quality-${{ github.ref }} + cancel-in-progress: true + +jobs: + appimage-quality: + name: Build, lint, and launch AppImage + runs-on: ubuntu-22.04 + timeout-minutes: 45 + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + desktop-file-utils \ + libayatana-appindicator3-dev \ + libfile-mimeinfo-perl \ + libfuse2 \ + libgtk-3-dev \ + libpulse-dev \ + librsvg2-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libx11-dev \ + libxcb1-dev \ + libxrandr-dev \ + patchelf \ + xdotool \ + xvfb + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "20" + cache: npm + + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: stable + targets: x86_64-unknown-linux-gnu + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: "./src-tauri -> target" + + - run: npm ci + + - name: Download verified FFmpeg sidecar + shell: bash + run: | + set -euo pipefail + ffmpeg_url="https://johnvansickle.com/ffmpeg/releases/ffmpeg-7.0.2-amd64-static.tar.xz" + ffmpeg_sha256="abda8d77ce8309141f83ab8edf0596834087c52467f6badf376a6a2a4c87cf67" + + curl --fail --location --show-error --silent \ + "$ffmpeg_url" \ + --output ffmpeg.tar.xz + printf '%s %s\n' "$ffmpeg_sha256" ffmpeg.tar.xz | sha256sum --check --strict - + + mkdir -p ffmpeg_tmp src-tauri/binaries + tar -xf ffmpeg.tar.xz -C ffmpeg_tmp + ffmpeg_bin="$(find ffmpeg_tmp -maxdepth 3 -name ffmpeg -type f -print -quit)" + test -n "$ffmpeg_bin" + install -m 755 "$ffmpeg_bin" src-tauri/binaries/ffmpeg-x86_64-unknown-linux-gnu + + - name: Build real AppImage + run: npm run tauri -- build --bundles appimage --target x86_64-unknown-linux-gnu + + - name: Lint and verify relocatable AppImage metadata + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + appimages=(src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage) + test "${#appimages[@]}" -eq 1 + bash scripts/validate-linux-appimage.sh "${appimages[0]}" + printf '%s\n' "${appimages[0]}" > "$RUNNER_TEMP/appimage-path" + + - name: Launch AppImage under X11 + shell: bash + run: | + set -euo pipefail + appimage="$(cat "$RUNNER_TEMP/appimage-path")" + log="$RUNNER_TEMP/flowtake-appimage.log" + + Xvfb :99 -screen 0 1280x800x24 >"$RUNNER_TEMP/xvfb.log" 2>&1 & + xvfb_pid=$! + app_pid="" + export DISPLAY=:99 + cleanup() { + if [[ -n "$app_pid" ]]; then + kill "$app_pid" 2>/dev/null || true + fi + kill "$xvfb_pid" 2>/dev/null || true + } + trap cleanup EXIT + + "$appimage" >"$log" 2>&1 & + app_pid=$! + + window_found=false + for _ in {1..30}; do + if ! kill -0 "$app_pid" 2>/dev/null; then + wait "$app_pid" || true + cat "$log" + echo "AppImage exited before showing a Flowtake window." >&2 + exit 1 + fi + if xdotool search --name Flowtake >/dev/null 2>&1; then + window_found=true + break + fi + sleep 1 + done + + if [[ "$window_found" != true ]]; then + cat "$log" + echo "AppImage did not expose a Flowtake window within 30 seconds." >&2 + exit 1 + fi + + kill "$app_pid" + wait "$app_pid" || true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a938781..c92774e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -298,6 +298,8 @@ jobs: libxcb1-dev \ libxrandr-dev \ libpulse-dev \ + desktop-file-utils \ + libfile-mimeinfo-perl \ rpm \ patchelf @@ -355,6 +357,18 @@ jobs: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + - name: Validate AppImage metadata + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + appimages=(src-tauri/target/release/bundle/appimage/*.AppImage) + if (( ${#appimages[@]} != 1 )); then + echo "ERROR: expected exactly one Linux AppImage, found ${#appimages[@]}" >&2 + exit 1 + fi + bash scripts/validate-linux-appimage.sh "${appimages[0]}" + - name: Collect Linux installers and portable binary run: | set -euo pipefail diff --git a/package-lock.json b/package-lock.json index e022145..2558a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "@eslint/js": "9.39.5", "@tailwindcss/postcss": "^4.3.0", "@tanstack/eslint-plugin-query": "^5.101.2", - "@tauri-apps/cli": "^2.11.2", + "@tauri-apps/cli": "^2.11.4", "@vitejs/plugin-react": "^6.0.2", "autoprefixer": "^10.5.0", "daisyui": "^5.5.23", @@ -2631,9 +2631,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", - "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -2647,23 +2647,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.2", - "@tauri-apps/cli-darwin-x64": "2.11.2", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", - "@tauri-apps/cli-linux-arm64-musl": "2.11.2", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", - "@tauri-apps/cli-linux-x64-gnu": "2.11.2", - "@tauri-apps/cli-linux-x64-musl": "2.11.2", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", - "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", - "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", "cpu": [ "arm64" ], @@ -2678,9 +2678,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", - "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", "cpu": [ "x64" ], @@ -2695,9 +2695,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", - "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", "cpu": [ "arm" ], @@ -2712,9 +2712,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", - "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", "cpu": [ "arm64" ], @@ -2729,9 +2729,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", - "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", "cpu": [ "arm64" ], @@ -2746,9 +2746,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", - "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", "cpu": [ "riscv64" ], @@ -2763,9 +2763,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", - "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", "cpu": [ "x64" ], @@ -2780,9 +2780,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", - "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", "cpu": [ "x64" ], @@ -2797,9 +2797,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", - "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", "cpu": [ "arm64" ], @@ -2814,9 +2814,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", - "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", "cpu": [ "ia32" ], @@ -2831,9 +2831,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", - "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index e52d3cc..edb20cb 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@eslint/js": "9.39.5", "@tailwindcss/postcss": "^4.3.0", "@tanstack/eslint-plugin-query": "^5.101.2", - "@tauri-apps/cli": "^2.11.2", + "@tauri-apps/cli": "^2.11.4", "@vitejs/plugin-react": "^6.0.2", "autoprefixer": "^10.5.0", "daisyui": "^5.5.23", diff --git a/scripts/validate-linux-appimage.sh b/scripts/validate-linux-appimage.sh new file mode 100755 index 0000000..016f01d --- /dev/null +++ b/scripts/validate-linux-appimage.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -euo pipefail + +APPDIR_LINT_COMMIT="5da36fb54d5847ed76d2b9959d96722cbc857923" +APPDIR_LINT_SHA256="408c60f34269b74ace4a87f074e9f215e9009772af57af4afc6be95de6790091" +EXCLUDELIST_SHA256="8893d840861b37c23992310ddd50f30d2ad9401e0d50b7b56593bec9e1c2b93d" + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 64 +fi + +appimage="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +if [[ ! -s "$appimage" ]]; then + echo "AppImage is missing or empty: $appimage" >&2 + exit 1 +fi + +work_dir="$(mktemp -d)" +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT + +tool_dir="$work_dir/appdir-lint" +extract_dir="$work_dir/extract" +mkdir -p "$tool_dir" "$extract_dir" + +curl --fail --location --show-error --silent \ + "https://raw.githubusercontent.com/AppImageCommunity/pkg2appimage/${APPDIR_LINT_COMMIT}/appdir-lint.sh" \ + --output "$tool_dir/appdir-lint.sh" +curl --fail --location --show-error --silent \ + "https://raw.githubusercontent.com/AppImageCommunity/pkg2appimage/${APPDIR_LINT_COMMIT}/excludelist" \ + --output "$tool_dir/excludelist" + +printf '%s %s\n' "$APPDIR_LINT_SHA256" "$tool_dir/appdir-lint.sh" \ + | sha256sum --check --strict - +printf '%s %s\n' "$EXCLUDELIST_SHA256" "$tool_dir/excludelist" \ + | sha256sum --check --strict - + +( + cd "$extract_dir" + chmod +x "$appimage" + "$appimage" --appimage-extract >/dev/null +) + +app_dir="$extract_dir/squashfs-root" +bash "$tool_dir/appdir-lint.sh" "$app_dir" + +validate_relocatable_path() { + local path="$1" + local label="$2" + + if [[ ! -e "$path" ]]; then + echo "$label is missing or resolves outside the AppDir: $path" >&2 + exit 1 + fi + + if [[ -L "$path" ]]; then + local target + target="$(readlink "$path")" + if [[ "$target" = /* ]]; then + echo "$label must use a relative symlink target, found: $target" >&2 + exit 1 + fi + fi +} + +validate_relocatable_path "$app_dir/.DirIcon" ".DirIcon" + +mapfile -t desktop_entries < <( + find "$app_dir" -maxdepth 1 \( -type l -o -type f \) -name '*.desktop' +) +if [[ ${#desktop_entries[@]} -ne 1 ]]; then + echo "Expected exactly one root desktop entry, found ${#desktop_entries[@]}" >&2 + exit 1 +fi +validate_relocatable_path "${desktop_entries[0]}" "Root desktop entry" + +echo "AppImage metadata is lint-clean and relocatable." diff --git a/test/appImagePackaging.test.js b/test/appImagePackaging.test.js new file mode 100644 index 0000000..1b8091a --- /dev/null +++ b/test/appImagePackaging.test.js @@ -0,0 +1,65 @@ +import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" +import test from "node:test" +import yaml from "js-yaml" + +const readRepoFile = path => readFile(new URL(`../${path}`, import.meta.url), "utf8") + +const [packageJson, packageLock, qualityWorkflowSource, releaseWorkflow, validationScript] = + await Promise.all([ + readRepoFile("package.json").then(JSON.parse), + readRepoFile("package-lock.json").then(JSON.parse), + readRepoFile(".github/workflows/linux-appimage-quality.yml"), + readRepoFile(".github/workflows/main.yml"), + readRepoFile("scripts/validate-linux-appimage.sh"), + ]) + +const qualityWorkflow = yaml.load(qualityWorkflowSource) + +test("AppImage packaging uses the Tauri release with relocatable metadata", () => { + assert.equal(packageJson.devDependencies["@tauri-apps/cli"], "^2.11.4") + assert.equal(packageLock.packages["node_modules/@tauri-apps/cli"].version, "2.11.4") + assert.equal( + packageLock.packages["node_modules/@tauri-apps/cli-linux-x64-gnu"].version, + "2.11.4" + ) +}) + +test("AppImage validation pins its external linter and rejects absolute metadata links", () => { + assert.match(validationScript, /APPDIR_LINT_COMMIT="5da36fb54d5847ed76d2b9959d96722cbc857923"/) + assert.match(validationScript, /APPDIR_LINT_SHA256="[A-Fa-f0-9]{64}"/) + assert.match(validationScript, /EXCLUDELIST_SHA256="[A-Fa-f0-9]{64}"/) + assert.match(validationScript, /sha256sum --check --strict/) + assert.match(validationScript, /--appimage-extract/) + assert.match(validationScript, /bash "\$tool_dir\/appdir-lint\.sh" "\$app_dir"/) + assert.match(validationScript, /validate_relocatable_path "\$app_dir\/\.DirIcon"/) + assert.match(validationScript, /if \[\[ "\$target" = \/\* \]\]/) +}) + +test("Linux packaging CI builds, lints, and launches the real AppImage", () => { + const job = qualityWorkflow.jobs["appimage-quality"] + assert.equal(job["runs-on"], "ubuntu-22.04") + assert.equal(job["timeout-minutes"], 45) + + const commands = job.steps.map(step => step.run ?? "").join("\n") + assert.match(commands, /npm run tauri -- build --bundles appimage/) + assert.match(commands, /bash scripts\/validate-linux-appimage\.sh/) + assert.match(commands, /Xvfb :99/) + assert.match(commands, /xdotool search --name Flowtake/) + assert.match(commands, /AppImage exited before showing a Flowtake window/) +}) + +test("release publication rejects an AppImage that fails metadata validation", () => { + const buildStart = releaseWorkflow.indexOf(" build-linux:") + const buildEnd = releaseWorkflow.indexOf(" build-macos:") + const linuxJob = releaseWorkflow.slice(buildStart, buildEnd) + + assert.match(linuxJob, /desktop-file-utils/) + assert.match(linuxJob, /- name: Validate AppImage metadata/) + assert.match(linuxJob, /bash scripts\/validate-linux-appimage\.sh/) + assert.ok( + linuxJob.indexOf("- name: Validate AppImage metadata") < + linuxJob.indexOf("- name: Collect Linux installers"), + "AppImage validation must run before artifacts are collected" + ) +})