This document describes the CI/CD workflow, build artifacts, release process, and deployment procedures for the Ethos-Protocol mobile apps.
Security: Found a vulnerability? Please read our Security Policy and report it privately to security@ethos-protocol.app — do not open a public issue. A
security.txt(RFC 9116) is published at/.well-known/security.txtand will also be served fromhttps://ethos-protocol.app/.well-known/security.txtonce the domain is configured.
Ethos-Protocol mobile apps (iOS + Android) provide a native interface for managing vaults, checking in, and receiving expiry reminders. Both apps share the same REST API contract and feature set.
mobile/
├── shared/
│ └── api-contract.md # Shared API spec (iOS + Android)
├── ios/EthosProtocol/
│ └── Sources/
│ ├── App/ # Entry point, app lifecycle
│ ├── Models/ # Vault, AuthToken, etc.
│ ├── Services/
│ │ ├── APIClient.swift # Ktor-style async HTTP client
│ │ ├── PasskeyService.swift # ASAuthorization / WebAuthn
│ │ ├── KeychainService.swift# Secure token storage
│ │ ├── NotificationService.swift # APNs + local reminders
│ │ └── OfflineSupport.swift # NetworkMonitor + disk cache
│ ├── ViewModels/ # AuthStore, VaultStore (ObservableObject)
│ └── Views/ # SwiftUI screens
└── android/app/src/main/java/com/ethosprotocol/
├── api/
│ ├── ApiClient.kt # Ktor HTTP client
│ └── Infrastructure.kt # NetworkMonitor, OfflineCache, TokenProvider
├── models/ # Kotlinx.serialization data classes
├── services/
│ ├── PasskeyService.kt # CredentialManager / WebAuthn
│ ├── PushService.kt # Firebase Messaging
│ └── NotificationHelper.kt# Local notification display
├── ui/
│ ├── ViewModels.kt # AuthViewModel, VaultViewModel (Hilt)
│ ├── MainActivity.kt # NavHost entry point
│ ├── screens/Screens.kt # Compose screens
│ └── theme/Theme.kt # Material3 dynamic color
└── di/AppModule.kt # Hilt DI bindings
- iOS:
ASAuthorizationPlatformPublicKeyCredentialProvider(iOS 16+) - Android:
CredentialManagerAPI (Android 9+, API 28+) - Flow:
getChallenge()→ device biometric prompt →verifyPasskey()→ JWT stored in Keychain/SharedPreferences - Relying party:
ethos-protocol.app(requires.well-known/assetlinks.json+ Apple App Site Association)
- iOS: APNs via
UNUserNotificationCenter. Device token registered to backend on first launch.- Local reminders scheduled 24h before vault expiry via
UNTimeIntervalNotificationTrigger - Actionable notification category
CHECK_INwith inline "Check In" action
- Local reminders scheduled 24h before vault expiry via
- Android: Firebase Cloud Messaging (FCM). Token refreshed via
onNewToken.- Notification channel
ttl_reminders(IMPORTANCE_HIGH) - Deep-link intent to
MainActivitywithvault_idextra
- Notification channel
NetworkMonitorchecks live connectivity before every requestOfflineCachestores last successful GET responses keyed by URL (SHA-256 filename)- On network unavailable: cached data served transparently; mutations show "offline" error
- iOS:
CryptoKit.SHA256for cache keys; Android:MessageDigest("SHA-256") - Offline check-in queue: a check-in made while offline is queued for retry rather
than just failing.
- iOS:
PendingCheckInStore(disk-backed JSON) is the sole insertion point;CheckInSyncTaskdrains it via aBGProcessingTaskonce connectivity returns. This is the only check-in queue implementation — an earlier duplicate (CheckInQueue/CheckInSyncService) was removed in 8d8d59d; seePendingCheckInStoreTests/CheckInSyncTaskTestsfor the regression guard. - Android:
PendingActionDao/PendingActionDatabase(Room), drained byPendingActionSyncWorker(WorkManager)
- iOS:
- iOS:
@StateObject/ObservableObjectstores (AuthStore,VaultStore) injected via SwiftUI environment - Android: Hilt-injected
ViewModels withStateFlow+collectAsStateWithLifecycle
- Install XcodeGen (
brew install xcodegen) — the.xcodeprojis generated, not checked in - From
ios/EthosProtocol, runmkdir -p Xcode && xcodegen generate --project Xcodeto produceXcode/EthosProtocol.xcodeproj(anEthosProtocolapp target +TTLWidgetwidget extension, perproject.yml) — theXcode/directory must exist beforexcodegen generateruns, or the copy step fails - Open
ios/EthosProtocol/Xcode/EthosProtocol.xcodeprojin Xcode 15+ - Set your Apple Developer Team in signing settings for both the
EthosProtocolandTTLWidgettargets (project.ymlleavesDEVELOPMENT_TEAMblank on purpose — bundle IDscom.ethosprotocol/com.ethosprotocol.TTLWidgetare already set) API_BASE_URLis already set inEthosProtocol/Info.plistandTTLWidget/Info.plist; edit both (they're separate bundles, read independently at runtime) if you need to point at a different environment- Certificate pinning is not active by default: both
Info.plists declareTLS_PUBLIC_KEY_PINSas the$(TLS_PUBLIC_KEY_PIN_CURRENT)/$(TLS_PUBLIC_KEY_PIN_BACKUP)build settings (declared blank inproject.yml), andPinningDelegateignores blank/unexpanded entries. Set both settings to Base64-encoded SPKI SHA-256 hashes — in Xcode's build settings, an.xcconfig, or on thexcodebuildinvocation (xcodebuild … TLS_PUBLIC_KEY_PIN_CURRENT=… TLS_PUBLIC_KEY_PIN_BACKUP=…) — before shipping a Release build; seeSources/Services/CertificatePinning.swiftfor the rotation strategy.ios-ci.yml'sbuild-and-testjob runs.github/scripts/check_tls_pinning.py --configuration Releaseagainst both files and fails the build if either is missing or empty; Debug builds are exempt (PinningDelegateintentionally treats an empty pin set as "pinning disabled" for local dev) - Configure Apple App Site Association at
https://ethos-protocol.app/.well-known/apple-app-site-association, listing this app's App ID under bothapplinks(Universal Links) andwebcredentials(platform passkeys). CI automatically verifies this file daily and on any change toEthosProtocol.entitlements(seeios-applinks-verify.yml). - In the Apple Developer portal, enable Push Notifications, Associated Domains, iCloud (Key-Value storage), and Keychain Sharing capabilities for the
com.ethosprotocolApp ID, and Keychain Sharing forcom.ethosprotocol.TTLWidget— matchingEthosProtocol/EthosProtocol.entitlements/TTLWidget/TTLWidget.entitlements. Set up an APNs key in App Store Connect for push. - Re-run
mkdir -p Xcode && xcodegen generate --project Xcodeany timeproject.ymlchanges; the generatedXcode/directory is disposable and shouldn't be committed
- Open
androidin Android Studio Hedgehog+ - Add
google-services.jsonfrom Firebase Console - Configure
assetlinks.jsonathttps://ethos-protocol.app/.well-known/assetlinks.json. CI automatically verifies this file daily and on any change toAndroidManifest.xml(seeandroid-applinks-verify.yml). - Set
API_BASE_URLinbuild.gradle.ktsbuildConfigField - Configure the certificate pins for release builds by setting
ETHOS_CERT_PINS(environment variable) orethos.certPins(in~/.gradle/gradle.properties, never committed) to a comma-separated list of Base64 SHA-256 SPKI digests — the current certificate's pin plus a backup for the next one. This is required before any release build: pinning is not active by default. When unset,CertificatePinner's pin set is empty — pinning is disabled and the system trust store decides (#169), so there is no compiled-in pin that could reject every real certificate. CI'sVerify release certificate pins are not placeholdersstep reports an unconfigured release build (#173), and fails it outright once the pins are configured but wrong, or once release signing is configured (i.e. the artifact is actually shippable). Debug builds are not gated, since an empty pin set disables pinning for local/dev hosts. Compute a pin with:openssl s_client -connect api.ethos-protocol.app:443 2>/dev/null \ | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \ | openssl dgst -sha256 -binary | openssl enc -base64
cd ios/EthosProtocol
swift testCovers: model decoding, Keychain round-trip, offline cache, Base64URL encoding.
Tests run against the SPM package (Package.swift) directly and don't require the
XcodeGen-generated project; CI runs this the same way, via xcodebuild test against
an iOS Simulator destination (swift test alone defaults to macOS, which can't build
the app's iOS-only framework imports).
CI runs a weekly (and per-PR on Package.swift / Package.resolved changes) vulnerability
scan against all pinned SPM dependencies using osv-scanner,
querying the OSV database. The scan fails the build for any dependency
with a published CVE at CVSS ≥ 7.0 (high or critical). This mirrors the Android
android-dependency-check.yml OWASP scan.
Workflow: .github/workflows/ios-dependency-check.yml
False-positive suppressions: ios/EthosProtocol/spm-vulnerability-suppressions.toml
(follows the same pattern as android/dependency-check-suppressions.xml — each entry
requires a documented rationale).
To run locally:
brew install osv-scanner
cd ios/EthosProtocol
osv-scanner --lockfile "swift:Package.resolved" --fail-on-severity HIGHcd android
./gradlew test # Unit tests (JVM)
./gradlew connectedAndroidTest # Instrumented tests (device/emulator)Covers: ViewModel state transitions, model logic, Compose UI smoke tests.
The app supports Right-to-Left (RTL) locales (Arabic, Hebrew, etc.) via automatic layout mirroring. To test RTL functionality:
Enable RTL layout direction on a device/emulator:
adb shell settings put global debug.force_rtl_layout 1Run the app and verify:
- All screens display with proper mirroring (buttons, text, icons)
- No text clipping or overlap at edges
- Numerical values format correctly (see Issue #313 for locale-aware formatting)
Disable RTL when done:
adb shell settings put global debug.force_rtl_layout 0See docs/rtl-layout-testing.md for comprehensive RTL testing procedures on both platforms.
Enable RTL pseudo-language in Xcode to test Right-to-Left layout support:
- Edit Scheme → Run → Options
- Set "App Language" to an RTL pseudo-language (e.g.,
ar-XBfor Arabic-Pseudo) - Run the app and verify all screens mirror correctly
Both platforms automatically capture screen load times and API response times with no user action required. iOS uses an os_signpost-based PerformanceMonitor (zero external dependencies — traces appear natively in Instruments). Android uses Firebase Performance Monitoring with manual traces, surfaced in the Firebase Console.
PerformanceMonitor.shared— singleton; records API metrics and screen metrics in-memory with a rolling retention buffer (100 API entries, 50 screen entries).ScreenTracingModifier/.trackScreen("Name")— SwiftUIViewModifierapplied to top-level screens. Measures the time between.onAppearand.onDisappearand records it viarecordScreenLoad.APMConfiguration— central threshold constants:apiSlowThresholdMs: 2000,screenSlowThresholdMs: 500, etc.- No additional setup required;
os_signpostintervals are automatically visible in Instruments > System Trace. - To view traces: Product > Profile (Cmd+I) > System Trace, then filter by subsystem
com.ethosprotocol. PerformanceMonitor.shared.summary()returns aPerformanceSummarywith p50/p95/p99 for API calls, slow-call counts, and average screen load time.
- Firebase Performance Monitoring (
firebase-perf-ktx) added under the existing Firebase BOM — no separate version pin needed. PerformanceMonitorobject inutils/— wraps Firebase traces and maintains bounded in-memory ring buffers for local aggregation.TrackScreen("Name")composable —DisposableEffect-based helper; add it as the first call inside any screen composable to measure its active lifetime.APMConfiguration— same threshold constants as iOS (API_SLOW_THRESHOLD_MS,SCREEN_SLOW_THRESHOLD_MS, etc.).- Firebase setup: ensure
google-services.jsonis present (already required for FCM — see Android setup step 2). - View traces in Firebase Console > Performance > Traces tab.
Thresholds follow the Google RAIL model: response < 100 ms feels instant, < 1 000 ms is noticeable, > 5 000 ms causes abandonment.
| Metric | Slow (warn) | Critical (alert) |
|---|---|---|
| API response | > 2 000 ms | > 5 000 ms |
| Screen load | > 500 ms | > 2 000 ms |
| App startup | > 3 000 ms | — |
Slow calls are logged at error level on both platforms. Counts appear in PerformanceMonitor.shared.summary() (iOS) and PerformanceMonitor.summary() (Android).
- iOS:
PerformanceMonitor.shared.reset()clears in-memory metrics between test runs. - Android:
PerformanceMonitor.reset()clears in-memory metrics between test runs. - Both: slow-call log entries appear in the system log / logcat under the
PerformanceMonitortag.
All CI/CD is driven by GitHub Actions workflows under .github/workflows/. Workflows are triggered on push to main, on pull_request targeting main, and on a weekly schedule for security and drift checks.
- iOS (
ios-ci.yml): generates the Xcode project via XcodeGen, runsxcodebuild testagainst an iOS Simulator destination, and runs.github/scripts/check_tls_pinning.py --configuration Releaseto fail the build if TLS pins are missing or empty. - Android (
android-ci.yml): runs./gradlew test(JVM unit tests) and./gradlew connectedAndroidTest(instrumented tests) on an emulator, plusverifyPaparazziDebugfor snapshot comparison. - Dependency scanning:
android-dependency-check.yml(OWASP) andios-dependency-check.yml(osv-scanner) run weekly and on dependency manifest changes, failing on high/critical CVEs. - App Links verification:
ios-applinks-verify.ymlandandroid-applinks-verify.ymlverify the hostedapple-app-site-associationandassetlinks.jsonfiles daily and on entitlement/manifest changes. - Parity validation:
release-notes-parity-check.ymlensures release notes stay aligned with the "Known gaps" table inPARITY.md. - Staging smoke test:
staging-smoke-test.ymlexercises auth,GET /vaults, andPOST /vaults/{id}/checkinagainst a staging deployment viascripts/smoke_test_staging.sh.
- iOS: the XcodeGen-generated
Xcode/EthosProtocol.xcodeprojis disposable and not committed; the shippable artifact is the signed.ipaproduced from a Release build withTLS_PUBLIC_KEY_PIN_CURRENT/TLS_PUBLIC_KEY_PIN_BACKUPset. - Android: the shippable artifact is the signed
.apk/.aabproduced from a Release build withETHOS_CERT_PINSconfigured (viaETHOS_CERT_PINSenv var orethos.certPinsin~/.gradle/gradle.properties). - Coverage reports: uploaded to Codecov under the
iosandandroidflags (see badges above). - Test reports: JUnit XML and Paparazzi snapshot diffs are uploaded as workflow artifacts for inspection on failure.
- Ensure all CI checks on
mainare green, including dependency scans and App Links verification. - Confirm
PARITY.md's "Known gaps" table matches the release notes (enforced byrelease-notes-parity-check.yml). - Verify TLS pins are configured for iOS (
TLS_PUBLIC_KEY_PIN_CURRENT/TLS_PUBLIC_KEY_PIN_BACKUP) and Android (ETHOS_CERT_PINS) — release builds fail if pins are placeholders or wrong. - Run the staging smoke test against the target staging deployment.
- Tag the release and build signed artifacts for both platforms.
- Publish release notes and update the parity tracking table if any gaps were closed.
- Staging: deploy the backend to the staging environment referenced by
STAGING_API_BASE_URL, then runstaging-smoke-test.yml(orscripts/smoke_test_staging.shlocally) to validate the client/backend contract before cutting a release. - Production (iOS): distribute the signed
.ipavia App Store Connect; ensure APNs key, Associated Domains, and Keychain Sharing capabilities are enabled forcom.ethosprotocolandcom.ethosprotocol.TTLWidget. - Production (Android): distribute the signed
.aabvia Google Play Console; ensuregoogle-services.jsonis present andassetlinks.jsonis hosted athttps://ethos-protocol.app/.well-known/assetlinks.json. - Rollback: revert to the previous tagged release artifact; both platforms support staged rollout so a bad build can be halted before full rollout.
<<<<<<< HEAD
The repo runs a dependency scan for both platforms with the same trigger model:
pushtomainwhen dependency manifests changepull_requesttomainfor the same dependency-focused paths- weekly
scheduleruns to catch newly disclosed CVEs between dependency bumps
Workflow files:
- Android:
.github/workflows/android-dependency-check.yml - iOS:
.github/workflows/ios-dependency-check.yml
Both workflows treat dependency-scan failures as a consistent, human-readable warning in the job log and create a scheduled-run issue alert when the scan fails outside a PR context.
Release notes are expected to stay aligned with the "Known gaps" table in PARITY.md. To prevent a release from claiming a parity issue is closed while the table still lists it as open, CI includes a parity audit workflow:
- Workflow:
.github/workflows/release-notes-parity-check.yml - Script:
.github/scripts/release_notes_parity_check.py
The validator:
- extracts issue numbers from the "Known gaps" table in PARITY.md
- scans merged PRs for parity-gap issue references and close verbs such as "closes #..." or "fixes #..."
- checks the current release notes for the same claim patterns
- fails when a listed parity gap is explicitly called out as closed without the table being updated
Run it locally from the repo root with:
python3 .github/scripts/release_notes_parity_check.py \
--parity-file PARITY.md \
--prs-file merged-prs.json \
--release-notes-file release-notes.mdTo generate the JSON input for the PR scan:
gh pr list --state merged --limit 200 --json number,title,body > merged-prs.jsonThis keeps parity-status messaging consistent with the cross-platform tracking table and helps release notes communicate platform catch-up progress accurately.
Pushing a vX.Y.Z tag (matching MARKETING_VERSION in ios/EthosProtocol/project.yml) builds, signs, and uploads the iOS app to TestFlight via fastlane. Review submission is opt-in and gated behind approval on the app-store environment. Manual runs default to a credential-free dry run.
- Workflow:
.github/workflows/ios-app-store-release.yml - Lanes:
ios/EthosProtocol/fastlane/Fastfile(validate,beta,app_store) - "What's New" generator:
.github/scripts/generate_release_notes.py - Setup, secrets, and the maintainer checklist: docs/ios-app-store-release.md
Both platforms verify that their respective deep-linking and passkey configuration files are correctly hosted and match the app's entitlements/manifest expectations. These checks run daily and on any change to app configuration, catching server-side drift without requiring a code push:
- Workflow:
.github/workflows/ios-applinks-verify.yml - Script:
scripts/verify_apple_app_site_association.sh - Verification targets:
- File is reachable at
https://ethos-protocol.app/.well-known/apple-app-site-association(HTTP 200) - File contains valid JSON
applinkssection lists the app's Team ID + Bundle ID (com.ethosprotocol)webcredentialssection lists the app's Team ID + Bundle ID (required for platform passkeys)
- File is reachable at
- Configuration: Set
APPLE_TEAM_IDENTIFIERas a repository variable (Apple Developer Team ID, e.g., "ABCDEFGHIJ")
- Workflow:
.github/workflows/android-applinks-verify.yml - Script:
scripts/verify_assetlinks.sh - Verification targets:
- File is reachable at
https://ethos-protocol.app/.well-known/assetlinks.json(HTTP 200) - File contains valid JSON
- Contains
delegate_permission/common.handle_all_urlsrelation forcom.ethosprotocol - Namespace is
android_app - Certificate fingerprint matches the release signing certificate (optional, configurable)
- File is reachable at
- Configuration: Set
ANDROID_CERT_SHA256as a repository secret (SHA-256 fingerprints, one per line)
Both workflows file an automated GitHub issue alert on scheduled-run failures, avoiding duplicate alerts by commenting on existing open issues instead of creating new ones each run.
.github/workflows/staging-smoke-test.yml runs scripts/smoke_test_staging.sh
against a staging deployment (a separate STAGING_API_BASE_URL from the
per-client API_BASE_URL set in Info.plist / build.gradle.kts — staging
is a fixed CI-only target, not something either app build points at). It
exercises auth, GET /vaults, and POST /vaults/{id}/checkin to catch a
backend/client contract mismatch (see shared/api-contract.md) before a
release build is cut. The workflow is exposed via workflow_call so a release
workflow can add needs: on it once one exists.
The Android suite includes unit-level localization checks for:
- string length and validation guards (e.g., username and address constraints)
- locale-sensitive number and duration formatting across common locales
- long-string and RTL layout rendering to catch clipping or truncation regressions
- Arabic/Hebrew locale detection for Rtl-aware UI behavior
These checks live in android/app/src/test/java/com/ethosprotocol/LocalizationTest.kt and run under the normal testDebugUnitTest pipeline, so a locale regression is surfaced in CI with the rest of the Android unit-tests.
Battery-impact checks are tracked via the Android background-task metrics in android/app/src/main/java/com/ethosprotocol/services/BackgroundTaskScheduler.kt and the unit suite in android/app/src/test/java/com/ethosprotocol/BatteryDrainTest.kt.
The checks cover:
- background task frequency and wake-up budget
- network-bound work that should stay behind a conservative cadence
- power-hungry operations that are explicitly documented and kept under threshold
- scheduled refresh intervals for time-critical vs. idle vault states
These metrics are intended to keep urgent refresh work at a capped wake-up rate while leaving normal idle refreshes at a much lower power profile.
Snapshot testing framework: Paparazzi configured Screens covered: core app screens + widget snapshots Snapshot update flow: recordPaparazziDebug is documented in the tests CI comparison: verifyPaparazziDebug is in the Android CI workflow Documentation: snapshot/test guidance is in the project docs
This repo already satisfies the requested accessibility-testing work
- #443: Add Memory Leak Detection Tests