# Builds the Linux .deb once, repackages it as a Flatpak and as a Snap, and # starts each package headless to check that it runs inside its sandbox. # # The release workflows call it with `release-tag`: it then repackages the # amd64 .deb already attached to that release instead of building one, and # uploads the Flatpak and the Snap that passed their tests to the release. name: Linux Packages on: pull_request: paths: - "packaging/**" - "snap/**" - ".github/workflows/linux-packages.yml" - "src-tauri/tauri.conf.json" - "src-tauri/Cargo.toml" - "src-tauri/donutbrowser.desktop" push: branches: - main paths: - "packaging/**" - "snap/**" - ".github/workflows/linux-packages.yml" - "src-tauri/tauri.conf.json" - "src-tauri/Cargo.toml" - "src-tauri/donutbrowser.desktop" workflow_call: inputs: release-tag: description: "Release whose amd64 .deb is repackaged; the packages are uploaded to it" type: string required: true workflow_dispatch: inputs: check-wayfern: description: "Also download Wayfern (about 1 GB) and start it inside each sandbox" type: boolean default: false permissions: contents: read concurrency: group: linux-packages-${{ inputs.release-tag || github.event.pull_request.number || github.ref }} cancel-in-progress: true env: APP_ID: com.donutbrowser.DonutBrowser FLATPAK_MANIFEST: packaging/flatpak/com.donutbrowser.DonutBrowser.yml FLATPAK_LINT_EXCEPTIONS: packaging/flatpak/flatpak-builder-lint-exceptions.json METAINFO: packaging/linux/com.donutbrowser.DonutBrowser.metainfo.xml jobs: deb: name: Build .deb if: ${{ !inputs.release-tag }} runs-on: ubuntu-22.04 timeout-minutes: 90 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 - name: Setup pnpm uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 #v6.1.0 with: run_install: false - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0 with: node-version-file: .node-version cache: "pnpm" - name: Setup Rust uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 #master with: toolchain: stable - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev pkg-config unzip xdg-utils # `workspaces`: rust-cache v2 has no `workdir` input, and with it the # cache would look for Cargo.toml at the repository root and save nothing. - name: Rust cache uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 #v2.9.2 with: workspaces: "src-tauri -> target" save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install frontend dependencies run: pnpm install --frozen-lockfile # beforeBuildCommand builds the donut-proxy sidecar, downloads the # verified Xray-core sidecar and builds the frontend. - name: Build the .deb env: NEXT_PUBLIC_TURNSTILE: ${{ secrets.NEXT_PUBLIC_TURNSTILE }} run: | BUILD_TAG="nightly-$(git show -s --format=%cs HEAD)-${GITHUB_SHA::7}" export BUILD_TAG pnpm tauri build --bundles deb - name: Collect the .deb run: | mkdir -p linux-deb cp src-tauri/target/release/bundle/deb/*.deb linux-deb/ ls -l linux-deb test "$(find linux-deb -name '*.deb' | wc -l)" -eq 1 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: name: donut-deb path: linux-deb/*.deb if-no-files-found: error retention-days: 7 deb-from-release: name: Take the .deb from the release if: ${{ inputs.release-tag }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Download the amd64 .deb env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.release-tag }} run: | mkdir -p linux-deb gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern 'Donut_*_amd64.deb' --dir linux-deb ls -l linux-deb test "$(find linux-deb -name '*.deb' | wc -l)" -eq 1 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: name: donut-deb path: linux-deb/*.deb if-no-files-found: error retention-days: 7 flatpak: name: Build Flatpak needs: [deb, deb-from-release] # Exactly one of the two .deb jobs runs; the other is skipped. if: ${{ !cancelled() && (needs.deb.result == 'success' || needs.deb-from-release.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: 45 container: # flatpak, flatpak-builder, flatpak-builder-lint and appstreamcli, with # org.gnome.Platform//51 and org.gnome.Sdk//51 already installed. image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-51 options: --privileged steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 with: name: donut-deb path: linux-deb - name: Put the .deb where the manifest expects it run: mv linux-deb/*.deb packaging/donut.deb - name: Validate the AppStream metadata run: appstreamcli validate --no-net --explain "$METAINFO" - name: Lint the manifest run: flatpak-builder-lint --exceptions --user-exceptions "$FLATPAK_LINT_EXCEPTIONS" manifest "$FLATPAK_MANIFEST" - name: Restore the flatpak-builder cache uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 with: path: .flatpak-builder key: flatpak-builder-${{ hashFiles('packaging/flatpak/**') }} restore-keys: flatpak-builder- # No --install-deps-from: the image already has the runtime and the SDK, # and updating a runtime in place fails on the container's overlay # filesystem ("Invalid cross-device link"). - name: Build the Flatpak run: | flatpak-builder --repo=repo --disable-rofiles-fuse --force-clean \ --default-branch=master build-dir "$FLATPAK_MANIFEST" - name: Lint the build run: flatpak-builder-lint --exceptions --user-exceptions "$FLATPAK_LINT_EXCEPTIONS" repo repo - name: Create the bundle run: | flatpak build-bundle --runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo \ repo Donut.flatpak "$APP_ID" master ls -l Donut.flatpak - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: name: donut-flatpak-x86_64 path: Donut.flatpak if-no-files-found: error flatpak-smoke: name: Test Flatpak needs: flatpak # Stated, not implied: without it the job inherits the skip of whichever # .deb job did not run, and a release would go out without the Flatpak. if: ${{ !cancelled() && needs.flatpak.result == 'success' }} runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 with: name: donut-flatpak-x86_64 path: flatpak-bundle - name: Install Flatpak and the test tools run: | sudo apt-get update sudo apt-get install -y flatpak xvfb imagemagick dbus # Ubuntu 24.04 can restrict unprivileged user namespaces, which # bubblewrap needs to build the sandbox. sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo - name: Prepare a user runtime directory run: | dir="/run/user/$(id -u)" sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "$dir" echo "XDG_RUNTIME_DIR=$dir" >> "$GITHUB_ENV" - name: Install the bundle run: | flatpak install --user -y --noninteractive flatpak-bundle/Donut.flatpak flatpak info --user "$APP_ID" - name: Check that every library resolves inside the sandbox run: | # shellcheck disable=SC2016 # expanded by the shell inside the sandbox dbus-run-session -- flatpak run --command=sh "$APP_ID" -c ' for f in /app/bin/donutbrowser /app/bin/donut-proxy /app/lib/libayatana-appindicator3.so.1; do echo "== $f" /lib64/ld-linux-x86-64.so.2 --list "$f" done' 2>&1 | tee libraries.txt if grep "not found" libraries.txt || ! grep -q "=>" libraries.txt; then echo "::error::A library does not resolve inside the Flatpak sandbox." exit 1 fi - name: Start the app on a virtual display run: bash packaging/linux/smoke-test.sh smoke 45 flatpak run "$APP_ID" - name: Stop the app and collect its logs if: always() run: | flatpak kill "$APP_ID" 2>/dev/null || true mkdir -p smoke cp -r "$HOME/.var/app/$APP_ID/data/com.donutbrowser/logs" smoke/app-logs 2>/dev/null || true cp libraries.txt smoke/ 2>/dev/null || true - name: Download Wayfern if: ${{ inputs.check-wayfern }} run: | url="$(curl -fsSL https://donutbrowser.com/wayfern.json | jq -r '.downloads["linux-x64"]')" curl -fsSL --retry 5 -o "$RUNNER_TEMP/wayfern.tar.xz" "$url" - name: Start Wayfern inside the sandbox if: ${{ inputs.check-wayfern }} run: bash packaging/linux/wayfern-check.sh flatpak "$RUNNER_TEMP/wayfern.tar.xz" smoke/wayfern - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 if: always() with: name: flatpak-smoke-test path: smoke if-no-files-found: ignore retention-days: 14 snap: name: Build and test Snap needs: [deb, deb-from-release] if: ${{ !cancelled() && (needs.deb.result == 'success' || needs.deb-from-release.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 with: name: donut-deb path: linux-deb - name: Put the .deb where snapcraft.yaml expects it run: mv linux-deb/*.deb packaging/donut.deb - name: Install Snapcraft run: sudo snap install snapcraft --classic # The runner is Ubuntu 24.04, the same system as the core24 base, so # the snap is built on the host rather than in an LXD instance. - name: Build the snap run: | sudo snapcraft pack --destructive-mode --verbosity=brief snap_file="$(ls donutbrowser_*.snap)" echo "SNAP_FILE=$snap_file" >> "$GITHUB_ENV" ls -l "$snap_file" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: name: donut-snap-amd64 path: ${{ env.SNAP_FILE }} if-no-files-found: error # `snap run` moves the app into its own systemd scope and asks the # user's systemd for it. The runner has no user session, so the app died # with "... is not a snap cgroup". Lingering starts systemd --user and, # with dbus-user-session, its bus at /run/user//bus. - name: Start a user session run: | sudo apt-get update sudo apt-get install -y xvfb imagemagick dbus dbus-user-session sudo loginctl enable-linger "$(id -un)" uid="$(id -u)" for _ in $(seq 1 60); do [ -S "/run/user/$uid/bus" ] && break sleep 1 done test -S "/run/user/$uid/bus" echo "XDG_RUNTIME_DIR=/run/user/$uid" >> "$GITHUB_ENV" echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus" >> "$GITHUB_ENV" - name: Install the snap run: | sudo snap install gnome-46-2404 mesa-2404 gtk-common-themes sudo snap install --dangerous "$SNAP_FILE" snap connections donutbrowser - name: Check that every library resolves inside the snap run: | # shellcheck disable=SC2016 # expanded by the shell inside the sandbox snap run --shell donutbrowser -c ' for f in "$SNAP/usr/bin/donutbrowser" "$SNAP/usr/bin/donut-proxy"; do echo "== $f" /lib64/ld-linux-x86-64.so.2 --list "$f" done' 2>&1 | tee libraries.txt if grep "not found" libraries.txt || ! grep -q "=>" libraries.txt; then echo "::error::A library does not resolve inside the snap." exit 1 fi - name: Start the app on a virtual display run: bash packaging/linux/smoke-test.sh smoke 45 snap run donutbrowser - name: Stop the app and collect its logs if: always() run: | pkill -f /snap/donutbrowser/ 2>/dev/null || true mkdir -p smoke cp -r "$HOME/snap/donutbrowser/current/.local/share/com.donutbrowser/logs" smoke/app-logs 2>/dev/null || true cp libraries.txt smoke/ 2>/dev/null || true sudo journalctl --no-pager -k --grep 'apparmor="DENIED"' 2>/dev/null | grep donutbrowser > smoke/apparmor-denials.txt || true echo "AppArmor denials: $(wc -l < smoke/apparmor-denials.txt)" - name: Download Wayfern if: ${{ inputs.check-wayfern }} run: | url="$(curl -fsSL https://donutbrowser.com/wayfern.json | jq -r '.downloads["linux-x64"]')" curl -fsSL --retry 5 -o "$RUNNER_TEMP/wayfern.tar.xz" "$url" - name: Start Wayfern inside the snap if: ${{ inputs.check-wayfern }} run: bash packaging/linux/wayfern-check.sh snap "$RUNNER_TEMP/wayfern.tar.xz" smoke/wayfern - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 if: always() with: name: snap-smoke-test path: smoke if-no-files-found: ignore retention-days: 14 publish: name: Attach the packages to the release needs: [flatpak-smoke, snap] # Each package goes up only when its own test passed, so a Snap failure # does not hold back a working Flatpak. if: ${{ !cancelled() && inputs.release-tag && (needs.flatpak-smoke.result == 'success' || needs.snap.result == 'success') }} runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 with: name: donut-deb path: linux-deb - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 if: ${{ needs.flatpak-smoke.result == 'success' }} with: name: donut-flatpak-x86_64 path: flatpak-bundle - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 if: ${{ needs.snap.result == 'success' }} with: name: donut-snap-amd64 path: snap-package # Named like the other assets (Donut__.), so the # nightly job renames them the same way it renames the rest. - name: Upload to the release env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.release-tag }} run: | deb="$(find linux-deb -name 'Donut_*_amd64.deb' | head -n 1)" version="$(basename "$deb" | sed -E 's/^Donut_(.+)_amd64\.deb$/\1/')" test -n "$version" files=() if [ -f flatpak-bundle/Donut.flatpak ]; then cp flatpak-bundle/Donut.flatpak "Donut_${version}_x86_64.flatpak" files+=("Donut_${version}_x86_64.flatpak") fi snap="$(find snap-package -name '*.snap' 2>/dev/null | head -n 1)" if [ -n "$snap" ]; then cp "$snap" "Donut_${version}_amd64.snap" files+=("Donut_${version}_amd64.snap") fi ls -l "${files[@]}" gh release upload "$TAG" "${files[@]}" --clobber --repo "$GITHUB_REPOSITORY"