mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
This PR implements MVP, iOS-only,
[NIP-PL](8d2d0ff5ad/docs/nips/NIP-PL.md)-compliant
push notifications.
A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any
message that appears in the in-app Notifications tab.
## Enrollment flow
The first time the client first connects to a relay with
`BUZZ_PUSH_ENABLED`:
```mermaid
sequenceDiagram
autonumber
participant App as Buzz iOS app
participant iOS
participant Relay as Buzz relay
participant Attest as Apple App Attest
participant Gateway as Push gateway
App->>Relay: Fetch NIP-11 push capability
Relay-->>App: Push profile, current relay public key, and limits
par
App->>iOS: Request notification permission
iOS-->>App: Permission result
and
App->>iOS: Register for remote notifications
iOS-->>App: Device token
end
App->>Gateway: Request installation challenge
Gateway-->>App: Single-use challenge
App->>Attest: Attest installation transcript
Attest-->>App: Attestation proof
App->>Gateway: Enroll device token and proof
Gateway-->>App: Installation handle
App->>Gateway: Request delegation challenge
Gateway-->>App: Single-use challenge
App->>Attest: Assert relay-key delegation
Attest-->>App: Assertion
App->>Gateway: Create delegation
Gateway-->>App: Opaque endpoint grant
App->>Relay: Publish encrypted push lease and filters
Relay-->>App: Lease acknowledged
```
## Push-time flow
When a notification-eligible event is received by the relay:
```mermaid
%%{init: {
"sequence": {
"actorMargin": 20,
"width": 110,
"messageMargin": 18,
"diagramMarginX": 8,
"wrap": true
}
}}%%
sequenceDiagram
autonumber
participant Relay as Buzz relay
participant Gateway as Push gateway
participant APNs as Apple Push<br/>Notification service
participant iOS
participant NSE as Notification service<br/>extension
Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization
Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload
APNs-->>Gateway: 200 OK: request accepted
Gateway-->>Relay: 200 OK: accepted status
APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1
iOS->>NSE: Invoke extension<br/>original notification content
NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization
Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003
NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target
```
relay → push gateway → APNs -> NSE -> Notification Center
## Known limitations
The APNs wake payload is intentionally constant and opaque: it contains
no originating community or message identifier, in keeping with the
implemented NIP-PL privacy design.
The Notification Service Extension must therefore reconnect to the relay
and resolve eligible messages after each wake. Around overlapping wakes,
timing boundaries, or resolution windows, notification presentation may
occasionally omit an expected message or display a message more than
once.
This best-effort behavior is deliberately accepted for the current
implementation and will be measured during the internal rollout to
determine whether the user experience is acceptable before any broader
deployment; the implementation does not claim exactly-once presentation.
## Validation
Live end-to-end hardware validation used an internal remotely hosted
development relay and push gateway, the APNs sandbox, and a physical
iPhone 12 mini:
- A second real Buzz client published a uniquely marked message through
the hosted relay.
- The relay matched the message and sent the constant opaque wake
through the hosted gateway. The gateway made an actual APNs request; no
`simctl push` or simulated notification was used.
- The iPhone received the notification on its lock screen. The
Notification Service Extension reconnected to the relay, fetched the
event, verified its ID and signature, and replaced the placeholder
content with the real notification title and body.
- After the app populated its shared presentation cache, a final marked
notification visibly showed the sender display name, sender avatar, and
hashtag-prefixed channel name.
- Tapping a lock-screen notification opened Buzz and exercised the
notification-response path and navigated to the corresponding message.
Final validation with a dogfood-signed artifact and production App
Attest/APNs configuration remains a release step.
## Independent pre-reviews
- **First pass:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585)
found missing transient retries, executor-key rotation suppression,
duplicate installation renewal, and an unauthenticated challenge write
amplifier. These were resolved by [retry-safe
bootstrap](https://github.com/block/buzz/pull/6269/changes/12c66ea62)
and [authenticated renewal plus a cross-replica
quota](https://github.com/block/buzz/pull/6269/changes/8e5ece0bd).
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962)
found delegation generation burning and an edited applied migration,
resolved by [exact-generation
revocation](https://github.com/block/buzz/pull/6269/changes/c26d2159d)
and a [forward-only
migration](https://github.com/block/buzz/pull/6269/changes/956c1d099).
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1)
found no blockers.
- **Exact-head re-review:**
[Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68),
[sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849),
and
[k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb)
independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also
revalidated every remediation and the endpoint-specific App Attest
enrollment bound.
---------
Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Jordan Mecom <jm@squareup.com>
1259 lines
59 KiB
YAML
1259 lines
59 KiB
YAML
name: CI
|
|
on:
|
|
push:
|
|
branches: [main, release]
|
|
pull_request:
|
|
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev
|
|
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
|
|
|
|
jobs:
|
|
changes:
|
|
name: Detect Changed Paths
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 2
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
outputs:
|
|
rust: ${{ steps.filter.outputs.rust }}
|
|
desktop: ${{ steps.filter.outputs.desktop }}
|
|
desktop-rust: ${{ steps.filter.outputs.desktop-rust }}
|
|
web: ${{ steps.filter.outputs.web }}
|
|
mobile: ${{ steps.filter.outputs.mobile }}
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
fetch-depth: 2
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
|
|
id: filter
|
|
with:
|
|
token: ''
|
|
filters: |
|
|
rust:
|
|
- 'crates/**'
|
|
- 'migrations/**'
|
|
- 'schema/**'
|
|
- 'Cargo.toml'
|
|
- 'Cargo.lock'
|
|
- 'rust-toolchain.toml'
|
|
- 'deny.toml'
|
|
- '.github/workflows/ci.yml'
|
|
- 'scripts/run-tests.sh'
|
|
- 'scripts/model-capabilities.json'
|
|
- 'scripts/normative-corpus.json'
|
|
- 'justfile'
|
|
desktop:
|
|
- 'scripts/model-capabilities.json'
|
|
- 'scripts/normative-corpus.json'
|
|
- 'desktop/**'
|
|
- '!desktop/src-tauri/**'
|
|
- 'pnpm-lock.yaml'
|
|
desktop-rust:
|
|
- 'desktop/src-tauri/**'
|
|
web:
|
|
- 'web/**'
|
|
- 'pnpm-lock.yaml'
|
|
mobile:
|
|
- 'mobile/**'
|
|
- 'scripts/mobile-release.sh'
|
|
- 'scripts/mobile-worktree-overrides.sh'
|
|
- 'scripts/mobile-worktree-clean.sh'
|
|
- 'scripts/publish-mobile-release-candidate.sh'
|
|
- 'scripts/release-rulesets.sh'
|
|
- 'scripts/test-mobile-release-contract.sh'
|
|
- 'scripts/test-mobile-release-candidate-publisher.sh'
|
|
- 'scripts/test-mobile-worktree-overrides.sh'
|
|
- '.github/workflows/mobile-release-candidate.yml'
|
|
- '.github/workflows/ci.yml'
|
|
- name: Release workflow source contract
|
|
run: scripts/test-release-ref-contract.sh
|
|
- name: Relay image eligibility contract
|
|
run: scripts/test-relay-image-eligibility-workflow.sh
|
|
- name: Desktop release candidate contract
|
|
run: scripts/test-desktop-release-candidate.sh
|
|
- name: OSS desktop promotion contract
|
|
run: |
|
|
scripts/test-oss-desktop-promotion.sh
|
|
scripts/test-oss-desktop-promotion-behavior.sh
|
|
- name: Mobile release contract
|
|
run: |
|
|
scripts/test-mobile-release-contract.sh
|
|
scripts/test-mobile-release-candidate-publisher.sh
|
|
- name: Mobile worktree identity contract
|
|
run: scripts/test-mobile-worktree-overrides.sh
|
|
- name: Codex security review contract
|
|
run: just security-review-check
|
|
- name: Rust cache contract
|
|
run: |
|
|
scripts/test-rust-cache-contract.sh
|
|
scripts/test-rust-cache-contract-regressions.sh
|
|
- name: File size policy
|
|
run: just file-size-check
|
|
|
|
rust-lint:
|
|
name: Rust Lint
|
|
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'
|
|
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
|
|
|
|
unit-tests:
|
|
name: Unit Tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
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
|
|
|
|
desktop-core:
|
|
name: Desktop Core
|
|
runs-on: ubuntu-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
|
|
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') }}
|
|
|
|
desktop-smoke-e2e:
|
|
name: Desktop Smoke E2E (${{ matrix.shard }})
|
|
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'
|
|
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: [changes, desktop-core, desktop-smoke-e2e]
|
|
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
|
|
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
|
|
echo "Desktop jobs passed"
|
|
|
|
desktop-e2e-relay:
|
|
name: Desktop E2E Relay
|
|
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' }}
|
|
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
|
|
key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.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'
|
|
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
|
|
- 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
|
|
key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.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
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
desktop-e2e-integration-shard:
|
|
name: Desktop E2E Integration (${{ matrix.shard }}/2)
|
|
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
|
|
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: [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
|
|
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
|
|
needs: [changes, desktop-e2e-relay]
|
|
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: 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: Replaceable persistence PostgreSQL tests
|
|
# Transaction, concurrency, and mention-index coverage for the
|
|
# replaceable-event store seam. These tests require real Postgres and
|
|
# are ignored by the infrastructure-free unit-test job.
|
|
run: |
|
|
filter='package(buzz-db) and test(/tests::(parameterized_|concurrent_parameterized_)/)'
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E "${filter}" \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Database pressure observability PostgreSQL tests
|
|
# Explicit pool acquisition and advisory-lock metrics require real
|
|
# Postgres and are ignored by the infrastructure-free unit-test job.
|
|
run: |
|
|
filter='package(buzz-db) and test(/observability::tests::(pool_acquire_records_success_timeout_and_error_with_wait_time|advisory_lock_records_success_contention_timeout_and_error)/)'
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E "${filter}" \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- 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: Invite security tests
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E '(package(buzz-db) and test(/relay_invite::tests/)) or (package(buzz-relay) and test(/api::invites::tests/))' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Workspace profile (kind:9033) gate tests
|
|
# Call-site integration for the 9033 authorization gate: open relay
|
|
# rosterless/steward transitions and the closed-relay admin/owner rule,
|
|
# against real Postgres. #[ignore]d in the default suite, selected
|
|
# explicitly here — see handlers::relay_admin::tests.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'package(buzz-relay) and test(/handlers::relay_admin::tests/)' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- 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: NIP-MP coordinate deletion guard
|
|
# Verifies the never-delete-newer invariant of soft_delete_by_coordinate:
|
|
# a stale tombstone (created_at earlier than the live head) spares that
|
|
# head, and an equal-timestamp tombstone deletes it.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'package(buzz-db) and test(coordinate_delete_spares_head_newer_than_the_deletion)' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Admin API nip98 read-write attribution test
|
|
# The only real HTTP → nip98 operator principal → mutation → cross-table
|
|
# attribution coverage: an authenticated operator's dismiss attributes
|
|
# to the operator's own key with relay_operator authority. Staffing
|
|
# PUT/DELETE attribution is covered by
|
|
# nip98_staffing_put_and_delete_write_attributed_audit_rows in the
|
|
# roster-audit lane below. #[ignore]d in the default suite — see
|
|
# api::admin::tests::nip98_operator_dismiss_succeeds_attributed_to_operator.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'package(buzz-relay) and test(=api::admin::tests::nip98_operator_dismiss_succeeds_attributed_to_operator)' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Admin API unrostered-signer replay invariant
|
|
# The only causal proof that a validly-signing but unrostered key cannot
|
|
# consume NIP-98 replay slots: it asserts principal resolution fails
|
|
# BEFORE the replay ID is claimed (tracking.claim_count() == 0). This
|
|
# test is non-ignored, so it runs neither in Backend Integration's
|
|
# ignored-only selectors nor in the infra-free unit job — the unit job's
|
|
# api::admin selector excludes it because DB-free it only passes by
|
|
# waiting out the ~30s sqlx acquire timeout on a read-route fallthrough.
|
|
# It lives here so a reachable Postgres resolves (and fails) the lookup
|
|
# fast instead of timing out.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'package(buzz-relay) and test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)'
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Admin API roster-audit / timeout / canonicalization security tests
|
|
# Security-review fixes for the roster admin API, all #[ignore]d in the
|
|
# default suite (they need Postgres) and selected by no other job:
|
|
# - buzz-db relay_operators::tests: audit pre-image trail, per-target
|
|
# lock serialization, insertion-time audit ordering, and
|
|
# audit-failure rollback coupling.
|
|
# - buzz-db relay_operators::tests last-operator invariant: sole DB
|
|
# operator cannot self-demote or self-delete to zero, config presence
|
|
# lifts the guard, and concurrent cross-target deletes racing to zero
|
|
# leave exactly one operator (roster-wide advisory lock).
|
|
# - buzz-relay api::admin: NIP-98 staffing writes attributed audit rows,
|
|
# adversarial expirationSecs rejected at the resolve route, mixed-case
|
|
# staffing normalizes to one canonical row.
|
|
#
|
|
# --test-threads=1: the last-operator invariant counts the roster
|
|
# globally, and the sole-operator tests clear the roster then assert
|
|
# their operator is the only one. They must not race each other on the
|
|
# shared test roster, so this lane runs serially.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
--test-threads=1 \
|
|
-E '(package(buzz-db) and test(=relay_operators::tests::roster_mutations_write_pre_image_audit_rows)) or (package(buzz-db) and test(=relay_operators::tests::concurrent_upserts_serialize_and_record_true_pre_image)) or (package(buzz-db) and test(=relay_operators::tests::audit_order_follows_seq_under_backward_clock)) or (package(buzz-db) and test(=relay_operators::tests::audit_insert_failure_rolls_back_roster_mutation)) or (package(buzz-db) and test(=relay_operators::tests::demoting_sole_db_operator_without_config_is_rejected)) or (package(buzz-db) and test(=relay_operators::tests::deleting_sole_db_operator_without_config_is_rejected)) or (package(buzz-db) and test(=relay_operators::tests::config_present_allows_deleting_last_db_operator)) or (package(buzz-db) and test(=relay_operators::tests::concurrent_deletes_racing_to_zero_leave_one_operator)) or (package(buzz-relay) and test(=api::admin::tests::nip98_staffing_put_and_delete_write_attributed_audit_rows)) or (package(buzz-relay) and test(=api::admin::tests::resolve_route_rejects_adversarial_expiration_and_leaves_report_open)) or (package(buzz-relay) and test(=api::admin::tests::mixed_case_non_config_staffing_normalizes_to_one_row))' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Admin API escalation-scoping tests
|
|
# Escalation scoping for the moderation queue, all #[ignore]d (they need
|
|
# Postgres) and selected by no other job:
|
|
# - GET /reports defaults to the escalated-only backstop, scope=all
|
|
# restores full visibility, explicit status= overrides the default.
|
|
# - member reports with category 'illegal' auto-escalate at ingestion
|
|
# while every other category still lands 'open'.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E '(package(buzz-relay) and test(=api::admin::tests::reports_default_lists_escalated_only)) or (package(buzz-relay) and test(=api::admin::tests::reports_scope_all_lists_every_status)) or (package(buzz-relay) and test(=api::admin::tests::reports_explicit_status_filter_overrides_default)) or (package(buzz-db) and test(=moderation::tests::illegal_report_auto_escalates_at_ingest)) or (package(buzz-db) and test(=moderation::tests::non_illegal_report_lands_open_at_ingest)) or (package(buzz-db) and test(=relay_admin_actions::tests::auto_escalated_report_reopens_like_an_admin_escalated_one))' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- 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
|
|
needs: [changes, desktop-e2e-relay]
|
|
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: 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
|
|
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
|
|
|
|
web:
|
|
name: Web
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.web == 'true'
|
|
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
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.mobile == 'true'
|
|
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: 10
|
|
needs: [changes]
|
|
if: needs.changes.outputs.mobile == 'true'
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
- 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
|
|
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
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
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
|
|
|
|
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/<bin>-<triple>.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
|
|
# 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
|
|
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"
|