mirror of
https://github.com/block/buzz.git
synced 2026-11-10 11:21:05 +00:00
The mobile app can now be built as a public build or an internal build, set explicitly with `BUZZ_DISTRIBUTION=internal|public`. Each build trusts exactly one Builderlab (`BUZZ_BUILDERLAB_URL`), and sends that Builderlab's relay assertions only to the relays in `BUZZ_BUILDERLAB_RELAYS`. The three settings are set together or not at all; with none set, the app is a Nostr-only development build. The decision is recorded in `docs/adr/0001-internal-and-public-mobile-builds.md`, and the terms are defined in `CONTEXT.md`. Before this change, one build supported a public Builderlab and an organization Builderlab at the same time, with two sessions. An organization's members could reach public communities from the organization's app. ### Behavior - **Public build:** QR pairing, email sign-in and multiple communities are unchanged. It has no knowledge of any organization. An organization's relay already rejects clients that can't present its Builderlab's assertion, and the public build relies on that. - **Internal build:** the first screen has a single **Sign in** button that goes straight to the organization's single sign-on and then joins the organization's community. There is no QR pairing, community link or community creation, and the community switcher is hidden. Only the exact relay hosts listed in `BUZZ_BUILDERLAB_RELAYS` are reachable. - **Identity (internal):** a member whose account has no Nostr identity yet gets a new key on first sign-in; the first relay assertion binds it, and the app backs it up to the organization automatically. A member whose account is already bound uses that key from this device, or restores it from the organization's backup. If neither is possible, Sign in says it couldn't restore the identity and saves nothing. - **Sign-out (internal):** a destructive **Sign out** row at the bottom of Settings, the build's only way out. It removes the community, closes Settings and returns to Sign in. The Builderlab session is cleared from storage immediately and can never be reused to sign back in. The relay push-stop keeps the retired credential in memory only, for up to 10 minutes, and uses it only to get an assertion for a key that is already bound. It is a best-effort request. - **Upgrade to an internal build:** a saved public Builderlab session is cleared. Communities on other relays are hidden, not deleted, and their keys stay on the device. - `BUZZ_HOSTED_RELAY_AUTH` is unchanged here. ### Screenshots (internal build) | Sign in | Community menu | Settings | | --- | --- | --- | |  |  |  | Gateway-side push revocation, which needs no Builderlab session, is planned as a separate PR. ### Validation Added tests: - An internal-build journey: one Sign in creates and binds a key, and a retry after a cancelled join reuses it. The app uploads that key's backup itself. Sign out from Settings closes it and returns to the landing, which ends the session, and Sign in then restores the same identity from the backup. Communities on other relays stay stored but hidden. - Internal Sign in for a bound account: the key on this device, a key restored from backup, and no backup (clear error, nothing saved). Each guard was shown to fail when removed. - Organization relay assertions: assertions go only to exact listed hosts, not subdomains. After sign-out, only a push stop for a bound key gets an assertion, and nothing can sign back in. Proven to fail without the fix. - Builderlab build setting parsing. CI runs the internal and public journeys in their own lanes with those settings. The full suite passes in the default configuration. The full suite isn't meant to run under the internal configuration, because most fixtures use relays that an internal build hides. Automated full-app run on an iPhone 17 Pro simulator at the current head: - Public build, upgraded in place over a signed-in install: home loads, a channel opens and a message sends. - Internal build: a fresh install shows only Sign in. With a saved organization community, the community menu has no Switch community, and Settings → Sign out → confirm returns to Sign in. Real single sign-on can't run in the simulator. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
300 lines
12 KiB
YAML
300 lines
12 KiB
YAML
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 lanes run in parallel; the required check reads the combined
|
|
# matrix result. Test shards split files, so each compiles only its own.
|
|
mobile:
|
|
name: Mobile (${{ matrix.name }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
if: inputs.lane == 'required' && (github.event_name == 'push' || inputs.mobile)
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: Checks
|
|
lane: checks
|
|
- name: Tests 1/3
|
|
lane: test
|
|
shard: 0
|
|
- name: Tests 2/3
|
|
lane: test
|
|
shard: 1
|
|
- name: Tests 3/3
|
|
lane: test
|
|
shard: 2
|
|
- name: Android
|
|
lane: android
|
|
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
|
|
if: matrix.lane == 'checks'
|
|
run: cd mobile && dart format --output=none --set-exit-if-changed .
|
|
- name: Analyze
|
|
if: matrix.lane == 'checks'
|
|
run: cd mobile && flutter analyze
|
|
- name: Test
|
|
if: matrix.lane == 'test'
|
|
run: /bin/bash scripts/mobile-test-shard.sh ${{ matrix.shard }} 3 --dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example
|
|
- name: Test native secp256k1 package
|
|
if: matrix.lane == 'checks'
|
|
run: cd mobile/packages/buzz_secp256k1 && dart pub get && dart test
|
|
- name: Test internal build
|
|
if: matrix.lane == 'checks'
|
|
run: >-
|
|
cd mobile && flutter test
|
|
--dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example
|
|
--dart-define=BUZZ_DISTRIBUTION=internal
|
|
--dart-define=BUZZ_BUILDERLAB_URL=https://id.example.org
|
|
--dart-define=BUZZ_BUILDERLAB_RELAYS=buzz.example.org
|
|
test/features/onboarding/internal_build_entry_test.dart
|
|
- name: Test public build
|
|
if: matrix.lane == 'checks'
|
|
run: >-
|
|
cd mobile && flutter test
|
|
--dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example
|
|
--dart-define=BUZZ_DISTRIBUTION=public
|
|
--dart-define=BUZZ_BUILDERLAB_URL=https://app.builderlab.xyz
|
|
--dart-define=BUZZ_BUILDERLAB_RELAYS=communities.buzz.xyz
|
|
test/shared/auth/community_entry_requirements_test.dart
|
|
test/features/agents/agent_creation_link_journey_test.dart
|
|
test/features/invites/invite_join_provider_test.dart
|
|
test/features/onboarding/onboarding_entry_routes_test.dart
|
|
test/shared/hosted/self_hosted_entry_test.dart
|
|
test/features/agents/agents_signed_out_prompt_test.dart
|
|
test/features/agents/agents_restore_prompt_test.dart
|
|
test/shared/hosted/relay_auth_isolation_test.dart
|
|
test/features/onboarding/hosted_app_gate_environment_test.dart
|
|
test/features/onboarding/hosted_journeys_test.dart
|
|
test/features/onboarding/onboarding_sign_out_test.dart
|
|
test/features/onboarding/hosted_communities_test.dart
|
|
test/features/pairing/pairing_hosted_handoff_test.dart
|
|
test/shared/agents/remote_agents_test.dart
|
|
test/features/agents
|
|
- name: Test mobile without push configuration
|
|
if: matrix.lane == 'checks'
|
|
run: cd mobile && flutter test test/shared/push/push_unconfigured_build_test.dart
|
|
- name: Test mobile recipe argument forwarding
|
|
if: matrix.lane == 'checks'
|
|
run: /bin/bash scripts/test-mobile-gateway-recipes.sh
|
|
# ubuntu-latest (26.04) defaults to JDK 25, which Gradle 8.14 does not
|
|
# support. Pin the JDK 17 the 24.04 image used.
|
|
- name: Set up JDK 17
|
|
if: matrix.lane == 'android'
|
|
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
|
|
with:
|
|
distribution: temurin
|
|
java-version: "17"
|
|
- name: Restore Gradle cache
|
|
if: matrix.lane == 'android'
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-${{ runner.os }}-${{ hashFiles('mobile/android/**/*.gradle*', 'mobile/android/gradle/wrapper/gradle-wrapper.properties', 'mobile/pubspec.lock') }}
|
|
restore-keys: gradle-${{ runner.os }}-
|
|
# The Hermit package cache above holds only Flutter, so the build's first
|
|
# just and cmake calls download them.
|
|
- name: Fetch Hermit tools
|
|
if: matrix.lane == 'android'
|
|
run: |
|
|
scripts/ci-hermit-retry.sh ./bin/just --version
|
|
scripts/ci-hermit-retry.sh ./bin/cmake --version
|
|
# SDK component and plugin downloads intermittently fail on hosted
|
|
# runners; retry once only for those, so real build failures still fail.
|
|
- name: Build Android debug APK
|
|
if: matrix.lane == 'android'
|
|
run: |
|
|
set -o pipefail
|
|
log="$RUNNER_TEMP/android-build.log"
|
|
if ! just mobile-build-android 2>&1 | tee "$log"; then
|
|
grep -qE 'InstallFailedException|Could not (resolve|GET|download)' "$log"
|
|
just mobile-build-android
|
|
fi
|
|
- name: Test Android JVM units
|
|
if: matrix.lane == 'android'
|
|
run: cd mobile/android && ./gradlew :app:testDebugUnitTest --console=plain
|
|
- name: Save Gradle cache
|
|
if: matrix.lane == 'android' && github.event_name == 'push'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
continue-on-error: true
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-${{ runner.os }}-${{ hashFiles('mobile/android/**/*.gradle*', 'mobile/android/gradle/wrapper/gradle-wrapper.properties', 'mobile/pubspec.lock') }}
|
|
|
|
# The unsigned release build needs nothing from the native lane, so it runs
|
|
# beside it instead of after the simulator tests.
|
|
mobile-swift:
|
|
name: Mobile Swift (${{ matrix.name }})
|
|
runs-on: macos-latest
|
|
timeout-minutes: 45
|
|
if: inputs.lane == 'mobile-swift' && inputs.mobile
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: Native
|
|
lane: native
|
|
- name: Release
|
|
lane: release
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Install Flutter dependencies
|
|
run: cd mobile && flutter pub get
|
|
# `swift test` builds BuzzPushKit; the Release lane covers its release build.
|
|
- name: Test
|
|
if: matrix.lane == 'native'
|
|
run: swift test --package-path mobile/ios/BuzzPushKit
|
|
- name: Test native pagination geometry
|
|
if: matrix.lane == 'native'
|
|
run: /bin/bash scripts/test-mobile-pagination.sh
|
|
- name: Generate iOS build configuration
|
|
if: matrix.lane == 'native'
|
|
run: cd mobile && flutter build ios --simulator --debug --no-pub --config-only
|
|
# Build the complete simulator app and RunnerTests once, in the
|
|
# background, while the simulator boots and runs the native-control
|
|
# scripts. The build also guards Apple Silicon simulator linking (#7646).
|
|
- name: Test native controls and Runner unit tests
|
|
if: matrix.lane == 'native'
|
|
run: |
|
|
device=$(xcrun simctl create "Native tests" "iPhone 16")
|
|
trap 'xcrun simctl shutdown "$device"; xcrun simctl delete "$device"' EXIT
|
|
xcode=(xcodebuild -workspace mobile/ios/Runner.xcworkspace -scheme Runner
|
|
-configuration Debug -destination "platform=iOS Simulator,id=$device"
|
|
CODE_SIGNING_ALLOWED=NO)
|
|
build_log="$RUNNER_TEMP/runner-build.log"
|
|
"${xcode[@]}" build-for-testing > "$build_log" 2>&1 &
|
|
build=$!
|
|
xcrun simctl boot "$device"
|
|
xcrun simctl bootstatus "$device" -b
|
|
/bin/bash scripts/test-mobile-pagination-accessibility.sh "$device"
|
|
/bin/bash scripts/test-mobile-carousel.sh "$device"
|
|
/bin/bash scripts/test-mobile-member-picker.sh "$device"
|
|
if ! wait "$build"; then
|
|
cat "$build_log"
|
|
exit 1
|
|
fi
|
|
tail -n 5 "$build_log"
|
|
"${xcode[@]}" -parallel-testing-enabled NO -only-testing:RunnerTests \
|
|
test-without-building
|
|
- name: Build complete unsigned iOS release
|
|
if: matrix.lane == 'release'
|
|
run: |
|
|
/bin/bash scripts/test-mobile-gateway-recipes.sh
|
|
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"
|