name: Linux Canary # Produces unsigned Linux .deb and .AppImage packages from main without # creating a tag, GitHub Release, or auto-updater artifact. Artifacts are # available as a short-lived GitHub Actions artifact for explicit testing. # # Design notes vs. signed-macos-canary.yml: # - fix-appimage.sh is run without signing env vars; the script detects # their absence and skips re-signing, repacking only (documented inline). # - Build tools match release.yml; cache keys derive the concrete linker and # native library identity rather than assuming the moving runner image. # - pnpm store restore/save pattern mirrors ci.yml:149-196. on: workflow_dispatch: permissions: contents: read jobs: build: name: Build Linux canary if: github.repository == 'block/buzz' runs-on: ubuntu-latest container: ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 timeout-minutes: 60 permissions: contents: read env: # AppImage tools are themselves AppImages; containers lack FUSE so we # must use the extract-and-run fallback. APPIMAGE_EXTRACT_AND_RUN: "1" defaults: run: shell: bash steps: - name: Require main env: SOURCE_REF: ${{ github.ref }} run: | if [[ "$SOURCE_REF" != "refs/heads/main" ]]; then echo "::error::Canary builds must run from main; got $SOURCE_REF" exit 1 fi - name: Install system dependencies env: DEBIAN_FRONTEND: noninteractive run: | apt-get update \ -o Acquire::Retries=3 \ -o Acquire::http::Timeout=30 \ -o Acquire::https::Timeout=30 apt-get install -y --no-install-recommends \ -o Acquire::Retries=3 \ -o Acquire::http::Timeout=30 \ -o Acquire::https::Timeout=30 \ -o DPkg::Lock::Timeout=120 \ build-essential \ ca-certificates \ curl \ desktop-file-utils \ file \ git \ libasound2-dev \ libayatana-appindicator3-dev \ libgtk-3-dev \ librsvg2-dev \ libssl-dev \ libwebkit2gtk-4.1-dev \ libxdo-dev \ patchelf \ pkg-config \ squashfs-tools \ wget \ xdg-utils - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Mark workspace safe for git (containerized job) run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Install appimagetool run: | case "$(uname -m)" in x86_64) ARCH_SUFFIX="x86_64" ;; aarch64) ARCH_SUFFIX="aarch64" ;; *) echo "::error::Unsupported architecture: $(uname -m)" exit 1 ;; esac wget -q --tries=3 --timeout=30 -O /tmp/appimagetool \ "https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${ARCH_SUFFIX}.AppImage" if [[ "$ARCH_SUFFIX" == "x86_64" ]]; then echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 /tmp/appimagetool" | sha256sum -c else echo "::error::No pinned SHA256 for appimagetool-${ARCH_SUFFIX} — add it before enabling this architecture" exit 1 fi install -m 755 /tmp/appimagetool /usr/local/bin/appimagetool wget -q --tries=3 --timeout=30 -O /tmp/appimage-runtime \ "https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-${ARCH_SUFFIX}" echo "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d /tmp/appimage-runtime" | sha256sum -c install -D -m 644 /tmp/appimage-runtime /usr/local/lib/appimage-runtime echo "APPIMAGETOOL_RUNTIME_FILE=/usr/local/lib/appimage-runtime" >> "$GITHUB_ENV" - name: Get pnpm store directory id: pnpm-cache run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - name: Restore pnpm store cache uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: pnpm-${{ runner.os }}- - name: Install desktop dependencies run: just desktop-install-ci - name: Derive canary version id: version run: | set -euo pipefail BASE_VERSION=$(node -p "require('./desktop/package.json').version") if ! [[ "$BASE_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?$ ]]; then echo "::error::Desktop version '$BASE_VERSION' is not semver" exit 1 fi VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))-test.${GITHUB_RUN_NUMBER}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Building canary version $VERSION from $GITHUB_SHA" - name: Patch canary version env: VERSION: ${{ steps.version.outputs.version }} run: | cd desktop && node scripts/set-version-from-tag.mjs "$VERSION" cd src-tauri && cargo update --workspace - name: Resolve native toolchain identity id: native_toolchain run: echo "id=$(scripts/desktop-native-toolchain-id.sh linux)" >> "$GITHUB_OUTPUT" # Compute this after cargo update so the key describes the graph that is # actually compiled. The helper normalizes only Buzz Desktop's release # version, allowing a canary to warm an otherwise identical tag build. - name: Compute exact release cache key id: rust_cache_key env: NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }} run: | KEY=$(scripts/desktop-release-cache-key.py \ --platform "$RUNNER_OS" \ --target x86_64-unknown-linux-gnu \ --features mesh-llm \ --native-inputs "$NATIVE_TOOLCHAIN_ID") echo "key=$KEY" >> "$GITHUB_OUTPUT" echo "Release cache key: $KEY" - name: Restore exact release Cargo cache id: rust_cache uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target !desktop/src-tauri/target/**/release/bundle key: ${{ steps.rust_cache_key.outputs.key }} - name: Generate non-updating bundle config run: | cat > desktop/src-tauri/tauri.canary.conf.json <<'JSON' { "bundle": { "createUpdaterArtifacts": false } } JSON - name: Build sidecars run: | cargo build --release -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli ./scripts/bundle-sidecars.sh - name: Build Linux Tauri app run: cd desktop && pnpm tauri build --ci --bundles deb,appimage --features mesh-llm --config src-tauri/tauri.canary.conf.json env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" - name: Fix AppImage (remove infra libs, symlink system GStreamer) # fix-appimage.sh checks for TAURI_SIGNING_PRIVATE_KEY and skips # re-signing when absent, so no signing env vars are needed here. # Repacking still runs, removing the Mesa/GLib/GStreamer conflict libs. run: | mapfile -t APPIMAGES < <(find desktop/src-tauri/target/release/bundle/appimage -name '*.AppImage' -type f) if [[ ${#APPIMAGES[@]} -eq 0 ]]; then echo "::error::No AppImage found to post-process" exit 1 fi if [[ ${#APPIMAGES[@]} -gt 1 ]]; then echo "::error::Expected exactly one AppImage, found ${#APPIMAGES[@]}: ${APPIMAGES[*]}" exit 1 fi bash desktop/scripts/fix-appimage.sh "${APPIMAGES[0]}" - name: Measure release Cargo cache inputs if: always() run: du -sh ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target 2>/dev/null || true # Only this trusted, main-bound canary writes the cache. Excluding bundle # output prevents installers from entering it. - name: Save exact release Cargo cache if: steps.rust_cache.outputs.cache-hit != 'true' uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target !desktop/src-tauri/target/**/release/bundle key: ${{ steps.rust_cache_key.outputs.key }} - name: Save pnpm store cache uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - name: Locate Linux build artifacts id: artifacts run: | set -euo pipefail BUNDLE_DIR="desktop/src-tauri/target/release/bundle" DEB=$(find "$BUNDLE_DIR/deb" -name '*.deb' -type f | head -1) if [[ -z "$DEB" ]]; then echo "::error::No DEB found in $BUNDLE_DIR/deb" exit 1 fi echo "deb=$DEB" >> "$GITHUB_OUTPUT" APPIMAGE=$(find "$BUNDLE_DIR/appimage" -name '*.AppImage' -type f | head -1) if [[ -z "$APPIMAGE" ]]; then echo "::error::No AppImage found in $BUNDLE_DIR/appimage" exit 1 fi echo "appimage=$APPIMAGE" >> "$GITHUB_OUTPUT" - name: Upload Linux canary packages uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: buzz-linux-canary-${{ github.sha }} path: | ${{ steps.artifacts.outputs.deb }} ${{ steps.artifacts.outputs.appimage }} if-no-files-found: error retention-days: 7