mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
apps/team moved to hanzo-apps/team with its history, and builds there against the published @hanzo/gui. It leaves the workspace list, the lockfile, and the Written-at-Hanzo list in every NOTICE (ten names become nine). Co-authored-by: Hanzo Dev <[email protected]>
@hanzogui/native-ci
Native CI/CD helpers for React Native apps with Expo. Provides fingerprint-based build caching and Detox test runners for GitHub Actions.
Features
- Fingerprint-based caching: Uses
@expo/fingerprintto detect when native rebuilds are needed - 2-level caching: Pre-fingerprint hash (fast) + full fingerprint (accurate)
- KV store integration: Optional Redis/Upstash KV for persistent fingerprint cache
- Detox test runners: Clean TypeScript scripts for running Detox E2E tests
- Reusable GitHub Actions: Drop-in composite actions for iOS and Android
- Signal handling: Proper cleanup on CI cancellation (SIGINT/SIGTERM)
Requirements
- Bun: The Detox runner scripts require Bun runtime
- Node.js: >= 18 for the CLI and library functions
Installation
npm install @hanzogui/native-ci
# or
yarn add @hanzogui/native-ci
# or
bun add @hanzogui/native-ci
CLI Usage
# Generate fingerprint for a platform
npx @hanzogui/native-ci fingerprint ios
npx @hanzogui/native-ci fingerprint android
# Generate pre-fingerprint hash (fast)
npx @hanzogui/native-ci pre-hash yarn.lock app.json
# Generate cache key
npx @hanzogui/native-ci cache-key ios <fingerprint>
# KV operations (requires env vars)
npx @hanzogui/native-ci kv-get <key>
npx @hanzogui/native-ci kv-set <key> <value>
Options
--project-root <path>- Project root directory (default: cwd)--prefix <prefix>- Cache key prefix (default: native-build)--github-output- Output results for GitHub Actions--json- Output as JSON
Environment Variables
KV_STORE_REDIS_REST_URL- Redis REST API URL for fingerprint cachingKV_STORE_REDIS_REST_TOKEN- Redis REST API token
Detox Test Runners
Run Detox tests with Metro bundler and proper cleanup:
# iOS
bun run node_modules/@hanzogui/native-ci/src/run-detox-ios.ts \
--project-root ./my-app \
--config ios.sim.debug
# Android
bun run node_modules/@hanzogui/native-ci/src/run-detox-android.ts \
--project-root ./my-app \
--config android.emu.ci.debug \
--headless
Runner Options
| Option | Description | Default |
|---|---|---|
--config |
Detox configuration name | ios.sim.debug / android.emu.ci.debug |
--project-root |
Project root directory | Current directory |
--record-logs |
Log recording: none, failing, all | all |
--retries |
Number of test retries | 0 |
--headless |
Run in headless mode (Android only) | false |
GitHub Actions
Fingerprint Action
- name: Generate Fingerprint
uses: hanzogui/hanzogui/pkgs/native-ci/actions/fingerprint@main
id: fingerprint
with:
platform: ios # or android
project-root: ./my-app
kv-url: ${{ secrets.KV_STORE_REDIS_REST_URL }}
kv-token: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
- name: Use fingerprint
run: echo "Fingerprint: ${{ steps.fingerprint.outputs.fingerprint }}"
Inputs
| Input | Description | Default |
|---|---|---|
platform |
Platform (ios or android) | Required |
project-root |
Path to Expo project | . |
cache-prefix |
Prefix for cache keys | native-build |
kv-url |
Redis KV REST URL (optional) | - |
kv-token |
Redis KV REST token (optional) | - |
pre-hash-files |
Files for pre-fingerprint hash | yarn.lock,package-lock.json,app.json |
Outputs
| Output | Description |
|---|---|
fingerprint |
Generated fingerprint hash |
cache-key |
Cache key for this build |
pre-fingerprint-hash |
Quick pre-fingerprint hash |
cache-hit |
Whether fingerprint was cached |
iOS Detox Tests Action
- name: Run iOS Detox Tests
uses: hanzogui/hanzogui/pkgs/native-ci/actions/test-detox-ios@main
with:
working-directory: ./my-app
config: ios.sim.debug
app-path: ${{ env.IOS_APP_PATH }}
Inputs
| Input | Description | Default |
|---|---|---|
project-root |
Path to project root | . |
working-directory |
Working directory for tests | . |
config |
Detox configuration name | ios.sim.debug |
record-logs |
Log recording: none, failing, all | all |
retries |
Number of test retries | 0 |
simulator |
iOS simulator device type | iPhone 15 |
app-path |
Path to built app (optional) | - |
Android Detox Tests Action
- name: Run Android Detox Tests
uses: hanzogui/hanzogui/pkgs/native-ci/actions/test-detox-android@main
with:
working-directory: ./my-app
config: android.emu.ci.debug
Inputs
| Input | Description | Default |
|---|---|---|
project-root |
Path to project root | . |
working-directory |
Working directory for tests | . |
config |
Detox configuration name | android.emu.ci.debug |
record-logs |
Log recording: none, failing, all | all |
retries |
Number of test retries | 0 |
api-level |
Android API level | 30 |
emulator-options |
Emulator options | See defaults |
Programmatic API
import {
// Fingerprinting
generateFingerprint,
generatePreFingerprintHash,
// Caching
createCacheKey,
saveFingerprintToKV,
getFingerprintFromKV,
// Build runner
runWithCache,
// Metro utilities
withMetro,
waitForMetro,
// Detox utilities
runDetoxTests,
parseDetoxArgs,
// Android utilities
setupAndroidDevice,
// Constants
METRO_PORT,
DETOX_SERVER_PORT,
} from '@hanzogui/native-ci'
// Generate fingerprint
const { hash } = await generateFingerprint({
platform: 'ios',
projectRoot: './my-app',
})
// Run build with caching
const result = await runWithCache({
platform: 'ios',
buildCommand: 'xcodebuild ...',
outputPaths: ['./ios/build'],
})
// Run tests with Metro
const exitCode = await withMetro('ios', async () => {
return runDetoxTests({
config: 'ios.sim.debug',
projectRoot: './my-app',
recordLogs: 'failing',
retries: 0,
})
})
Example Workflow
name: Native Tests
on:
push:
branches: [main]
pull_request:
jobs:
build-ios:
runs-on: macos-14
outputs:
cache-key: ${{ steps.fingerprint.outputs.cache-key }}
steps:
- uses: actions/checkout@v4
- name: Generate Fingerprint
uses: hanzogui/hanzogui/pkgs/native-ci/actions/fingerprint@main
id: fingerprint
with:
platform: ios
project-root: ./my-app
- name: Check Build Cache
uses: actions/cache/restore@v4
id: cache
with:
path: ./my-app/ios/build
key: ${{ steps.fingerprint.outputs.cache-key }}
lookup-only: true
- name: Build iOS App
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd my-app
npx expo prebuild --platform ios
xcodebuild -workspace ios/*.xcworkspace ...
- name: Save Build Cache
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ./my-app/ios/build
key: ${{ steps.fingerprint.outputs.cache-key }}
test-ios:
needs: build-ios
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Restore Build
uses: actions/cache/restore@v4
with:
path: ./my-app/ios/build
key: ${{ needs.build-ios.outputs.cache-key }}
- name: Run Tests
uses: hanzogui/hanzogui/pkgs/native-ci/actions/test-detox-ios@main
with:
working-directory: ./my-app
How Fingerprinting Works
- Pre-fingerprint hash: Quick hash of
yarn.lock,app.json, etc. - KV cache lookup: Check if we've seen this pre-hash before
- Full fingerprint: If not cached, run
@expo/fingerprintfor accurate native dependency detection - Cache build artifacts: Use fingerprint as cache key
This 2-level approach means:
- Cache hits are instant (no fingerprint generation needed)
- Rebuilds only happen when native dependencies actually change
- Works across CI runs with KV persistence
Architecture
src/
├── constants.ts # Shared constants and types
├── fingerprint.ts # Fingerprint generation
├── cache.ts # KV store and local cache
├── runner.ts # Build runner with caching
├── metro.ts # Metro bundler utilities
├── detox.ts # Detox test utilities
├── android.ts # Android-specific utilities
├── cli.ts # CLI entry point
├── run-detox-ios.ts # iOS test runner script
└── run-detox-android.ts # Android test runner script
License
MIT