diff --git a/.github/workflows/_ci-desktop.yml b/.github/workflows/_ci-desktop.yml index 90eacc59f1..bcdb3b63d9 100644 --- a/.github/workflows/_ci-desktop.yml +++ b/.github/workflows/_ci-desktop.yml @@ -69,7 +69,7 @@ jobs: desktop-tauri: name: Desktop Tauri (${{ matrix.lane }}) runs-on: ubuntu-latest - # A lane needs up to ~15m. ci-apt-retry.sh bounds the apt step at ~34m worst + # A lane needs up to ~15m. ci-apt-retry.sh bounds the apt steps at ~34m worst # case (update ~8.5m + install ~25m); 55m fits both, so a degraded mirror # fails with the wrapper's error instead of the job timeout. timeout-minutes: 55 @@ -93,6 +93,22 @@ jobs: run: just desktop-tauri-test-compiled-flags permissions: contents: read + env: + # Resolved and installed by the steps below; the .deb cache key hashes + # what apt resolves for this list. + TAURI_APT_PACKAGES: >- + build-essential + curl + file + libasound2-dev + libayatana-appindicator3-dev + libgtk-3-dev + librsvg2-dev + libssl-dev + libwebkit2gtk-4.1-dev + libxdo-dev + patchelf + wget steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -104,24 +120,70 @@ jobs: workspaces: desktop/src-tauri shared-key: ${{ matrix.cache }} save-if: ${{ matrix.save-cache && github.event_name != 'pull_request' }} + # A degraded Ubuntu mirror serves .debs at ~50 KB/s, and the ~62 MB + # Tauri download (one 25.5 MB libwebkit2gtk .deb) then outlasts every + # ci-apt-retry.sh attempt. Cache the .debs in the Actions cache so the + # mirror only serves `apt-get update` and packages Ubuntu has updated + # since main last saved. + - name: Resolve Tauri apt archives + id: tauri-debs + run: | + set -euo pipefail + CI_APT_ATTEMPT_SECONDS=90 scripts/ci-apt-retry.sh sudo apt-get update + read -ra packages <<< "$TAURI_APT_PACKAGES" + # Name, size and SHA256 (from the signed index) of each .deb the + # install will fetch. The mirror URI is left out so the key doesn't + # depend on which mirror answered. Runs before the restore: + # --print-uris omits files that are already in + # /var/cache/apt/archives. + sudo apt-get install --print-uris -qq -y --no-install-recommends \ + -o Acquire::ForceHash=SHA256 "${packages[@]}" | + awk '{print $2, $3, $4}' | sort > "$RUNNER_TEMP/tauri-debs.txt" + . /etc/os-release + prefix="tauri-debs-${ID}${VERSION_ID}-" + echo "prefix=$prefix" >> "$GITHUB_OUTPUT" + echo "key=$prefix$(sha256sum "$RUNNER_TEMP/tauri-debs.txt" | cut -c1-16)" >> "$GITHUB_OUTPUT" + - name: Restore Tauri apt archives + id: tauri-debs-cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ~/.cache/tauri-debs + key: ${{ steps.tauri-debs.outputs.key }} + restore-keys: ${{ steps.tauri-debs.outputs.prefix }} - name: Install Tauri dependencies (Linux) + id: tauri-debs-install env: DEBIAN_FRONTEND: noninteractive run: | - CI_APT_ATTEMPT_SECONDS=90 scripts/ci-apt-retry.sh sudo apt-get update - scripts/ci-apt-retry.sh sudo apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - file \ - libasound2-dev \ - libayatana-appindicator3-dev \ - libgtk-3-dev \ - librsvg2-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - patchelf \ - wget + set -euo pipefail + debs=~/.cache/tauri-debs + mkdir -p "$debs" + # apt reuses an archived .deb of the expected size without checking + # its hash, so seed only restored files whose SHA256 matches the + # signed index. apt downloads and verifies the rest. sha256sum exits + # non-zero when a file fails or none is restored; neither is fatal. + awk '{sub(/^SHA256:/, "", $3); print $3 " " $1}' "$RUNNER_TEMP/tauri-debs.txt" > "$RUNNER_TEMP/tauri-debs.sha256" + (cd "$debs" && { sha256sum --check --ignore-missing "$RUNNER_TEMP/tauri-debs.sha256" || true; }) | + sed -n 's/: OK$//p' | (cd "$debs" && xargs -r sudo cp -t /var/cache/apt/archives/) + read -ra packages <<< "$TAURI_APT_PACKAGES" + scripts/ci-apt-retry.sh sudo apt-get install -y --no-install-recommends "${packages[@]}" + # Keep exactly the resolved files, so a restore-keys fallback can't + # pile up superseded versions. The packages are installed by now, so + # a failure here only warns and skips the save. + if rm -rf "$debs" && mkdir -p "$debs" && + awk '{print "/var/cache/apt/archives/" $1}' "$RUNNER_TEMP/tauri-debs.txt" | xargs -r cp -t "$debs"; then + echo "rebuilt=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::Could not rebuild the Tauri apt archive cache; not saving it." + fi + # Like the other caches here, only pushes save; PR runs restore. All + # three lanes resolve the same key, so one lane saves it. + - name: Save Tauri apt archives + if: matrix.lane == 'core' && github.event_name == 'push' && steps.tauri-debs-cache.outputs.cache-hit != 'true' && steps.tauri-debs-install.outputs.rebuilt == 'true' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ~/.cache/tauri-debs + key: ${{ steps.tauri-debs.outputs.key }} - name: Desktop Tauri ${{ matrix.lane }} run: ${{ matrix.run }} env: diff --git a/.github/workflows/_ci-rust.yml b/.github/workflows/_ci-rust.yml index 3190d877ae..e536e13162 100644 --- a/.github/workflows/_ci-rust.yml +++ b/.github/workflows/_ci-rust.yml @@ -111,9 +111,12 @@ jobs: windows-rust: name: Windows Rust (x86_64-pc-windows-msvc) runs-on: windows-latest - # Windows runners are slow and this compiles the workspace + Tauri crate - # cold across four steps; budget generously. - timeout-minutes: 45 + # With no rust-cache (after a toolchain bump or eviction), the steps take + # 39-41m on main (2026-10-22..26): clippy 17-18m, Tauri check ~6m, Tauri + # tests 10-11m, the rest ~5m. The cache save that follows takes ~4.5m and + # runs inside this cap. If the cap kills it, nothing is saved and the next + # push is cold again. 60m leaves ~15m over that; warm runs take 15-23m. + timeout-minutes: 60 if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.rust || inputs.desktop_rust) permissions: contents: read @@ -144,10 +147,10 @@ jobs: for bin in buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz; do touch "desktop/src-tauri/binaries/${bin}-${TARGET}.exe" done + # Clippy type-checks everything `cargo check` would, so no separate + # workspace check step. - name: Clippy (workspace) run: cargo clippy --workspace --all-targets --target $env:TARGET -- -D warnings - - name: Check (workspace) - run: cargo check --workspace --all-targets --target $env:TARGET - name: Test (buzz-dev-mcp) # The Windows-only bash resolver lives in buzz-dev-mcp; its unit tests # only gate if this crate is tested ON Windows. diff --git a/scripts/flake-check.test.mjs b/scripts/flake-check.test.mjs index 79739d2b45..5688a91478 100644 --- a/scripts/flake-check.test.mjs +++ b/scripts/flake-check.test.mjs @@ -1079,14 +1079,20 @@ test("repeat invocations mirror the lanes they stand in for", () => { runner, ); // The Tauri lane builds what the Desktop Tauri core lane does: the same - // workspace cache, system packages and sidecar stubs. + // workspace cache, system packages and sidecar stubs. The core lane lists + // its packages in TAURI_APT_PACKAGES, which its .deb cache key hashes. const desktop = read("../.github/workflows/_ci-desktop.yml"); - const packages = (text) => - /apt-get install -y --no-install-recommends \\\n((?: {12}\S+(?: \\)?\n)+)/.exec( - text, - )?.[1]; - assert.ok(packages(desktop), "Desktop Tauri apt packages"); - assert.equal(packages(flake), packages(desktop)); + const desktopPackages = /^ {6}TAURI_APT_PACKAGES: >-\n((?: {8}\S+\n)+)/m + .exec(desktop)?.[1] + .split(/\s+/) + .filter(Boolean); + const flakePackages = + /apt-get install -y --no-install-recommends \\\n((?: {12}\S+(?: \\)?\n)+)/ + .exec(flake)?.[1] + .split(/[\s\\]+/) + .filter(Boolean); + assert.ok(desktopPackages?.length, "Desktop Tauri apt packages"); + assert.deepEqual(flakePackages, desktopPackages); assert.match( flake, /flake-tauri:\n(?: {4}.*\n)*? {6}- uses: Swatinem\/rust-cache@\w+\n {8}with:\n {10}workspaces: desktop\/src-tauri\n {10}shared-key: desktop-core\n {10}save-if: false\n/,