# Tauri desktop (Linux) build — SEPARATE from ci.yml on purpose: this is a heavy # Rust + AppImage build (~20-40 min) that should NOT run on backend/frontend-only # pushes. Scoped to desktop/** (+ this file). Produces the .deb and .AppImage. # # Toolchain comes from the ci-tauri image (Rust + Node + WebKitGTK 4.1 + tauri-cli); # runs-on is just a registered scheduling label (Label Model B), not a per-purpose # runner. The frontend is built here because tauri's generate_context! embeds it. name: Desktop (Tauri) on: push: branches: [dev, main] tags: ["v*"] paths: - "desktop/**" # The desktop app embeds the frontend, and the data seam / Tauri bridge are # what the offline core rides on — rebuild the app when those change too. - "frontend/src/adapters/**" - "frontend/src/desktop/**" - ".forgejo/workflows/desktop.yml" workflow_dispatch: concurrency: group: desktop-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} permissions: # write (not read) so the tag build can publish a Release with the bundles # attached (the "Publish release" step). Read is enough for dev/main builds, # but the token scope is per-workflow, so it's set once here. contents: write jobs: build: name: Tauri desktop (Linux) if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-tauri:1.97 env: # AppImage tooling (linuxdeploy) FUSE-mounts itself by default; CI containers # have no /dev/fuse, so tell it to extract-and-run instead. Without this the # AppImage bundle step fails with a FUSE error. APPIMAGE_EXTRACT_AND_RUN: "1" steps: - uses: actions/checkout@v6 # tauri's generate_context! embeds the built frontend at compile time, so the # frontend must exist before any cargo compile (clippy/test/build), not just # at bundle time. - name: Build the shared frontend run: npm ci && npm run build working-directory: frontend - name: Rust format check run: cargo fmt --check working-directory: desktop/src-tauri - name: Clippy run: cargo clippy --all-targets -- -D warnings working-directory: desktop/src-tauri - name: Test run: cargo test working-directory: desktop/src-tauri # Frontend already built above; skip the beforeBuildCommand rebuild. - name: Tauri build (deb + AppImage) run: cargo tauri build --config '{"build":{"beforeBuildCommand":""}}' working-directory: desktop/src-tauri # Tauri's AppImage bundles the build host's graphics/display libs # (libEGL/libGL/libdrm/libgbm/libwayland-*), which clash with end-user GPU # drivers and abort to a black window (EGL_BAD_PARAMETER, issue 2021). # Strip that host-coupled stack so the app uses the running system's # graphics libs; webkit/gtk stay bundled. Runs from the repo root (the # script resolves its own paths), overwriting the AppImage in place. - name: De-bundle AppImage graphics libraries run: bash desktop/packaging/appimage/debundle-graphics.sh # install.sh hands the .deb to every Debian/Ubuntu user, so the package's # Depends must be right BEFORE a release exists. Prints the generated # control file and cross-checks it against what the ELF actually needs # (dpkg-shlibdeps). # # We deliberately do NOT set bundle.linux.deb.depends: run 2872 showed # tauri already infers exactly libwebkit2gtk-4.1-0 + libgtk-3-0, so # declaring them again only produced a control file listing each twice. # This step is the guard instead — if tauri's inference ever stops # covering what the binary links, the build fails here. - name: Verify the .deb run: bash desktop/packaging/deb/verify.sh # Repackage the binary just built into a native pacman package, so Arch / # CachyOS gets a real native install from install.sh instead of the AppImage # fallback — without a second Rust build or an Arch CI image. Safe because # nothing is bundled: the binary resolves webkit/gtk by soname, which is # identical across the two distros. Can't be pacman-tested here (Debian # runner), so the step logs .PKGINFO + the full file listing for audit. - name: Package for Arch (pacman) run: bash desktop/packaging/arch/package-prebuilt.sh # Make the built .deb + .AppImage downloadable from the run (for hand-testing). # continue-on-error: the Forgejo artifact backend may not be configured yet; a # failed upload must not fail the build itself. # Forgejo doesn't support the v4 artifact protocol (@actions/artifact v2+), # so pin v3, which uses the older protocol the instance accepts. - name: Upload bundles continue-on-error: true uses: actions/upload-artifact@v3 with: name: thoughtsync-linux path: | desktop/src-tauri/target/release/bundle/appimage/*.AppImage desktop/src-tauri/target/release/bundle/deb/*.deb desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* if-no-files-found: warn # Tag builds only: publish a real, versioned Fabled-Git Release with the # AppImage + .deb attached — the stable fetch target the install script and # the in-app updater consume (Actions artifacts above are ephemeral/test). # Cutting the tag is the operator's action (rule 2); this only publishes a # Release for a tag that already exists. Dormant on dev/main pushes. - name: Publish release if: startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ github.token }} run: bash desktop/packaging/publish-release.sh # Windows installer, CROSS-COMPILED from Linux — there is no Windows build host. # A Windows container can't run on a Linux host (containers share the host # kernel), so cross-compiling is the only route without Windows hardware: # cargo-xwin + LLVM's lld-link + makensis are Linux programs that emit Windows # PE output. That toolchain is why this needs its own image rather than ci-tauri. # # NSIS only. `.msi` needs WiX v3, which is a Windows program — Tauri: ".msi # installers can only be created on Windows". It returns if a Windows node does. # # A separate job, so a Windows-side failure never blocks the Linux artifacts that # are the primary product today. Tauri calls this path "not tested as much" and a # last resort, and nothing here can LAUNCH a Windows binary — green means it # built, not that it runs. A real-machine check stays mandatory before trusting it. windows: name: Windows installer (cross-compiled) if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-tauri-win:1.97 steps: - uses: actions/checkout@v6 # Same reason as the Linux job: generate_context! embeds the built frontend # at compile time, so it must exist before cargo runs. - name: Build the shared frontend run: npm ci && npm run build working-directory: frontend # --runner cargo-xwin swaps cargo for the cross-compiling driver (it supplies # the MSVC CRT/SDK, pre-warmed into the image, and links with lld-link). # Frontend already built above; skip the beforeBuildCommand rebuild. - name: Tauri build (NSIS installer) run: | cargo tauri build \ --runner cargo-xwin \ --target x86_64-pc-windows-msvc \ --bundles nsis \ --config '{"build":{"beforeBuildCommand":""}}' working-directory: desktop/src-tauri - name: Upload installer continue-on-error: true uses: actions/upload-artifact@v3 with: name: thoughtsync-windows path: desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe if-no-files-found: warn # Publishes to the SAME release as the Linux job. Safe to run twice: the # script reuses an existing release (409) and nullglob means each job uploads # only the bundles present in its own workspace. - name: Publish release if: startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ github.token }} run: bash desktop/packaging/publish-release.sh