diff --git a/.github/workflows/_ci-clients.yml b/.github/workflows/_ci-clients.yml new file mode 100644 index 00000000000..af5861e5f13 --- /dev/null +++ b/.github/workflows/_ci-clients.yml @@ -0,0 +1,148 @@ +name: CI / Clients +on: + workflow_call: + inputs: + web: + required: true + type: boolean + mobile: + required: true + type: boolean + lane: + required: true + type: string + outputs: + web_result: + value: ${{ jobs.results.outputs.web_result }} + mobile_result: + value: ${{ jobs.results.outputs.mobile_result }} + +env: + CARGO_TERM_COLOR: always + BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + +jobs: + web: + name: Web + runs-on: ubuntu-latest + timeout-minutes: 15 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.web) + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 2 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - 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 dependencies + run: pnpm install --frozen-lockfile + - name: Web lint and format + run: just web-check + - name: Web build + run: just web-build + - name: Save pnpm store cache + if: github.event_name == 'push' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + + mobile: + name: Mobile + runs-on: ubuntu-latest + timeout-minutes: 30 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.mobile) + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 2 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Compute Hermit cache key + id: hermit-bin-hash + run: | + hash="$(find ./bin ! -type d | sort | xargs openssl sha256 | openssl sha256 -r | cut -d' ' -f1)" + echo "hash=$hash" >> "$GITHUB_OUTPUT" + - name: Restore Hermit package cache + id: hermit-cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ~/.cache/hermit/pkg + key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }} + restore-keys: ${{ runner.os }}-hermit-cache- + - name: Prime Flutter SDK + run: flutter --version + - name: Save Hermit package cache + if: always() && steps.hermit-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + continue-on-error: true + with: + path: ~/.cache/hermit/pkg + key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }} + - name: Restore pub cache + id: pub-cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ~/.pub-cache + key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }} + restore-keys: pub-${{ runner.os }}- + - name: Install dependencies + run: cd mobile && flutter pub get + - name: Save pub cache + if: always() && steps.pub-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + continue-on-error: true + with: + path: ~/.pub-cache + key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }} + - name: Format check + run: cd mobile && dart format --output=none --set-exit-if-changed . + - name: Analyze + run: cd mobile && flutter analyze + - name: Test + run: cd mobile && flutter test + - name: Build Android debug APK + run: just mobile-build-android + + mobile-swift: + name: Mobile Swift + runs-on: macos-latest + timeout-minutes: 30 + if: inputs.lane == 'mobile-swift' && inputs.mobile + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Install Flutter dependencies + run: cd mobile && flutter pub get + - name: Build + run: swift build --package-path mobile/ios/BuzzPushKit + - name: Build release + run: swift build -c release --package-path mobile/ios/BuzzPushKit + - name: Test + run: swift test --package-path mobile/ios/BuzzPushKit + - name: Build complete unsigned iOS release + run: cd mobile && flutter build ios --release --no-codesign --no-pub + + results: + name: Results + if: ${{ always() }} + needs: [web, mobile, mobile-swift] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + outputs: + web_result: ${{ needs.web.result }} + mobile_result: ${{ needs.mobile.result }} + steps: + - run: echo "Captured client job results" diff --git a/.github/workflows/_ci-desktop-macos.yml b/.github/workflows/_ci-desktop-macos.yml new file mode 100644 index 00000000000..728f17f3243 --- /dev/null +++ b/.github/workflows/_ci-desktop-macos.yml @@ -0,0 +1,110 @@ +name: CI / Desktop macOS +on: + workflow_call: + inputs: + rust: + required: true + type: boolean + desktop: + required: true + type: boolean + desktop_rust: + required: true + type: boolean + outputs: + desktop_macos_result: + value: ${{ jobs.results.outputs.desktop_macos_result }} + +env: + CARGO_TERM_COLOR: always + BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + +jobs: + desktop-build-macos: + name: Desktop Build (macOS) + runs-on: macos-latest + timeout-minutes: 45 + if: github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: desktop/src-tauri + save-if: ${{ github.event_name != 'pull_request' }} + - name: Install desktop dependencies + run: just desktop-install-ci + - name: Create sidecar placeholders + run: | + TARGET=$(rustc -vV | sed -n 's|host: ||p') + mkdir -p desktop/src-tauri/binaries + touch "desktop/src-tauri/binaries/buzz-acp-$TARGET" + touch "desktop/src-tauri/binaries/buzz-agent-$TARGET" + touch "desktop/src-tauri/binaries/buzz-backend-kubernetes-$TARGET" + touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET" + touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET" + touch "desktop/src-tauri/binaries/buzz-$TARGET" + # Mesh rev is derived from Cargo.lock so a dependency bump needs no + # lockstep edit here; the cache key tracks it automatically. + - name: Resolve mesh-llm rev + id: mesh_rev + run: | + set -euo pipefail + REV=$(python3 -c 'import tomllib; d=tomllib.load(open("Cargo.lock", "rb")); p=next(p for p in d["package"] if p["name"] == "mesh-llm-sdk"); print(p["source"].rsplit("#", 1)[1])') + [[ -n "$REV" ]] || { echo "::error::could not resolve mesh-llm rev from Cargo.lock"; exit 1; } + echo "rev=$REV" >> "$GITHUB_OUTPUT" + echo "short=${REV:0:7}" >> "$GITHUB_OUTPUT" + - name: Restore mesh llama build cache + id: llama_cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ github.workspace }}/.cache/mesh-llama + key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + - name: Build mesh llama native libraries + if: steps.llama_cache.outputs.cache-hit != 'true' + env: + MESH_REV_SHORT: ${{ steps.mesh_rev.outputs.short }} + run: | + set -euo pipefail + cargo fetch --manifest-path desktop/src-tauri/Cargo.toml + SHORT="$MESH_REV_SHORT" + MESH_ROOT=$(find "${CARGO_HOME:-$HOME/.cargo}/git/checkouts" -path "*/$SHORT" -type d -name "$SHORT" | head -1) + if [[ -z "$MESH_ROOT" ]]; then + echo "::error::mesh-llm checkout for $SHORT not found after cargo fetch" + exit 1 + fi + export LLAMA_STAGE_BACKEND=metal + export LLAMA_STAGE_BUILD_DIR="$GITHUB_WORKSPACE/.cache/mesh-llama/build-stage-abi-metal" + export CMAKE_OSX_DEPLOYMENT_TARGET=10.15 + "$MESH_ROOT/scripts/prepare-llama.sh" pinned + "$MESH_ROOT/scripts/build-llama.sh" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 + - name: Save mesh llama build cache + if: steps.llama_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ github.workspace }}/.cache/mesh-llama + key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} + - name: Build Tauri app + run: cd desktop && pnpm tauri build + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + MACOSX_DEPLOYMENT_TARGET: "10.15" + CMAKE_OSX_DEPLOYMENT_TARGET: "10.15" + LLAMA_STAGE_BACKEND: metal + LLAMA_STAGE_BUILD_DIR: ${{ github.workspace }}/.cache/mesh-llama/build-stage-abi-metal + SKIPPY_LLAMA_AUTO_BUILD: "0" + + results: + name: Results + if: ${{ always() }} + needs: [desktop-build-macos] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + outputs: + desktop_macos_result: ${{ needs.desktop-build-macos.result }} + steps: + - run: echo "Captured Desktop Build (macOS) result" diff --git a/.github/workflows/_ci-desktop.yml b/.github/workflows/_ci-desktop.yml new file mode 100644 index 00000000000..667b4841418 --- /dev/null +++ b/.github/workflows/_ci-desktop.yml @@ -0,0 +1,244 @@ +name: CI / Desktop +on: + workflow_call: + inputs: + rust: + required: true + type: boolean + desktop: + required: true + type: boolean + desktop_rust: + required: true + type: boolean + outputs: + desktop_result: + value: ${{ jobs.results.outputs.desktop_result }} + +env: + CARGO_TERM_COLOR: always + BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + +jobs: + desktop-core: + name: Desktop Core + runs-on: ubuntu-latest + timeout-minutes: 45 + if: github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 2 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: desktop/src-tauri + save-if: ${{ github.event_name != 'pull_request' }} + - name: Install Tauri dependencies (Linux) + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update \ + -o Acquire::Retries=3 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 + sudo 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 \ + curl \ + file \ + libasound2-dev \ + libayatana-appindicator3-dev \ + libgtk-3-dev \ + librsvg2-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev \ + patchelf \ + wget + - 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: Desktop lint and format + run: just desktop-check + - name: Desktop unit tests + run: just desktop-test + - name: Desktop build + run: just desktop-build + - name: Desktop Tauri clippy + run: just desktop-tauri-clippy + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Desktop Tauri check + run: just desktop-tauri-check + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Desktop Tauri tests + run: just desktop-tauri-test + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Desktop Tauri compiled-flag verification + run: just desktop-tauri-test-compiled-flags + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Upload desktop e2e artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: desktop-e2e-artifacts + path: | + desktop/playwright-report + desktop/test-results + if-no-files-found: ignore + - name: Save pnpm store cache + if: github.event_name == 'push' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + + desktop-smoke-e2e: + name: Desktop Smoke E2E (${{ matrix.shard }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + if: github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - 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: Get Playwright version + id: pw-version + run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT" + - name: Restore Playwright browser cache + id: playwright-cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} + - name: Install Playwright Chromium + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: cd desktop && pnpm exec playwright install chromium + - name: Install Playwright system dependencies + run: cd desktop && pnpm exec playwright install-deps chromium + - name: Save Playwright browser cache + if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1 + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} + - name: Desktop E2E build + run: pnpm -C desktop build:e2e + - name: Desktop smoke e2e + run: cd desktop && pnpm exec playwright test --project=smoke --shard=${{ matrix.shard }}/4 + - name: Summarize flaky tests + if: ${{ !cancelled() }} + run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop Smoke E2E (${{ matrix.shard }})" + working-directory: desktop + - name: Upload desktop smoke e2e artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: desktop-smoke-e2e-artifacts-${{ matrix.shard }} + path: | + desktop/playwright-report + desktop/playwright-report.json + desktop/test-results + if-no-files-found: ignore + retention-days: 7 + + desktop: + name: Desktop + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [desktop-core, desktop-smoke-e2e, desktop-windows-build] + if: always() && (github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust) + permissions: + contents: read + steps: + - name: Check desktop jobs + run: | + if [ "${{ needs.desktop-core.result }}" != "success" ]; then + echo "Desktop Core finished with: ${{ needs.desktop-core.result }}" + exit 1 + fi + if [ "${{ needs.desktop-smoke-e2e.result }}" != "success" ]; then + echo "Desktop Smoke E2E shards finished with: ${{ needs.desktop-smoke-e2e.result }}" + exit 1 + fi + if [ "${{ needs.desktop-windows-build.result }}" != "success" ]; then + echo "Desktop Windows Build finished with: ${{ needs.desktop-windows-build.result }}" + exit 1 + fi + echo "Desktop jobs passed" + + desktop-windows-build: + name: Desktop Windows Build + runs-on: windows-latest + timeout-minutes: 20 + if: github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24.14.1 + package-manager-cache: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + with: + version: 11.4.0 + - name: Install desktop dependencies + shell: bash + run: pnpm install --frozen-lockfile + - name: Build both protected-feature selections + shell: pwsh + run: | + Remove-Item Env:VITE_BUZZ_BESTIE -ErrorAction SilentlyContinue + pnpm -C desktop build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $env:VITE_BUZZ_BESTIE = "1" + pnpm -C desktop build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + results: + name: Results + if: ${{ always() }} + needs: [desktop] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + outputs: + desktop_result: ${{ needs.desktop.result }} + steps: + - run: echo "Captured Desktop job results" diff --git a/.github/workflows/_ci-relay.yml b/.github/workflows/_ci-relay.yml new file mode 100644 index 00000000000..c0fc3d5333f --- /dev/null +++ b/.github/workflows/_ci-relay.yml @@ -0,0 +1,611 @@ +name: CI / Relay and PostgreSQL +on: + workflow_call: + inputs: + rust: + required: true + type: boolean + desktop: + required: true + type: boolean + desktop_rust: + required: true + type: boolean + lane: + required: true + type: string + outputs: + desktop_e2e_relay_result: + value: ${{ jobs.results.outputs.desktop_e2e_relay_result }} + desktop_e2e_integration_result: + value: ${{ jobs.results.outputs.desktop_e2e_integration_result }} + backend_integration_result: + value: ${{ jobs.results.outputs.backend_integration_result }} + relay_e2e_result: + value: ${{ jobs.results.outputs.relay_e2e_result }} + postgres_tests_result: + value: ${{ jobs.results.outputs.postgres_tests_result }} + +env: + CARGO_TERM_COLOR: always + BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + +jobs: + desktop-e2e-relay: + name: Desktop E2E Relay + runs-on: ubuntu-latest + timeout-minutes: 30 + if: inputs.lane == 'artifacts' && (github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust) + permissions: + contents: read + env: + SCCACHE_GHA_ENABLED: "true" + SCCACHE_GHA_RW_MODE: ${{ (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.number == 5224)) && 'READ_WRITE' || 'READ_ONLY' }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + # Reuse the relay binaries and backend test archive when none of their + # inputs changed (desktop-only PRs hit this every time). The key covers + # everything they embed, including migrations via sqlx migrate!. + - name: Restore relay artifacts cache + id: relay-artifacts-cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: | + target/ci/buzz-relay + target/ci/git-credential-nostr + target/ci/backend-integration-tests.tar.zst + target/ci/postgres-tests.tar.zst + key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.config/nextest.toml', 'scripts/postgres-test-*.sh', 'scripts/check-postgres-test-discovery.py', '.github/workflows/ci.yml', '.github/workflows/_ci-*.yml') }} + - uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1 + if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' + with: + workspaces: | + . + desktop/src-tauri + save-if: ${{ github.event_name != 'pull_request' }} + # Cache rustc outputs for unchanged workspace crates. Trusted pushes write; + # the bounded PR 5224 trial writes only to its isolated merge-ref scope. + - name: Set up sccache + if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' + uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 # zizmor: ignore[cache-poisoning] Bounded trial: only PR 5224 writes to its isolated merge-ref scope; trusted pushes retain production writes. + with: + version: v0.16.0 + - name: Install cargo-nextest + if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 + with: + tool: cargo-nextest@0.9.136 + - name: Build relay artifacts + if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' + env: + RUSTC_WRAPPER: sccache + run: | + cargo build --profile ci -p buzz-relay -p git-credential-nostr + cargo nextest archive \ + --cargo-profile ci \ + -p buzz-db \ + -p buzz-relay \ + -p buzz-test-client \ + --lib \ + --test e2e_event_reminder \ + --archive-file target/ci/backend-integration-tests.tar.zst + postgres_package_args=() + while IFS= read -r package; do + postgres_package_args+=(-p "$package") + done < <(scripts/postgres-test-packages.sh) + if [[ "${#postgres_package_args[@]}" -eq 0 ]]; then + echo "no PostgreSQL test packages were discovered" >&2 + exit 1 + fi + cargo nextest archive \ + --cargo-profile ci \ + "${postgres_package_args[@]}" \ + --lib \ + --tests \ + --archive-file target/ci/postgres-tests.tar.zst + - name: Save relay artifacts cache + # PR-scoped exact-source entries cannot warm main or other PRs and churn + # the shared cache pool. sccache provides read-only PR reuse instead. + if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' && github.event_name == 'push' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: | + target/ci/buzz-relay + target/ci/git-credential-nostr + target/ci/backend-integration-tests.tar.zst + target/ci/postgres-tests.tar.zst + key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.config/nextest.toml', 'scripts/postgres-test-*.sh', 'scripts/check-postgres-test-discovery.py', '.github/workflows/ci.yml', '.github/workflows/_ci-*.yml') }} + - name: Upload relay artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: desktop-e2e-relay + path: | + target/ci/buzz-relay + target/ci/git-credential-nostr + target/ci/backend-integration-tests.tar.zst + target/ci/postgres-tests.tar.zst + if-no-files-found: error + retention-days: 1 + + postgres-tests: + name: PostgreSQL Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + if: inputs.lane == 'postgres' && (github.event_name == 'push' || inputs.rust) + permissions: + contents: read + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: buzz + POSTGRES_PASSWORD: ${{ env.BUZZ_TEST_POSTGRES_PASSWORD }} + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U buzz -d postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + env: + PGHOST: localhost + PGPORT: "5432" + PGUSER: buzz + PG_BIN_DIR: /usr/bin + REDIS_URL: redis://localhost:6379 + PGSCHEMA_PLAN_HOST: localhost + PGSCHEMA_PLAN_PORT: "5432" + PGSCHEMA_PLAN_USER: buzz + PGSCHEMA_PLAN_PASSWORD: buzz_dev + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Tune disposable PostgreSQL + env: + PGPASSWORD: ${{ env.BUZZ_TEST_POSTGRES_PASSWORD }} + run: | + # Database-per-test cloning forces checkpoints. Durability is redundant + # for this disposable service and makes runtime depend on runner disk I/O. + psql --dbname postgres --set ON_ERROR_STOP=1 <<'SQL' + ALTER SYSTEM SET fsync = off; + ALTER SYSTEM SET full_page_writes = off; + ALTER SYSTEM SET synchronous_commit = off; + SELECT pg_reload_conf(); + SQL + psql --dbname postgres --tuples-only --no-align --command \ + "SELECT bool_and(setting = 'off') FROM pg_settings WHERE name IN ('fsync', 'full_page_writes', 'synchronous_commit')" \ + | grep --fixed-strings --line-regexp t + - name: Install cargo-nextest + uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 + with: + tool: cargo-nextest@0.9.136 + - name: Download backend test archive + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: desktop-e2e-relay + path: target/ci + - name: PostgreSQL-backed tests + env: + BUZZ_POSTGRES_ADMIN_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/postgres + PGPASSWORD: ${{ env.BUZZ_TEST_POSTGRES_PASSWORD }} + run: | + scripts/postgres-test-run.sh \ + --archive-file target/ci/postgres-tests.tar.zst + + desktop-e2e-integration-shard: + name: Desktop E2E Integration (${{ matrix.shard }}/2) + runs-on: ubuntu-latest + timeout-minutes: 20 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust) + strategy: + fail-fast: false + matrix: + shard: [1, 2] + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Start integration services + run: | + for attempt in 1 2 3; do + if docker compose up -d postgres redis minio minio-init; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "docker compose up failed after 3 attempts" >&2 + exit 1 + fi + echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2 + sleep $((attempt * 5)) + done + - 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: Get Playwright version + id: pw-version + run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT" + - name: Restore Playwright browser cache + id: playwright-cache + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} + - name: Install Playwright Chromium + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: cd desktop && pnpm exec playwright install chromium + - name: Install Playwright system dependencies + run: cd desktop && pnpm exec playwright install-deps chromium + - name: Save Playwright browser cache + if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1 + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} + - name: Desktop E2E build + run: pnpm -C desktop build:e2e + - name: Wait for integration services + run: | + wait_healthy() { + local service="$1" + local container="$2" + for attempt in $(seq 1 60); do + status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found") + if [ "${status}" = "healthy" ]; then + echo "${service} is healthy" + return 0 + fi + sleep 2 + done + docker logs "${container}" || true + return 1 + } + wait_healthy "Postgres" "buzz-postgres" + wait_healthy "Redis" "buzz-redis" + wait_healthy "MinIO" "buzz-minio" + - name: Download relay binary + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: desktop-e2e-relay + path: target/ci + - name: Apply schema and seed deployment community + # MT: the relay resolves each request's tenant from the communities host + # map and fails closed on an unmapped host. The channel reconciler binds + # the deployment community ONCE at boot (outside its retry loop) and + # exits permanently on an unmapped host, so the 'localhost:3000' + # community MUST exist before the relay starts — the retry loop only + # handles late-seeded channels, not a late-seeded community. The relay + # migrates at boot via BUZZ_AUTO_MIGRATE, but that's too late for the + # pre-boot seed, so apply the schema here first (then drop AUTO_MIGRATE + # below). lower(host) is the unique index → ON CONFLICT target. psql + # isn't on PATH in hermit → exec into the buzz-postgres container. + env: + PGHOST: localhost + PGPORT: "5432" + PGUSER: buzz + PGPASSWORD: buzz_dev + PGDATABASE: buzz + # Use the already-running docker postgres for desired-state planning instead of + # downloading an embedded Postgres from Maven Central (transient-fetch flake source). + PGSCHEMA_PLAN_HOST: localhost + PGSCHEMA_PLAN_PORT: "5432" + PGSCHEMA_PLAN_DB: buzz + PGSCHEMA_PLAN_USER: buzz + PGSCHEMA_PLAN_PASSWORD: buzz_dev + run: | + ./bin/pgschema apply --file schema/schema.sql --auto-approve + docker exec -i -e PGPASSWORD=buzz_dev buzz-postgres \ + psql -U buzz -d buzz -v ON_ERROR_STOP=1 < scripts/reconcile-schema-after-pgschema.sql + docker exec -e PGPASSWORD=buzz_dev buzz-postgres \ + psql -U buzz -d buzz -qtA -c " + INSERT INTO communities (id, host) + VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000') + ON CONFLICT (lower(host)) DO NOTHING + ;" + - name: Start relay + run: | + chmod +x ./target/ci/buzz-relay + nohup env \ + DATABASE_URL="postgres://buzz:${BUZZ_TEST_POSTGRES_PASSWORD}@localhost:5432/buzz" \ + REDIS_URL=redis://localhost:6379 \ + RELAY_URL=ws://localhost:3000 \ + BUZZ_BIND_ADDR=0.0.0.0:3000 \ + BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \ + BUZZ_REQUIRE_AUTH_TOKEN=false \ + BUZZ_RECONCILE_CHANNELS=true \ + BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=100000 \ + BUZZ_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN=100000 \ + BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC=10000 \ + BUZZ_GIT_PROBE_WRITERS=8 \ + SPROUT_REMINDER_SCHEDULER_INTERVAL_SECS=1 \ + ./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 & + echo $! > /tmp/buzz-relay.pid + for attempt in $(seq 1 60); do + if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then + cat /tmp/buzz-relay.log + exit 1 + fi + status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true) + if [ "${status_code}" = "200" ]; then + exit 0 + fi + sleep 1 + done + cat /tmp/buzz-relay.log + exit 1 + - name: Seed desktop e2e data + run: bash scripts/setup-desktop-test-data.sh + - name: Desktop relay-backed e2e + run: cd desktop && pnpm exec playwright test --project=integration --shard=${{ matrix.shard }}/2 + - name: Summarize flaky tests + if: ${{ !cancelled() }} + run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop E2E Integration (${{ matrix.shard }}/2)" + working-directory: desktop + - name: Upload desktop integration artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: desktop-e2e-integration-artifacts-${{ matrix.shard }} + path: | + desktop/playwright-report + desktop/playwright-report.json + desktop/test-results + /tmp/buzz-relay.log + if-no-files-found: ignore + retention-days: 7 + - name: Save pnpm store cache + if: github.event_name == 'push' + uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + + desktop-e2e-integration: + name: Desktop E2E Integration + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [desktop-e2e-integration-shard] + if: always() && inputs.lane == 'required' && (github.event_name == 'push' || inputs.desktop || inputs.desktop_rust || inputs.rust) + permissions: + contents: read + steps: + - name: Check integration shards + run: | + if [ "${{ needs.desktop-e2e-integration-shard.result }}" != "success" ]; then + echo "Desktop E2E Integration shards finished with: ${{ needs.desktop-e2e-integration-shard.result }}" + exit 1 + fi + echo "Desktop E2E Integration shards passed" + + backend-integration: + name: Backend Integration (relay e2e) + runs-on: ubuntu-latest + timeout-minutes: 20 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.rust) + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Install cargo-nextest + uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 + with: + tool: cargo-nextest@0.9.136 + - name: Start integration services + run: | + for attempt in 1 2 3; do + if docker compose up -d postgres redis minio minio-init; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "docker compose up failed after 3 attempts" >&2 + exit 1 + fi + echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2 + sleep $((attempt * 5)) + done + - name: Wait for integration services + run: | + wait_healthy() { + local service="$1" + local container="$2" + for attempt in $(seq 1 60); do + status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found") + if [ "${status}" = "healthy" ]; then + echo "${service} is healthy" + return 0 + fi + sleep 2 + done + docker logs "${container}" || true + return 1 + } + wait_healthy "Postgres" "buzz-postgres" + wait_healthy "Redis" "buzz-redis" + wait_healthy "MinIO" "buzz-minio" + - name: Download relay artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: desktop-e2e-relay + path: target/ci + - name: Apply schema and seed deployment community + # MT: the relay resolves each request's tenant from the communities host + # map and fails closed on an unmapped host. The reminder scheduler binds + # the deployment community ONCE at boot and exits permanently on an + # unmapped host (no retry, unlike the channel reconciler), so the + # 'localhost:3000' community MUST exist before the relay starts — seeding + # after boot leaves the scheduler dead. The relay migrates at boot via + # BUZZ_AUTO_MIGRATE, but that's too late for the pre-boot seed, so apply + # the schema here first (then drop AUTO_MIGRATE below). lower(host) is the + # unique index → ON CONFLICT target. psql isn't on PATH in hermit → exec + # into the buzz-postgres container. + env: + PGHOST: localhost + PGPORT: "5432" + PGUSER: buzz + PGPASSWORD: buzz_dev + PGDATABASE: buzz + # Use the already-running docker postgres for desired-state planning instead of + # downloading an embedded Postgres from Maven Central (transient-fetch flake source). + PGSCHEMA_PLAN_HOST: localhost + PGSCHEMA_PLAN_PORT: "5432" + PGSCHEMA_PLAN_DB: buzz + PGSCHEMA_PLAN_USER: buzz + PGSCHEMA_PLAN_PASSWORD: buzz_dev + run: | + ./bin/pgschema apply --file schema/schema.sql --auto-approve + docker exec -i -e PGPASSWORD=buzz_dev buzz-postgres \ + psql -U buzz -d buzz -v ON_ERROR_STOP=1 < scripts/reconcile-schema-after-pgschema.sql + docker exec -e PGPASSWORD=buzz_dev buzz-postgres \ + psql -U buzz -d buzz -qtA -c " + INSERT INTO communities (id, host) + VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000') + ON CONFLICT (lower(host)) DO NOTHING + ;" + - name: Workflow message provenance unit tests + # The relay's workflow_sink suite is not selected by the infra-free + # unit job. Its ignored database cases run in the isolated PostgreSQL + # lane; keep the pure provenance cases covered here without duplication. + run: | + cargo nextest run \ + --archive-file target/ci/backend-integration-tests.tar.zst \ + -E 'package(buzz-relay) and test(/workflow_sink/)' + - name: Start relay + run: | + chmod +x ./target/ci/buzz-relay + nohup env \ + DATABASE_URL="postgres://buzz:${BUZZ_TEST_POSTGRES_PASSWORD}@localhost:5432/buzz" \ + REDIS_URL=redis://localhost:6379 \ + RELAY_URL=ws://localhost:3000 \ + BUZZ_BIND_ADDR=0.0.0.0:3000 \ + BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \ + BUZZ_REQUIRE_AUTH_TOKEN=false \ + BUZZ_RECONCILE_CHANNELS=true \ + BUZZ_GIT_PROBE_WRITERS=8 \ + SPROUT_REMINDER_SCHEDULER_INTERVAL_SECS=1 \ + ./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 & + echo $! > /tmp/buzz-relay.pid + for attempt in $(seq 1 60); do + if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then + cat /tmp/buzz-relay.log + exit 1 + fi + status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true) + if [ "${status_code}" = "200" ]; then + exit 0 + fi + sleep 1 + done + cat /tmp/buzz-relay.log + exit 1 + - name: NIP-ER reminder e2e + # Feature e2e for NIP-ER (Event Reminders, kind:30300): write-path + # validation, author-only read filtering, and scheduler delivery against + # a live relay. The schema-drift / migration-version guarantee is owned + # by the buzz-db migration.rs unit tests, not this suite. + run: | + cargo nextest run \ + --archive-file target/ci/backend-integration-tests.tar.zst \ + -E 'binary(e2e_event_reminder)' \ + --run-ignored ignored-only + env: + RELAY_URL: ws://localhost:3000 + - name: Upload relay log + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: backend-integration-relay-log + path: /tmp/buzz-relay.log + if-no-files-found: ignore + + relay-e2e: + name: Relay E2E + runs-on: ubuntu-latest + timeout-minutes: 20 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.rust) + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + save-if: ${{ github.event_name != 'pull_request' }} + # Reuse the relay + git-credential-nostr built by Desktop E2E Relay + # instead of compiling them a second time. + - name: Download relay binary + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: desktop-e2e-relay + path: target/ci + - name: Start relay + run: | + chmod +x ./target/ci/buzz-relay ./target/ci/git-credential-nostr + ./scripts/start-relay-for-tests.sh --no-build + - name: Relay E2E tests + run: | + cargo test -p buzz-test-client --test e2e_persona --test e2e_team_catalog --test e2e_nostr_interop --test e2e_project -- --ignored --nocapture + cargo test -p buzz-test-client --test e2e_relay invite -- --ignored --nocapture + cargo test -p buzz-test-client --test e2e_relay nip43_membership_snapshots_are_rejected -- --ignored --nocapture + env: + RELAY_URL: ws://localhost:3000 + GIT_CREDENTIAL_NOSTR_BIN: ${{ github.workspace }}/target/ci/git-credential-nostr + - name: Media read-auth e2e + # Reads require kind:24242 `t=get` auth, so these binaries are the only + # coverage that a real relay rejects bare reads and honours host- and + # hash-scoped tokens. They were #[ignore]d and selected by no CI job, so + # the lane never ran; select it here, where MinIO and the seeded + # 'localhost:3000' community already exist. + # --no-fail-fast: without it cargo stops after the first failing binary, + # so one broken case hides every later binary's result. + run: | + cargo test -p buzz-test-client --no-fail-fast --test e2e_media --test e2e_media_extended --test e2e_media_video -- --ignored --nocapture + env: + RELAY_URL: ws://localhost:3000 + RELAY_HTTP_URL: http://localhost:3000 + - name: Upload relay logs + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: relay-e2e-artifacts + path: /tmp/buzz-relay.log + if-no-files-found: ignore + + + results: + name: Results + if: ${{ always() }} + needs: [desktop-e2e-relay, postgres-tests, desktop-e2e-integration, backend-integration, relay-e2e] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + outputs: + desktop_e2e_relay_result: ${{ needs.desktop-e2e-relay.result }} + desktop_e2e_integration_result: ${{ needs.desktop-e2e-integration.result }} + backend_integration_result: ${{ needs.backend-integration.result }} + relay_e2e_result: ${{ needs.relay-e2e.result }} + postgres_tests_result: ${{ needs.postgres-tests.result }} + steps: + - run: echo "Captured Relay and PostgreSQL job results" diff --git a/.github/workflows/_ci-rust.yml b/.github/workflows/_ci-rust.yml new file mode 100644 index 00000000000..f49cc96eb2e --- /dev/null +++ b/.github/workflows/_ci-rust.yml @@ -0,0 +1,219 @@ +name: CI / Rust +on: + workflow_call: + inputs: + rust: + required: true + type: boolean + desktop_rust: + required: true + type: boolean + lane: + required: true + type: string + outputs: + rust_lint_result: + value: ${{ jobs.results.outputs.rust_lint_result }} + unit_tests_result: + value: ${{ jobs.results.outputs.unit_tests_result }} + windows_rust_result: + value: ${{ jobs.results.outputs.windows_rust_result }} + +env: + CARGO_TERM_COLOR: always + BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + +jobs: + rust-lint: + name: Rust Lint + runs-on: ubuntu-latest + timeout-minutes: 30 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.rust || inputs.desktop_rust) + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + save-if: ${{ github.event_name != 'pull_request' }} + - name: Format check + run: just fmt-check + - name: Desktop Tauri format check + run: just desktop-tauri-fmt-check + - name: Clippy + run: just clippy + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.rust) + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + key: sherpa-cache-v1 + save-if: ${{ github.event_name != 'pull_request' }} + - name: Install cargo-nextest + uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 + with: + tool: cargo-nextest@0.9.136 + - name: Unit tests + run: just test-unit + + server-cross-compile: + name: Server Cross-Compile + runs-on: ubuntu-latest + timeout-minutes: 30 + if: inputs.lane == 'cross-compile' && (github.event_name == 'push' || inputs.rust) + permissions: + contents: read + strategy: + fail-fast: false + matrix: + target: + - x86_64-unknown-linux-musl + - aarch64-unknown-linux-musl + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + key: cross-${{ matrix.target }} + save-if: ${{ github.event_name != 'pull_request' }} + - name: Install cross + uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 + with: + tool: cross@0.2.5 + - name: Build server binaries + env: + TARGET: ${{ matrix.target }} + # PRs: compile + build-script gate only (no codegen/link). Main: full link gate. + CARGO_CMD: ${{ github.event_name == 'pull_request' && 'check' || 'build' }} + run: | + cross "$CARGO_CMD" --release --target "$TARGET" \ + -p buzz-relay \ + -p buzz-acp \ + -p buzz-agent \ + -p buzz-dev-mcp \ + -p git-credential-nostr \ + -p git-sign-nostr + + 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 + if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.rust || inputs.desktop_rust) + permissions: + contents: read + env: + TARGET: x86_64-pc-windows-msvc + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + # MSVC needs windows.h (aws-lc-sys et al.), so this runs on a real Windows + # runner — hermit, used by the Linux jobs, does not provide MSVC. The + # toolchain (1.95.0 + clippy via profile = default) comes from the + # repo-root rust-toolchain.toml, which the runner's preinstalled rustup + # honors on demand; the host triple already is x86_64-pc-windows-msvc. + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: | + . + desktop/src-tauri + key: windows-msvc + save-if: ${{ github.event_name != 'pull_request' }} + # Tauri validates externalBin at compile time, so the Tauri-crate steps + # below fail without these stubs. Mirrors scripts/bundle-sidecars.sh's + # Windows naming (binaries/-.exe); empty files suffice for a + # type-check since nothing executes them. + - name: Create sidecar placeholders + shell: bash + run: | + mkdir -p desktop/src-tauri/binaries + for bin in buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz; do + touch "desktop/src-tauri/binaries/${bin}-${TARGET}.exe" + done + - 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. + # Serial: windows_resolver_tests mutate process-global env + # (BUZZ_SHELL/GIT_BASH/SystemRoot) that SharedState::new reads. + run: cargo test -p buzz-dev-mcp --target $env:TARGET -- --test-threads=1 + - name: Test (buzz-agent auth coordinator) + # The auth coordinator single-flights on an OS advisory lock, which is + # LockFileEx on Windows; this integration suite drives real second + # processes on the same lock file, so it only exercises the Windows + # lock runtime if it runs ON Windows. Every other job compiles it but + # never executes it. Tests exercised on Windows: lock serialization + # (two coordinators race for the same key), cooldown sidecar sharing + # across processes, attempt-sidecar adoption (UserInitiated waiter + # adopts a predecessor's denial), and the in-process single-flight for + # same-key coalescing. Tests that are UNIX-ONLY and NOT executed here: + # crash-release (flock drop on SIGKILL, guarded by #[cfg(unix)]) and + # cross-process cache success/race (on-disk token handoff, also + # #[cfg(unix)]). + run: cargo test -p buzz-agent --target $env:TARGET --test databricks_auth_coordinator + # Smoke-test the new host-prereq contract: Git for Windows (which provides + # bash) is available on the runner, a shell command round-trips, and bash + # does NOT resolve from System32 (so WSL's launcher is never picked up). + # windows-latest runners have Git for Windows pre-installed; the unit tests + # above exercise the MCP resolver itself. This step verifies the host env. + - name: Smoke-test host Git Bash prereq (host env check) + shell: bash + run: | + set -euo pipefail + # Git for Windows ships bash.exe under its bin/ directory; confirm it + # resolves from the standard location the runtime resolver probes first. + bash_path=$(command -v bash 2>/dev/null || true) + [[ -n "$bash_path" ]] || { echo "ERROR: bash not found on PATH — host Git for Windows missing" >&2; exit 1; } + echo "Resolved bash: $bash_path" + [[ "$bash_path" != *System32* ]] || { echo "ERROR: resolved bash is WSL's System32 launcher" >&2; exit 1; } + + # Run a basic pipeline through the resolved bash (same invocation the + # agent uses: bash -c '...'). + out=$(bash -c 'echo hello | tr a-z A-Z') + [[ "$out" == "HELLO" ]] || { echo "bash pipeline failed: got '$out'" >&2; exit 1; } + + # Confirm git itself works — agents run git commands frequently. + git --version + repo=$(mktemp -d) + cd "$repo" + git init -q + git -c user.name=ci -c user.email=ci@example.com commit -q --allow-empty -m smoke + git log -1 --format=%s | grep -qx smoke + echo "Host bash resolved and functional; git commit round-trip passed" + - name: Check (Tauri crate) + run: cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --target $env:TARGET + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Test (Tauri crate) + run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --target $env:TARGET + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + + + results: + name: Results + if: ${{ always() }} + needs: [rust-lint, unit-tests, server-cross-compile, windows-rust] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + outputs: + rust_lint_result: ${{ needs.rust-lint.result }} + unit_tests_result: ${{ needs.unit-tests.result }} + windows_rust_result: ${{ needs.windows-rust.result }} + steps: + - run: echo "Captured Rust job results" diff --git a/.github/workflows/_ci-security.yml b/.github/workflows/_ci-security.yml new file mode 100644 index 00000000000..29855e8f781 --- /dev/null +++ b/.github/workflows/_ci-security.yml @@ -0,0 +1,42 @@ +name: CI / Security +on: + workflow_call: + inputs: + rust: + required: true + type: boolean + outputs: + security_result: + value: ${{ jobs.results.outputs.security_result }} + +env: + CARGO_TERM_COLOR: always + BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright + +jobs: + security: + name: Security + runs-on: ubuntu-latest + timeout-minutes: 20 + if: github.event_name == 'push' || inputs.rust + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Dependency policy + run: cargo-deny check + + + results: + name: Results + if: ${{ always() }} + needs: [security] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + outputs: + security_result: ${{ needs.security.result }} + steps: + - run: echo "Captured Security job results" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc7965ad292..a663ef957c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: - 'scripts/test-postgres-test-wrapper.sh' - 'deny.toml' - '.github/workflows/ci.yml' + - '.github/workflows/_ci-*.yml' - 'scripts/run-tests.sh' - 'scripts/model-capabilities.json' - 'scripts/normative-corpus.json' @@ -81,6 +82,7 @@ jobs: - 'scripts/test-mobile-worktree-overrides.sh' - '.github/workflows/mobile-release-candidate.yml' - '.github/workflows/ci.yml' + - '.github/workflows/_ci-*.yml' - name: Validate PostgreSQL test discovery if: github.event_name == 'push' || steps.filter.outputs.rust == 'true' run: | @@ -110,1191 +112,298 @@ jobs: run: | scripts/test-rust-cache-contract.sh scripts/test-rust-cache-contract-regressions.sh + - name: CI required-context isolation contract + run: scripts/test-ci-required-context-isolation.sh - name: File size policy run: just file-size-check - rust-lint: - name: Rust Lint + dead-token-guard: + name: Dead Token Reference Guard runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true' + timeout-minutes: 5 permissions: contents: read steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - save-if: ${{ github.event_name != 'pull_request' }} - - name: Format check - run: just fmt-check - - name: Desktop Tauri format check - run: just desktop-tauri-fmt-check - - name: Clippy - run: just clippy + - name: Check for dead API token references in client code + run: | + # Fail if dead API token patterns reappear in desktop, mobile, docs, or config. + # Relay crates are excluded — they still use token auth internally. + PATTERNS='TokenScope|MintTokenResponse|hasApiToken|spr_tok_' + PATHS='desktop/src/ desktop/tests/ mobile/test/ mobile/lib/ .env.example' + EXCLUDES='--exclude-dir=node_modules --exclude-dir=.dart_tool' + if grep -rn $EXCLUDES -E "$PATTERNS" $PATHS 2>/dev/null; then + echo "::error::Dead API token references found in client code. See above." + exit 1 + fi + echo "No dead token references found." - unit-tests: - name: Unit Tests - runs-on: ubuntu-latest - timeout-minutes: 30 + rust: + name: Rust + needs: [changes] + if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true' + uses: ./.github/workflows/_ci-rust.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} + lane: required + + rust-cross-compile-domain: + name: Rust Cross-Compile needs: [changes] if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - key: sherpa-cache-v1 - save-if: ${{ github.event_name != 'pull_request' }} - - name: Install cargo-nextest - uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 - with: - tool: cargo-nextest@0.9.136 - - name: Unit tests - run: just test-unit + uses: ./.github/workflows/_ci-rust.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} + lane: cross-compile - desktop-core: - name: Desktop Core - runs-on: ubuntu-latest - timeout-minutes: 45 + desktop-domain: + name: Desktop Domain needs: [changes] if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 2 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - workspaces: desktop/src-tauri - save-if: ${{ github.event_name != 'pull_request' }} - - name: Install Tauri dependencies (Linux) - env: - DEBIAN_FRONTEND: noninteractive - run: | - sudo apt-get update \ - -o Acquire::Retries=3 \ - -o Acquire::http::Timeout=30 \ - -o Acquire::https::Timeout=30 - sudo 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 \ - curl \ - file \ - libasound2-dev \ - libayatana-appindicator3-dev \ - libgtk-3-dev \ - librsvg2-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - patchelf \ - wget - - 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: Desktop lint and format - run: just desktop-check - - name: Desktop unit tests - run: just desktop-test - - name: Desktop build - run: just desktop-build - - name: Desktop Tauri clippy - run: just desktop-tauri-clippy - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Desktop Tauri check - run: just desktop-tauri-check - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Desktop Tauri tests - run: just desktop-tauri-test - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Desktop Tauri compiled-flag verification - run: just desktop-tauri-test-compiled-flags - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Upload desktop e2e artifacts - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: desktop-e2e-artifacts - path: | - desktop/playwright-report - desktop/test-results - if-no-files-found: ignore - - name: Save pnpm store cache - if: github.event_name == 'push' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + uses: ./.github/workflows/_ci-desktop.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop: ${{ needs.changes.outputs.desktop == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} - desktop-smoke-e2e: - name: Desktop Smoke E2E (${{ matrix.shard }}) - runs-on: ubuntu-latest - timeout-minutes: 30 + relay-artifacts-domain: + name: Relay Artifact Producer needs: [changes] if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4] - permissions: - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - 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: Get Playwright version - id: pw-version - run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT" - - name: Restore Playwright browser cache - id: playwright-cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} - - name: Install Playwright Chromium - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: cd desktop && pnpm exec playwright install chromium - - name: Install Playwright system dependencies - run: cd desktop && pnpm exec playwright install-deps chromium - - name: Save Playwright browser cache - if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1 - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} - - name: Desktop E2E build - run: pnpm -C desktop build:e2e - - name: Desktop smoke e2e - run: cd desktop && pnpm exec playwright test --project=smoke --shard=${{ matrix.shard }}/4 - - name: Summarize flaky tests - if: ${{ !cancelled() }} - run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop Smoke E2E (${{ matrix.shard }})" - working-directory: desktop - - name: Upload desktop smoke e2e artifacts - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: desktop-smoke-e2e-artifacts-${{ matrix.shard }} - path: | - desktop/playwright-report - desktop/playwright-report.json - desktop/test-results - if-no-files-found: ignore - retention-days: 7 + uses: ./.github/workflows/_ci-relay.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop: ${{ needs.changes.outputs.desktop == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} + lane: artifacts - desktop: - name: Desktop + postgres-domain: + name: PostgreSQL Domain + needs: [changes, relay-artifacts-domain] + if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' + uses: ./.github/workflows/_ci-relay.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop: ${{ needs.changes.outputs.desktop == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} + lane: postgres + + desktop-macos-domain: + name: Desktop macOS Domain + needs: [changes] + if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' + uses: ./.github/workflows/_ci-desktop-macos.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop: ${{ needs.changes.outputs.desktop == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} + + relay-domain: + name: Relay and PostgreSQL + needs: [changes, relay-artifacts-domain] + if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' + uses: ./.github/workflows/_ci-relay.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + desktop: ${{ needs.changes.outputs.desktop == 'true' }} + desktop_rust: ${{ needs.changes.outputs.desktop-rust == 'true' }} + lane: required + + clients: + name: Clients + needs: [changes] + if: github.event_name == 'push' || needs.changes.outputs.web == 'true' || needs.changes.outputs.mobile == 'true' + uses: ./.github/workflows/_ci-clients.yml + with: + web: ${{ needs.changes.outputs.web == 'true' }} + mobile: ${{ needs.changes.outputs.mobile == 'true' }} + lane: required + + mobile-swift-domain: + name: Mobile Swift Domain + needs: [changes] + if: needs.changes.outputs.mobile == 'true' + uses: ./.github/workflows/_ci-clients.yml + with: + web: ${{ needs.changes.outputs.web == 'true' }} + mobile: ${{ needs.changes.outputs.mobile == 'true' }} + lane: mobile-swift + + security-domain: + name: Security Domain + needs: [changes] + if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' + uses: ./.github/workflows/_ci-security.yml + with: + rust: ${{ needs.changes.outputs.rust == 'true' }} + + rust-lint: + name: Rust Lint + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true') + needs: [changes, rust] runs-on: ubuntu-latest timeout-minutes: 5 - needs: [changes, desktop-core, desktop-smoke-e2e, desktop-windows-build] - if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') - permissions: - contents: read + permissions: {} steps: - - name: Check desktop jobs - run: | - if [ "${{ needs.desktop-core.result }}" != "success" ]; then - echo "Desktop Core finished with: ${{ needs.desktop-core.result }}" - exit 1 - fi - if [ "${{ needs.desktop-smoke-e2e.result }}" != "success" ]; then - echo "Desktop Smoke E2E shards finished with: ${{ needs.desktop-smoke-e2e.result }}" - exit 1 - fi - if [ "${{ needs.desktop-windows-build.result }}" != "success" ]; then - echo "Desktop Windows Build finished with: ${{ needs.desktop-windows-build.result }}" - exit 1 - fi - echo "Desktop jobs passed" + - name: Check Rust Lint result + env: + RESULT: ${{ needs.rust.outputs.rust_lint_result }} + run: test "$RESULT" = success - desktop-e2e-relay: - name: Desktop E2E Relay + unit-tests: + name: Unit Tests + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true') + needs: [changes, rust] runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - env: - SCCACHE_GHA_ENABLED: "true" - SCCACHE_GHA_RW_MODE: ${{ (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.number == 5224)) && 'READ_WRITE' || 'READ_ONLY' }} + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - # Reuse the relay binaries and backend test archive when none of their - # inputs changed (desktop-only PRs hit this every time). The key covers - # everything they embed, including migrations via sqlx migrate!. - - name: Restore relay artifacts cache - id: relay-artifacts-cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: | - target/ci/buzz-relay - target/ci/git-credential-nostr - target/ci/backend-integration-tests.tar.zst - target/ci/postgres-tests.tar.zst - key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.config/nextest.toml', 'scripts/postgres-test-*.sh', 'scripts/check-postgres-test-discovery.py', '.github/workflows/ci.yml') }} - - uses: rui314/setup-mold@7e4f20ad28a2e8ca6fd0892ccf72e2abb706b9c3 # v1 - if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' - with: - workspaces: | - . - desktop/src-tauri - save-if: ${{ github.event_name != 'pull_request' }} - # Cache rustc outputs for unchanged workspace crates. Trusted pushes write; - # the bounded PR 5224 trial writes only to its isolated merge-ref scope. - - name: Set up sccache - if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' - uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 # zizmor: ignore[cache-poisoning] Bounded trial: only PR 5224 writes to its isolated merge-ref scope; trusted pushes retain production writes. - with: - version: v0.16.0 - - name: Install cargo-nextest - if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' - uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 - with: - tool: cargo-nextest@0.9.136 - - name: Build relay artifacts - if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' + - name: Check Unit Tests result env: - RUSTC_WRAPPER: sccache - run: | - cargo build --profile ci -p buzz-relay -p git-credential-nostr - cargo nextest archive \ - --cargo-profile ci \ - -p buzz-db \ - -p buzz-relay \ - -p buzz-test-client \ - --lib \ - --test e2e_event_reminder \ - --archive-file target/ci/backend-integration-tests.tar.zst - postgres_package_args=() - while IFS= read -r package; do - postgres_package_args+=(-p "$package") - done < <(scripts/postgres-test-packages.sh) - if [[ "${#postgres_package_args[@]}" -eq 0 ]]; then - echo "no PostgreSQL test packages were discovered" >&2 - exit 1 - fi - cargo nextest archive \ - --cargo-profile ci \ - "${postgres_package_args[@]}" \ - --lib \ - --tests \ - --archive-file target/ci/postgres-tests.tar.zst - - name: Save relay artifacts cache - # PR-scoped exact-source entries cannot warm main or other PRs and churn - # the shared cache pool. sccache provides read-only PR reuse instead. - if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' && github.event_name == 'push' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: | - target/ci/buzz-relay - target/ci/git-credential-nostr - target/ci/backend-integration-tests.tar.zst - target/ci/postgres-tests.tar.zst - key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.config/nextest.toml', 'scripts/postgres-test-*.sh', 'scripts/check-postgres-test-discovery.py', '.github/workflows/ci.yml') }} - - name: Upload relay artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: desktop-e2e-relay - path: | - target/ci/buzz-relay - target/ci/git-credential-nostr - target/ci/backend-integration-tests.tar.zst - target/ci/postgres-tests.tar.zst - if-no-files-found: error - retention-days: 1 + RESULT: ${{ needs.rust.outputs.unit_tests_result }} + run: test "$RESULT" = success - postgres-tests: - name: PostgreSQL Tests + windows-rust: + name: Windows Rust (x86_64-pc-windows-msvc) + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true') + needs: [changes, rust] runs-on: ubuntu-latest - timeout-minutes: 10 - needs: [changes, desktop-e2e-relay] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - services: - postgres: - image: postgres:16 + timeout-minutes: 5 + permissions: {} + steps: + - name: Check Windows Rust result env: - POSTGRES_USER: buzz - POSTGRES_PASSWORD: ${{ env.BUZZ_TEST_POSTGRES_PASSWORD }} - POSTGRES_DB: postgres - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U buzz -d postgres" - --health-interval 5s - --health-timeout 5s - --health-retries 12 - redis: - image: redis:7 - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 5s - --health-timeout 5s - --health-retries 12 - env: - PGHOST: localhost - PGPORT: "5432" - PGUSER: buzz - PG_BIN_DIR: /usr/bin - REDIS_URL: redis://localhost:6379 - PGSCHEMA_PLAN_HOST: localhost - PGSCHEMA_PLAN_PORT: "5432" - PGSCHEMA_PLAN_USER: buzz - PGSCHEMA_PLAN_PASSWORD: buzz_dev + RESULT: ${{ needs.rust.outputs.windows_rust_result }} + run: test "$RESULT" = success + + desktop: + name: Desktop + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') + needs: [changes, desktop-domain] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Tune disposable PostgreSQL + - name: Check Desktop result env: - PGPASSWORD: ${{ env.BUZZ_TEST_POSTGRES_PASSWORD }} - run: | - # Database-per-test cloning forces checkpoints. Durability is redundant - # for this disposable service and makes runtime depend on runner disk I/O. - psql --dbname postgres --set ON_ERROR_STOP=1 <<'SQL' - ALTER SYSTEM SET fsync = off; - ALTER SYSTEM SET full_page_writes = off; - ALTER SYSTEM SET synchronous_commit = off; - SELECT pg_reload_conf(); - SQL - psql --dbname postgres --tuples-only --no-align --command \ - "SELECT bool_and(setting = 'off') FROM pg_settings WHERE name IN ('fsync', 'full_page_writes', 'synchronous_commit')" \ - | grep --fixed-strings --line-regexp t - - name: Install cargo-nextest - uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 - with: - tool: cargo-nextest@0.9.136 - - name: Download backend test archive - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: desktop-e2e-relay - path: target/ci - - name: PostgreSQL-backed tests + RESULT: ${{ needs.desktop-domain.outputs.desktop_result }} + run: test "$RESULT" = success + + desktop-build-macos: + name: Desktop Build (macOS) + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') + needs: [changes, desktop-macos-domain] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + - name: Check Desktop Build (macOS) result env: - BUZZ_POSTGRES_ADMIN_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/postgres - PGPASSWORD: ${{ env.BUZZ_TEST_POSTGRES_PASSWORD }} - run: | - scripts/postgres-test-run.sh \ - --archive-file target/ci/postgres-tests.tar.zst + RESULT: ${{ needs.desktop-macos-domain.outputs.desktop_macos_result }} + run: test "$RESULT" = success - desktop-e2e-integration-shard: - name: Desktop E2E Integration (${{ matrix.shard }}/2) + desktop-e2e-relay: + name: Desktop E2E Relay + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') + needs: [changes, relay-artifacts-domain] runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [changes, desktop-e2e-relay] - if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' - strategy: - fail-fast: false - matrix: - shard: [1, 2] - permissions: - contents: read + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Start integration services - run: | - for attempt in 1 2 3; do - if docker compose up -d postgres redis minio minio-init; then - break - fi - if [ "$attempt" -eq 3 ]; then - echo "docker compose up failed after 3 attempts" >&2 - exit 1 - fi - echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2 - sleep $((attempt * 5)) - done - - 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: Get Playwright version - id: pw-version - run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT" - - name: Restore Playwright browser cache - id: playwright-cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} - - name: Install Playwright Chromium - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: cd desktop && pnpm exec playwright install chromium - - name: Install Playwright system dependencies - run: cd desktop && pnpm exec playwright install-deps chromium - - name: Save Playwright browser cache - if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1 - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} - - name: Desktop E2E build - run: pnpm -C desktop build:e2e - - name: Wait for integration services - run: | - wait_healthy() { - local service="$1" - local container="$2" - for attempt in $(seq 1 60); do - status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found") - if [ "${status}" = "healthy" ]; then - echo "${service} is healthy" - return 0 - fi - sleep 2 - done - docker logs "${container}" || true - return 1 - } - wait_healthy "Postgres" "buzz-postgres" - wait_healthy "Redis" "buzz-redis" - wait_healthy "MinIO" "buzz-minio" - - name: Download relay binary - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: desktop-e2e-relay - path: target/ci - - name: Apply schema and seed deployment community - # MT: the relay resolves each request's tenant from the communities host - # map and fails closed on an unmapped host. The channel reconciler binds - # the deployment community ONCE at boot (outside its retry loop) and - # exits permanently on an unmapped host, so the 'localhost:3000' - # community MUST exist before the relay starts — the retry loop only - # handles late-seeded channels, not a late-seeded community. The relay - # migrates at boot via BUZZ_AUTO_MIGRATE, but that's too late for the - # pre-boot seed, so apply the schema here first (then drop AUTO_MIGRATE - # below). lower(host) is the unique index → ON CONFLICT target. psql - # isn't on PATH in hermit → exec into the buzz-postgres container. + - name: Check Desktop E2E Relay result env: - PGHOST: localhost - PGPORT: "5432" - PGUSER: buzz - PGPASSWORD: buzz_dev - PGDATABASE: buzz - # Use the already-running docker postgres for desired-state planning instead of - # downloading an embedded Postgres from Maven Central (transient-fetch flake source). - PGSCHEMA_PLAN_HOST: localhost - PGSCHEMA_PLAN_PORT: "5432" - PGSCHEMA_PLAN_DB: buzz - PGSCHEMA_PLAN_USER: buzz - PGSCHEMA_PLAN_PASSWORD: buzz_dev - run: | - ./bin/pgschema apply --file schema/schema.sql --auto-approve - docker exec -i -e PGPASSWORD=buzz_dev buzz-postgres \ - psql -U buzz -d buzz -v ON_ERROR_STOP=1 < scripts/reconcile-schema-after-pgschema.sql - docker exec -e PGPASSWORD=buzz_dev buzz-postgres \ - psql -U buzz -d buzz -qtA -c " - INSERT INTO communities (id, host) - VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000') - ON CONFLICT (lower(host)) DO NOTHING - ;" - - name: Start relay - run: | - chmod +x ./target/ci/buzz-relay - nohup env \ - DATABASE_URL="postgres://buzz:${BUZZ_TEST_POSTGRES_PASSWORD}@localhost:5432/buzz" \ - REDIS_URL=redis://localhost:6379 \ - RELAY_URL=ws://localhost:3000 \ - BUZZ_BIND_ADDR=0.0.0.0:3000 \ - BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \ - BUZZ_REQUIRE_AUTH_TOKEN=false \ - BUZZ_RECONCILE_CHANNELS=true \ - BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=100000 \ - BUZZ_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN=100000 \ - BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC=10000 \ - BUZZ_GIT_PROBE_WRITERS=8 \ - SPROUT_REMINDER_SCHEDULER_INTERVAL_SECS=1 \ - ./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 & - echo $! > /tmp/buzz-relay.pid - for attempt in $(seq 1 60); do - if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then - cat /tmp/buzz-relay.log - exit 1 - fi - status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true) - if [ "${status_code}" = "200" ]; then - exit 0 - fi - sleep 1 - done - cat /tmp/buzz-relay.log - exit 1 - - name: Seed desktop e2e data - run: bash scripts/setup-desktop-test-data.sh - - name: Desktop relay-backed e2e - run: cd desktop && pnpm exec playwright test --project=integration --shard=${{ matrix.shard }}/2 - - name: Summarize flaky tests - if: ${{ !cancelled() }} - run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop E2E Integration (${{ matrix.shard }}/2)" - working-directory: desktop - - name: Upload desktop integration artifacts - if: ${{ !cancelled() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: desktop-e2e-integration-artifacts-${{ matrix.shard }} - path: | - desktop/playwright-report - desktop/playwright-report.json - desktop/test-results - /tmp/buzz-relay.log - if-no-files-found: ignore - retention-days: 7 - - name: Save pnpm store cache - if: github.event_name == 'push' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + RESULT: ${{ needs.relay-artifacts-domain.outputs.desktop_e2e_relay_result }} + run: test "$RESULT" = success desktop-e2e-integration: name: Desktop E2E Integration + if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') + needs: [changes, relay-artifacts-domain, relay-domain] runs-on: ubuntu-latest timeout-minutes: 5 - needs: [changes, desktop-e2e-integration-shard] - if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') - permissions: - contents: read + permissions: {} steps: - - name: Check integration shards - run: | - if [ "${{ needs.desktop-e2e-integration-shard.result }}" != "success" ]; then - echo "Desktop E2E Integration shards finished with: ${{ needs.desktop-e2e-integration-shard.result }}" - exit 1 - fi - echo "Desktop E2E Integration shards passed" + - name: Check Desktop E2E Integration result + env: + RESULT: ${{ needs.relay-domain.outputs.desktop_e2e_integration_result }} + run: test "$RESULT" = success backend-integration: name: Backend Integration (relay e2e) + if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true') + needs: [changes, relay-artifacts-domain, relay-domain] runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [changes, desktop-e2e-relay] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' - permissions: - contents: read + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Install cargo-nextest - uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 - with: - tool: cargo-nextest@0.9.136 - - name: Start integration services - run: | - for attempt in 1 2 3; do - if docker compose up -d postgres redis minio minio-init; then - break - fi - if [ "$attempt" -eq 3 ]; then - echo "docker compose up failed after 3 attempts" >&2 - exit 1 - fi - echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2 - sleep $((attempt * 5)) - done - - name: Wait for integration services - run: | - wait_healthy() { - local service="$1" - local container="$2" - for attempt in $(seq 1 60); do - status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found") - if [ "${status}" = "healthy" ]; then - echo "${service} is healthy" - return 0 - fi - sleep 2 - done - docker logs "${container}" || true - return 1 - } - wait_healthy "Postgres" "buzz-postgres" - wait_healthy "Redis" "buzz-redis" - wait_healthy "MinIO" "buzz-minio" - - name: Download relay artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: desktop-e2e-relay - path: target/ci - - name: Apply schema and seed deployment community - # MT: the relay resolves each request's tenant from the communities host - # map and fails closed on an unmapped host. The reminder scheduler binds - # the deployment community ONCE at boot and exits permanently on an - # unmapped host (no retry, unlike the channel reconciler), so the - # 'localhost:3000' community MUST exist before the relay starts — seeding - # after boot leaves the scheduler dead. The relay migrates at boot via - # BUZZ_AUTO_MIGRATE, but that's too late for the pre-boot seed, so apply - # the schema here first (then drop AUTO_MIGRATE below). lower(host) is the - # unique index → ON CONFLICT target. psql isn't on PATH in hermit → exec - # into the buzz-postgres container. + - name: Check Backend Integration result env: - PGHOST: localhost - PGPORT: "5432" - PGUSER: buzz - PGPASSWORD: buzz_dev - PGDATABASE: buzz - # Use the already-running docker postgres for desired-state planning instead of - # downloading an embedded Postgres from Maven Central (transient-fetch flake source). - PGSCHEMA_PLAN_HOST: localhost - PGSCHEMA_PLAN_PORT: "5432" - PGSCHEMA_PLAN_DB: buzz - PGSCHEMA_PLAN_USER: buzz - PGSCHEMA_PLAN_PASSWORD: buzz_dev - run: | - ./bin/pgschema apply --file schema/schema.sql --auto-approve - docker exec -i -e PGPASSWORD=buzz_dev buzz-postgres \ - psql -U buzz -d buzz -v ON_ERROR_STOP=1 < scripts/reconcile-schema-after-pgschema.sql - docker exec -e PGPASSWORD=buzz_dev buzz-postgres \ - psql -U buzz -d buzz -qtA -c " - INSERT INTO communities (id, host) - VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000') - ON CONFLICT (lower(host)) DO NOTHING - ;" - - name: Workflow message provenance unit tests - # The relay's workflow_sink suite is not selected by the infra-free - # unit job. Its ignored database cases run in the isolated PostgreSQL - # lane; keep the pure provenance cases covered here without duplication. - run: | - cargo nextest run \ - --archive-file target/ci/backend-integration-tests.tar.zst \ - -E 'package(buzz-relay) and test(/workflow_sink/)' - - name: Start relay - run: | - chmod +x ./target/ci/buzz-relay - nohup env \ - DATABASE_URL="postgres://buzz:${BUZZ_TEST_POSTGRES_PASSWORD}@localhost:5432/buzz" \ - REDIS_URL=redis://localhost:6379 \ - RELAY_URL=ws://localhost:3000 \ - BUZZ_BIND_ADDR=0.0.0.0:3000 \ - BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \ - BUZZ_REQUIRE_AUTH_TOKEN=false \ - BUZZ_RECONCILE_CHANNELS=true \ - BUZZ_GIT_PROBE_WRITERS=8 \ - SPROUT_REMINDER_SCHEDULER_INTERVAL_SECS=1 \ - ./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 & - echo $! > /tmp/buzz-relay.pid - for attempt in $(seq 1 60); do - if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then - cat /tmp/buzz-relay.log - exit 1 - fi - status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true) - if [ "${status_code}" = "200" ]; then - exit 0 - fi - sleep 1 - done - cat /tmp/buzz-relay.log - exit 1 - - name: NIP-ER reminder e2e - # Feature e2e for NIP-ER (Event Reminders, kind:30300): write-path - # validation, author-only read filtering, and scheduler delivery against - # a live relay. The schema-drift / migration-version guarantee is owned - # by the buzz-db migration.rs unit tests, not this suite. - run: | - cargo nextest run \ - --archive-file target/ci/backend-integration-tests.tar.zst \ - -E 'binary(e2e_event_reminder)' \ - --run-ignored ignored-only + RESULT: ${{ needs.relay-domain.outputs.backend_integration_result }} + run: test "$RESULT" = success + + postgres-tests: + name: PostgreSQL Tests + if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true') + needs: [changes, relay-artifacts-domain, postgres-domain] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + - name: Check PostgreSQL Tests result env: - RELAY_URL: ws://localhost:3000 - - name: Upload relay log - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: backend-integration-relay-log - path: /tmp/buzz-relay.log - if-no-files-found: ignore + RESULT: ${{ needs.postgres-domain.outputs.postgres_tests_result }} + run: test "$RESULT" = success relay-e2e: name: Relay E2E + if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true') + needs: [changes, relay-artifacts-domain, relay-domain] runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [changes, desktop-e2e-relay] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' - permissions: - contents: read + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - save-if: ${{ github.event_name != 'pull_request' }} - # Reuse the relay + git-credential-nostr built by Desktop E2E Relay - # instead of compiling them a second time. - - name: Download relay binary - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: desktop-e2e-relay - path: target/ci - - name: Start relay - run: | - chmod +x ./target/ci/buzz-relay ./target/ci/git-credential-nostr - ./scripts/start-relay-for-tests.sh --no-build - - name: Relay E2E tests - run: | - cargo test -p buzz-test-client --test e2e_persona --test e2e_team_catalog --test e2e_nostr_interop --test e2e_project -- --ignored --nocapture - cargo test -p buzz-test-client --test e2e_relay invite -- --ignored --nocapture - cargo test -p buzz-test-client --test e2e_relay nip43_membership_snapshots_are_rejected -- --ignored --nocapture + - name: Check Relay E2E result env: - RELAY_URL: ws://localhost:3000 - GIT_CREDENTIAL_NOSTR_BIN: ${{ github.workspace }}/target/ci/git-credential-nostr - - name: Media read-auth e2e - # Reads require kind:24242 `t=get` auth, so these binaries are the only - # coverage that a real relay rejects bare reads and honours host- and - # hash-scoped tokens. They were #[ignore]d and selected by no CI job, so - # the lane never ran; select it here, where MinIO and the seeded - # 'localhost:3000' community already exist. - # --no-fail-fast: without it cargo stops after the first failing binary, - # so one broken case hides every later binary's result. - run: | - cargo test -p buzz-test-client --no-fail-fast --test e2e_media --test e2e_media_extended --test e2e_media_video -- --ignored --nocapture - env: - RELAY_URL: ws://localhost:3000 - RELAY_HTTP_URL: http://localhost:3000 - - name: Upload relay logs - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: relay-e2e-artifacts - path: /tmp/buzz-relay.log - if-no-files-found: ignore + RESULT: ${{ needs.relay-domain.outputs.relay_e2e_result }} + run: test "$RESULT" = success web: name: Web + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.web == 'true') + needs: [changes, clients] runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.web == 'true' - permissions: - contents: read + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 2 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - 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 dependencies - run: pnpm install --frozen-lockfile - - name: Web lint and format - run: just web-check - - name: Web build - run: just web-build - - name: Save pnpm store cache - if: github.event_name == 'push' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + - name: Check Web result + env: + RESULT: ${{ needs.clients.outputs.web_result }} + run: test "$RESULT" = success mobile: name: Mobile + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.mobile == 'true') + needs: [changes, clients] runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.mobile == 'true' - permissions: - contents: read + timeout-minutes: 5 + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 2 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Compute Hermit cache key - id: hermit-bin-hash - run: | - hash="$(find ./bin ! -type d | sort | xargs openssl sha256 | openssl sha256 -r | cut -d' ' -f1)" - echo "hash=$hash" >> "$GITHUB_OUTPUT" - - name: Restore Hermit package cache - id: hermit-cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ~/.cache/hermit/pkg - key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }} - restore-keys: ${{ runner.os }}-hermit-cache- - - name: Prime Flutter SDK - run: flutter --version - - name: Save Hermit package cache - if: always() && steps.hermit-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - continue-on-error: true - with: - path: ~/.cache/hermit/pkg - key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }} - - name: Restore pub cache - id: pub-cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ~/.pub-cache - key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }} - restore-keys: pub-${{ runner.os }}- - - name: Install dependencies - run: cd mobile && flutter pub get - - name: Save pub cache - if: always() && steps.pub-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - continue-on-error: true - with: - path: ~/.pub-cache - key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }} - - name: Format check - run: cd mobile && dart format --output=none --set-exit-if-changed . - - name: Analyze - run: cd mobile && flutter analyze - - name: Test - run: cd mobile && flutter test - - name: Build Android debug APK - run: just mobile-build-android + - name: Check Mobile result + env: + RESULT: ${{ needs.clients.outputs.mobile_result }} + run: test "$RESULT" = success - mobile-swift: - name: Mobile Swift - runs-on: macos-latest - timeout-minutes: 30 - needs: [changes] - if: needs.changes.outputs.mobile == 'true' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Install Flutter dependencies - run: cd mobile && flutter pub get - - name: Build - run: swift build --package-path mobile/ios/BuzzPushKit - - name: Build release - run: swift build -c release --package-path mobile/ios/BuzzPushKit - - name: Test - run: swift test --package-path mobile/ios/BuzzPushKit - - name: Build complete unsigned iOS release - run: cd mobile && flutter build ios --release --no-codesign --no-pub security: name: Security - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Dependency policy - run: cargo-deny check - - dead-token-guard: - name: Dead Token Reference Guard + if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true') + needs: [changes, security-domain] runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - contents: read + permissions: {} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Check for dead API token references in client code - run: | - # Fail if dead API token patterns reappear in desktop, mobile, docs, or config. - # Relay crates are excluded — they still use token auth internally. - PATTERNS='TokenScope|MintTokenResponse|hasApiToken|spr_tok_' - PATHS='desktop/src/ desktop/tests/ mobile/test/ mobile/lib/ .env.example' - EXCLUDES='--exclude-dir=node_modules --exclude-dir=.dart_tool' - if grep -rn $EXCLUDES -E "$PATTERNS" $PATHS 2>/dev/null; then - echo "::error::Dead API token references found in client code. See above." - exit 1 - fi - echo "No dead token references found." - - server-cross-compile: - name: Server Cross-Compile - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - strategy: - fail-fast: false - matrix: - target: - - x86_64-unknown-linux-musl - - aarch64-unknown-linux-musl - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - key: cross-${{ matrix.target }} - save-if: ${{ github.event_name != 'pull_request' }} - - name: Install cross - uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 - with: - tool: cross@0.2.5 - - name: Build server binaries - env: - TARGET: ${{ matrix.target }} - # PRs: compile + build-script gate only (no codegen/link). Main: full link gate. - CARGO_CMD: ${{ github.event_name == 'pull_request' && 'check' || 'build' }} - run: | - cross "$CARGO_CMD" --release --target "$TARGET" \ - -p buzz-relay \ - -p buzz-acp \ - -p buzz-agent \ - -p buzz-dev-mcp \ - -p git-credential-nostr \ - -p git-sign-nostr - - desktop-windows-build: - name: Desktop Windows Build - runs-on: windows-latest - timeout-minutes: 20 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24.14.1 - package-manager-cache: false - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - with: - version: 11.4.0 - - name: Install desktop dependencies - shell: bash - run: pnpm install --frozen-lockfile - - name: Build both protected-feature selections - shell: pwsh - run: | - Remove-Item Env:VITE_BUZZ_BESTIE -ErrorAction SilentlyContinue - pnpm -C desktop build - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $env:VITE_BUZZ_BESTIE = "1" - pnpm -C desktop build - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - 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 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true' - permissions: - contents: read - env: - TARGET: x86_64-pc-windows-msvc - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - # MSVC needs windows.h (aws-lc-sys et al.), so this runs on a real Windows - # runner — hermit, used by the Linux jobs, does not provide MSVC. The - # toolchain (1.95.0 + clippy via profile = default) comes from the - # repo-root rust-toolchain.toml, which the runner's preinstalled rustup - # honors on demand; the host triple already is x86_64-pc-windows-msvc. - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - workspaces: | - . - desktop/src-tauri - key: windows-msvc - save-if: ${{ github.event_name != 'pull_request' }} - # Tauri validates externalBin at compile time, so the Tauri-crate steps - # below fail without these stubs. Mirrors scripts/bundle-sidecars.sh's - # Windows naming (binaries/-.exe); empty files suffice for a - # type-check since nothing executes them. - - name: Create sidecar placeholders - shell: bash - run: | - mkdir -p desktop/src-tauri/binaries - for bin in buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz; do - touch "desktop/src-tauri/binaries/${bin}-${TARGET}.exe" - done - - 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. - # Serial: windows_resolver_tests mutate process-global env - # (BUZZ_SHELL/GIT_BASH/SystemRoot) that SharedState::new reads. - run: cargo test -p buzz-dev-mcp --target $env:TARGET -- --test-threads=1 - - name: Test (buzz-agent auth coordinator) - # The auth coordinator single-flights on an OS advisory lock, which is - # LockFileEx on Windows; this integration suite drives real second - # processes on the same lock file, so it only exercises the Windows - # lock runtime if it runs ON Windows. Every other job compiles it but - # never executes it. Tests exercised on Windows: lock serialization - # (two coordinators race for the same key), cooldown sidecar sharing - # across processes, attempt-sidecar adoption (UserInitiated waiter - # adopts a predecessor's denial), and the in-process single-flight for - # same-key coalescing. Tests that are UNIX-ONLY and NOT executed here: - # crash-release (flock drop on SIGKILL, guarded by #[cfg(unix)]) and - # cross-process cache success/race (on-disk token handoff, also - # #[cfg(unix)]). - run: cargo test -p buzz-agent --target $env:TARGET --test databricks_auth_coordinator - # Smoke-test the new host-prereq contract: Git for Windows (which provides - # bash) is available on the runner, a shell command round-trips, and bash - # does NOT resolve from System32 (so WSL's launcher is never picked up). - # windows-latest runners have Git for Windows pre-installed; the unit tests - # above exercise the MCP resolver itself. This step verifies the host env. - - name: Smoke-test host Git Bash prereq (host env check) - shell: bash - run: | - set -euo pipefail - # Git for Windows ships bash.exe under its bin/ directory; confirm it - # resolves from the standard location the runtime resolver probes first. - bash_path=$(command -v bash 2>/dev/null || true) - [[ -n "$bash_path" ]] || { echo "ERROR: bash not found on PATH — host Git for Windows missing" >&2; exit 1; } - echo "Resolved bash: $bash_path" - [[ "$bash_path" != *System32* ]] || { echo "ERROR: resolved bash is WSL's System32 launcher" >&2; exit 1; } - - # Run a basic pipeline through the resolved bash (same invocation the - # agent uses: bash -c '...'). - out=$(bash -c 'echo hello | tr a-z A-Z') - [[ "$out" == "HELLO" ]] || { echo "bash pipeline failed: got '$out'" >&2; exit 1; } - - # Confirm git itself works — agents run git commands frequently. - git --version - repo=$(mktemp -d) - cd "$repo" - git init -q - git -c user.name=ci -c user.email=ci@example.com commit -q --allow-empty -m smoke - git log -1 --format=%s | grep -qx smoke - echo "Host bash resolved and functional; git commit round-trip passed" - - name: Check (Tauri crate) - run: cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --target $env:TARGET - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - name: Test (Tauri crate) - run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --target $env:TARGET - env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - - desktop-build-macos: - name: Desktop Build (macOS) - runs-on: macos-latest - timeout-minutes: 45 - needs: [changes] - if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' - permissions: - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 - with: - workspaces: desktop/src-tauri - save-if: ${{ github.event_name != 'pull_request' }} - - name: Install desktop dependencies - run: just desktop-install-ci - - name: Create sidecar placeholders - run: | - TARGET=$(rustc -vV | sed -n 's|host: ||p') - mkdir -p desktop/src-tauri/binaries - touch "desktop/src-tauri/binaries/buzz-acp-$TARGET" - touch "desktop/src-tauri/binaries/buzz-agent-$TARGET" - touch "desktop/src-tauri/binaries/buzz-backend-kubernetes-$TARGET" - touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET" - touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET" - touch "desktop/src-tauri/binaries/buzz-$TARGET" - # Mesh rev is derived from Cargo.lock so a dependency bump needs no - # lockstep edit here; the cache key tracks it automatically. - - name: Resolve mesh-llm rev - id: mesh_rev - run: | - set -euo pipefail - REV=$(python3 -c 'import tomllib; d=tomllib.load(open("Cargo.lock", "rb")); p=next(p for p in d["package"] if p["name"] == "mesh-llm-sdk"); print(p["source"].rsplit("#", 1)[1])') - [[ -n "$REV" ]] || { echo "::error::could not resolve mesh-llm rev from Cargo.lock"; exit 1; } - echo "rev=$REV" >> "$GITHUB_OUTPUT" - echo "short=${REV:0:7}" >> "$GITHUB_OUTPUT" - - name: Restore mesh llama build cache - id: llama_cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} - - name: Build mesh llama native libraries - if: steps.llama_cache.outputs.cache-hit != 'true' - env: - MESH_REV_SHORT: ${{ steps.mesh_rev.outputs.short }} - run: | - set -euo pipefail - cargo fetch --manifest-path desktop/src-tauri/Cargo.toml - SHORT="$MESH_REV_SHORT" - MESH_ROOT=$(find "${CARGO_HOME:-$HOME/.cargo}/git/checkouts" -path "*/$SHORT" -type d -name "$SHORT" | head -1) - if [[ -z "$MESH_ROOT" ]]; then - echo "::error::mesh-llm checkout for $SHORT not found after cargo fetch" - exit 1 - fi - export LLAMA_STAGE_BACKEND=metal - export LLAMA_STAGE_BUILD_DIR="$GITHUB_WORKSPACE/.cache/mesh-llama/build-stage-abi-metal" - export CMAKE_OSX_DEPLOYMENT_TARGET=10.15 - "$MESH_ROOT/scripts/prepare-llama.sh" pinned - "$MESH_ROOT/scripts/build-llama.sh" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 - - name: Save mesh llama build cache - if: steps.llama_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: ${{ github.workspace }}/.cache/mesh-llama - key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }} - - name: Build Tauri app - run: cd desktop && pnpm tauri build + - name: Check Security result env: - CMAKE_POLICY_VERSION_MINIMUM: "3.5" - MACOSX_DEPLOYMENT_TARGET: "10.15" - CMAKE_OSX_DEPLOYMENT_TARGET: "10.15" - LLAMA_STAGE_BACKEND: metal - LLAMA_STAGE_BUILD_DIR: ${{ github.workspace }}/.cache/mesh-llama/build-stage-abi-metal - SKIPPY_LLAMA_AUTO_BUILD: "0" + RESULT: ${{ needs.security-domain.outputs.security_result }} + run: test "$RESULT" = success diff --git a/scripts/test-ci-required-context-isolation.sh b/scripts/test-ci-required-context-isolation.sh new file mode 100755 index 00000000000..dc5c11708ee --- /dev/null +++ b/scripts/test-ci-required-context-isolation.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +orchestrator="$repo_root/.github/workflows/ci.yml" +desktop_workflow="$repo_root/.github/workflows/_ci-desktop.yml" +macos_workflow="$repo_root/.github/workflows/_ci-desktop-macos.yml" +rust_workflow="$repo_root/.github/workflows/_ci-rust.yml" +relay_workflow="$repo_root/.github/workflows/_ci-relay.yml" +clients_workflow="$repo_root/.github/workflows/_ci-clients.yml" + +fail() { + echo "CI required-context isolation contract failed: $*" >&2 + exit 1 +} + +[[ -f "$macos_workflow" ]] || fail "missing _ci-desktop-macos.yml" + +extract_job() { + local job=$1 + local workflow=$2 + awk -v job="$job" ' + $0 == " " job ":" { found = 1 } + found && $0 ~ /^ [a-zA-Z0-9_-]+:$/ && $0 != " " job ":" { exit } + found { print } + ' "$workflow" +} + +desktop_call=$(extract_job desktop-domain "$orchestrator") +macos_call=$(extract_job desktop-macos-domain "$orchestrator") +rust_call=$(extract_job rust "$orchestrator") +rust_cross_compile_call=$(extract_job rust-cross-compile-domain "$orchestrator") +relay_call=$(extract_job relay-domain "$orchestrator") +relay_artifacts_call=$(extract_job relay-artifacts-domain "$orchestrator") +postgres_call=$(extract_job postgres-domain "$orchestrator") +clients_call=$(extract_job clients "$orchestrator") +mobile_swift_call=$(extract_job mobile-swift-domain "$orchestrator") +desktop_gate=$(extract_job desktop "$orchestrator") +macos_gate=$(extract_job desktop-build-macos "$orchestrator") + +[[ "$desktop_call" == *'uses: ./.github/workflows/_ci-desktop.yml'* ]] || + fail "Desktop Domain must call _ci-desktop.yml" +[[ "$macos_call" == *'uses: ./.github/workflows/_ci-desktop-macos.yml'* ]] || + fail "Desktop macOS domain must call _ci-desktop-macos.yml" +[[ "$desktop_gate" == *'needs: [changes, desktop-domain]'* ]] || + fail "Desktop required check must depend only on Desktop Domain" +[[ "$desktop_gate" == *'needs.desktop-domain.outputs.desktop_result'* ]] || + fail "Desktop required check must read the Desktop Domain result" +[[ "$macos_gate" == *'needs: [changes, desktop-macos-domain]'* ]] || + fail "Desktop Build (macOS) required check must depend only on the macOS domain" +[[ "$macos_gate" == *'needs.desktop-macos-domain.outputs.desktop_macos_result'* ]] || + fail "Desktop Build (macOS) required check must read the macOS domain result" + +[[ "$rust_call" == *'lane: required'* && "$rust_cross_compile_call" == *'lane: cross-compile'* ]] || + fail "required Rust checks and server cross-compiles must use isolated calls" +[[ "$relay_artifacts_call" == *'lane: artifacts'* && "$relay_call" == *'lane: required'* && "$postgres_call" == *'lane: postgres'* ]] || + fail "relay artifacts, required checks, and PostgreSQL tests must use isolated calls" +[[ "$relay_call" == *'needs: [changes, relay-artifacts-domain]'* ]] || + fail "required relay checks must wait for the artifact producer" +[[ "$postgres_call" == *'needs: [changes, relay-artifacts-domain]'* ]] || + fail "PostgreSQL tests must wait for the artifact producer" +[[ "$clients_call" == *'lane: required'* && "$mobile_swift_call" == *'lane: mobile-swift'* ]] || + fail "required client checks and Mobile Swift must use isolated calls" + +grep -Fq "inputs.lane == 'cross-compile'" "$rust_workflow" || + fail "Rust cross-compiles must be selected by their isolated lane" +grep -Fq "inputs.lane == 'postgres'" "$relay_workflow" || + fail "PostgreSQL tests must be selected by their isolated lane" +grep -Fq "inputs.lane == 'artifacts'" "$relay_workflow" || + fail "relay artifacts must be selected by their isolated lane" +grep -Fq "inputs.lane == 'mobile-swift'" "$clients_workflow" || + fail "Mobile Swift must be selected by its isolated lane" + +grep -Fq 'on:' "$macos_workflow" || fail "macOS workflow is missing its trigger" +grep -Fq ' workflow_call:' "$macos_workflow" || fail "macOS workflow must use workflow_call" +grep -Fq ' desktop-build-macos:' "$macos_workflow" || fail "macOS build job is missing" +grep -Fq 'desktop_macos_result:' "$macos_workflow" || fail "macOS result output is missing" + +if grep -Eq '^ desktop-build-macos:|desktop_macos_result:' "$desktop_workflow"; then + fail "Desktop Domain must not own the isolated macOS required check" +fi + +echo "CI required-context isolation contract passed" diff --git a/scripts/test-rust-cache-contract-regressions.sh b/scripts/test-rust-cache-contract-regressions.sh index 708c26e77be..0275b873415 100755 --- a/scripts/test-rust-cache-contract-regressions.sh +++ b/scripts/test-rust-cache-contract-regressions.sh @@ -139,7 +139,7 @@ YAML run_contract >/dev/null rm -rf "$tmp/.github/actions" "$tmp/.github/workflows/new-cache-user.yaml" -python3 - "$tmp/.github/workflows/ci.yml" <<'PY' +python3 - "$tmp/.github/workflows/_ci-rust.yml" <<'PY' import pathlib import sys diff --git a/scripts/test-rust-cache-contract.sh b/scripts/test-rust-cache-contract.sh index 30abd026597..cf69c8aadc6 100755 --- a/scripts/test-rust-cache-contract.sh +++ b/scripts/test-rust-cache-contract.sh @@ -91,9 +91,9 @@ cache_uses.each do |path, location, ref| end end -ci_path = root / ".github" / "workflows" / "ci.yml" -ci = workflows.fetch(ci_path) { load_workflow.call(ci_path) } -jobs = ci["jobs"] +rust_ci_path = root / ".github" / "workflows" / "_ci-rust.yml" +rust_ci = workflows.fetch(rust_ci_path) { load_workflow.call(rust_ci_path) } +jobs = rust_ci["jobs"] unit_tests = jobs.is_a?(Hash) ? jobs["unit-tests"] : nil abort "unit-tests job missing" unless unit_tests.is_a?(Hash) steps = unit_tests["steps"]