@tamagui/native-ci 1.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -0
- package/action.yml +98 -0
- package/actions/fingerprint/action.yml +110 -0
- package/actions/test-detox-android/action.yml +70 -0
- package/actions/test-detox-ios/action.yml +66 -0
- package/dist/cache.js +71 -0
- package/dist/cache.js.map +6 -0
- package/dist/cache.mjs +73 -0
- package/dist/cache.mjs.map +1 -0
- package/dist/cli.js +275 -0
- package/dist/cli.js.map +6 -0
- package/dist/cli.mjs +306 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/constants.js +12 -0
- package/dist/constants.js.map +6 -0
- package/dist/constants.mjs +10 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/deps.js +44 -0
- package/dist/deps.js.map +6 -0
- package/dist/deps.mjs +53 -0
- package/dist/deps.mjs.map +1 -0
- package/dist/detox.js +49 -0
- package/dist/detox.js.map +6 -0
- package/dist/detox.mjs +55 -0
- package/dist/detox.mjs.map +1 -0
- package/dist/fingerprint.js +43 -0
- package/dist/fingerprint.js.map +6 -0
- package/dist/fingerprint.mjs +40 -0
- package/dist/fingerprint.mjs.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +6 -0
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metro.js +79 -0
- package/dist/metro.js.map +6 -0
- package/dist/metro.mjs +75 -0
- package/dist/metro.mjs.map +1 -0
- package/dist/runner.js +73 -0
- package/dist/runner.js.map +6 -0
- package/dist/runner.mjs +73 -0
- package/dist/runner.mjs.map +1 -0
- package/package.json +50 -0
- package/src/android.ts +103 -0
- package/src/cache.ts +144 -0
- package/src/cli.ts +513 -0
- package/src/constants.ts +30 -0
- package/src/deps.ts +109 -0
- package/src/detox.ts +102 -0
- package/src/fingerprint.ts +77 -0
- package/src/index.ts +86 -0
- package/src/ios.ts +38 -0
- package/src/metro.ts +157 -0
- package/src/run-detox-android.ts +49 -0
- package/src/run-detox-ios.ts +40 -0
- package/src/runner.ts +123 -0
- package/types/android.d.ts +32 -0
- package/types/android.d.ts.map +1 -0
- package/types/cache.d.ts +41 -0
- package/types/cache.d.ts.map +1 -0
- package/types/cli.d.ts +11 -0
- package/types/cli.d.ts.map +1 -0
- package/types/constants.d.ts +18 -0
- package/types/constants.d.ts.map +1 -0
- package/types/deps.d.ts +32 -0
- package/types/deps.d.ts.map +1 -0
- package/types/detox.d.ts +39 -0
- package/types/detox.d.ts.map +1 -0
- package/types/fingerprint.d.ts +21 -0
- package/types/fingerprint.d.ts.map +1 -0
- package/types/index.d.ts +16 -0
- package/types/index.d.ts.map +1 -0
- package/types/ios.d.ts +18 -0
- package/types/ios.d.ts.map +1 -0
- package/types/metro.d.ts +51 -0
- package/types/metro.d.ts.map +1 -0
- package/types/run-detox-android.d.ts +15 -0
- package/types/run-detox-android.d.ts.map +1 -0
- package/types/run-detox-ios.d.ts +14 -0
- package/types/run-detox-ios.d.ts.map +1 -0
- package/types/runner.d.ts +35 -0
- package/types/runner.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# @tamagui/native-ci
|
|
2
|
+
|
|
3
|
+
Native CI/CD helpers for React Native apps with Expo. Provides fingerprint-based build caching and Detox test runners for GitHub Actions.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fingerprint-based caching**: Uses `@expo/fingerprint` to detect when native rebuilds are needed
|
|
8
|
+
- **2-level caching**: Pre-fingerprint hash (fast) + full fingerprint (accurate)
|
|
9
|
+
- **KV store integration**: Optional Redis/Upstash KV for persistent fingerprint cache
|
|
10
|
+
- **Detox test runners**: Clean TypeScript scripts for running Detox E2E tests
|
|
11
|
+
- **Reusable GitHub Actions**: Drop-in composite actions for iOS and Android
|
|
12
|
+
- **Signal handling**: Proper cleanup on CI cancellation (SIGINT/SIGTERM)
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- **Bun**: The Detox runner scripts require [Bun](https://bun.sh) runtime
|
|
17
|
+
- **Node.js**: >= 18 for the CLI and library functions
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @tamagui/native-ci
|
|
23
|
+
# or
|
|
24
|
+
yarn add @tamagui/native-ci
|
|
25
|
+
# or
|
|
26
|
+
bun add @tamagui/native-ci
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI Usage
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Generate fingerprint for a platform
|
|
33
|
+
npx @tamagui/native-ci fingerprint ios
|
|
34
|
+
npx @tamagui/native-ci fingerprint android
|
|
35
|
+
|
|
36
|
+
# Generate pre-fingerprint hash (fast)
|
|
37
|
+
npx @tamagui/native-ci pre-hash yarn.lock app.json
|
|
38
|
+
|
|
39
|
+
# Generate cache key
|
|
40
|
+
npx @tamagui/native-ci cache-key ios <fingerprint>
|
|
41
|
+
|
|
42
|
+
# KV operations (requires env vars)
|
|
43
|
+
npx @tamagui/native-ci kv-get <key>
|
|
44
|
+
npx @tamagui/native-ci kv-set <key> <value>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Options
|
|
48
|
+
|
|
49
|
+
- `--project-root <path>` - Project root directory (default: cwd)
|
|
50
|
+
- `--prefix <prefix>` - Cache key prefix (default: native-build)
|
|
51
|
+
- `--github-output` - Output results for GitHub Actions
|
|
52
|
+
- `--json` - Output as JSON
|
|
53
|
+
|
|
54
|
+
### Environment Variables
|
|
55
|
+
|
|
56
|
+
- `KV_STORE_REDIS_REST_URL` - Redis REST API URL for fingerprint caching
|
|
57
|
+
- `KV_STORE_REDIS_REST_TOKEN` - Redis REST API token
|
|
58
|
+
|
|
59
|
+
## Detox Test Runners
|
|
60
|
+
|
|
61
|
+
Run Detox tests with Metro bundler and proper cleanup:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# iOS
|
|
65
|
+
bun run node_modules/@tamagui/native-ci/src/run-detox-ios.ts \
|
|
66
|
+
--project-root ./my-app \
|
|
67
|
+
--config ios.sim.debug
|
|
68
|
+
|
|
69
|
+
# Android
|
|
70
|
+
bun run node_modules/@tamagui/native-ci/src/run-detox-android.ts \
|
|
71
|
+
--project-root ./my-app \
|
|
72
|
+
--config android.emu.ci.debug \
|
|
73
|
+
--headless
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Runner Options
|
|
77
|
+
|
|
78
|
+
| Option | Description | Default |
|
|
79
|
+
|--------|-------------|---------|
|
|
80
|
+
| `--config` | Detox configuration name | `ios.sim.debug` / `android.emu.ci.debug` |
|
|
81
|
+
| `--project-root` | Project root directory | Current directory |
|
|
82
|
+
| `--record-logs` | Log recording: none, failing, all | `all` |
|
|
83
|
+
| `--retries` | Number of test retries | `0` |
|
|
84
|
+
| `--headless` | Run in headless mode (Android only) | `false` |
|
|
85
|
+
|
|
86
|
+
## GitHub Actions
|
|
87
|
+
|
|
88
|
+
### Fingerprint Action
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
- name: Generate Fingerprint
|
|
92
|
+
uses: tamagui/tamagui/code/packages/native-ci/actions/fingerprint@main
|
|
93
|
+
id: fingerprint
|
|
94
|
+
with:
|
|
95
|
+
platform: ios # or android
|
|
96
|
+
project-root: ./my-app
|
|
97
|
+
kv-url: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
|
98
|
+
kv-token: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
|
99
|
+
|
|
100
|
+
- name: Use fingerprint
|
|
101
|
+
run: echo "Fingerprint: ${{ steps.fingerprint.outputs.fingerprint }}"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Inputs
|
|
105
|
+
|
|
106
|
+
| Input | Description | Default |
|
|
107
|
+
|-------|-------------|---------|
|
|
108
|
+
| `platform` | Platform (ios or android) | Required |
|
|
109
|
+
| `project-root` | Path to Expo project | `.` |
|
|
110
|
+
| `cache-prefix` | Prefix for cache keys | `native-build` |
|
|
111
|
+
| `kv-url` | Redis KV REST URL (optional) | - |
|
|
112
|
+
| `kv-token` | Redis KV REST token (optional) | - |
|
|
113
|
+
| `pre-hash-files` | Files for pre-fingerprint hash | `yarn.lock,package-lock.json,app.json` |
|
|
114
|
+
|
|
115
|
+
#### Outputs
|
|
116
|
+
|
|
117
|
+
| Output | Description |
|
|
118
|
+
|--------|-------------|
|
|
119
|
+
| `fingerprint` | Generated fingerprint hash |
|
|
120
|
+
| `cache-key` | Cache key for this build |
|
|
121
|
+
| `pre-fingerprint-hash` | Quick pre-fingerprint hash |
|
|
122
|
+
| `cache-hit` | Whether fingerprint was cached |
|
|
123
|
+
|
|
124
|
+
### iOS Detox Tests Action
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
- name: Run iOS Detox Tests
|
|
128
|
+
uses: tamagui/tamagui/code/packages/native-ci/actions/test-detox-ios@main
|
|
129
|
+
with:
|
|
130
|
+
working-directory: ./my-app
|
|
131
|
+
config: ios.sim.debug
|
|
132
|
+
app-path: ${{ env.IOS_APP_PATH }}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Inputs
|
|
136
|
+
|
|
137
|
+
| Input | Description | Default |
|
|
138
|
+
|-------|-------------|---------|
|
|
139
|
+
| `project-root` | Path to project root | `.` |
|
|
140
|
+
| `working-directory` | Working directory for tests | `.` |
|
|
141
|
+
| `config` | Detox configuration name | `ios.sim.debug` |
|
|
142
|
+
| `record-logs` | Log recording: none, failing, all | `all` |
|
|
143
|
+
| `retries` | Number of test retries | `0` |
|
|
144
|
+
| `simulator` | iOS simulator device type | `iPhone 15` |
|
|
145
|
+
| `app-path` | Path to built app (optional) | - |
|
|
146
|
+
|
|
147
|
+
### Android Detox Tests Action
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- name: Run Android Detox Tests
|
|
151
|
+
uses: tamagui/tamagui/code/packages/native-ci/actions/test-detox-android@main
|
|
152
|
+
with:
|
|
153
|
+
working-directory: ./my-app
|
|
154
|
+
config: android.emu.ci.debug
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### Inputs
|
|
158
|
+
|
|
159
|
+
| Input | Description | Default |
|
|
160
|
+
|-------|-------------|---------|
|
|
161
|
+
| `project-root` | Path to project root | `.` |
|
|
162
|
+
| `working-directory` | Working directory for tests | `.` |
|
|
163
|
+
| `config` | Detox configuration name | `android.emu.ci.debug` |
|
|
164
|
+
| `record-logs` | Log recording: none, failing, all | `all` |
|
|
165
|
+
| `retries` | Number of test retries | `0` |
|
|
166
|
+
| `api-level` | Android API level | `30` |
|
|
167
|
+
| `emulator-options` | Emulator options | See defaults |
|
|
168
|
+
|
|
169
|
+
## Programmatic API
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import {
|
|
173
|
+
// Fingerprinting
|
|
174
|
+
generateFingerprint,
|
|
175
|
+
generatePreFingerprintHash,
|
|
176
|
+
|
|
177
|
+
// Caching
|
|
178
|
+
createCacheKey,
|
|
179
|
+
saveFingerprintToKV,
|
|
180
|
+
getFingerprintFromKV,
|
|
181
|
+
|
|
182
|
+
// Build runner
|
|
183
|
+
runWithCache,
|
|
184
|
+
|
|
185
|
+
// Metro utilities
|
|
186
|
+
withMetro,
|
|
187
|
+
waitForMetro,
|
|
188
|
+
|
|
189
|
+
// Detox utilities
|
|
190
|
+
runDetoxTests,
|
|
191
|
+
parseDetoxArgs,
|
|
192
|
+
|
|
193
|
+
// Android utilities
|
|
194
|
+
setupAndroidDevice,
|
|
195
|
+
|
|
196
|
+
// Constants
|
|
197
|
+
METRO_PORT,
|
|
198
|
+
DETOX_SERVER_PORT,
|
|
199
|
+
} from '@tamagui/native-ci'
|
|
200
|
+
|
|
201
|
+
// Generate fingerprint
|
|
202
|
+
const { hash } = await generateFingerprint({
|
|
203
|
+
platform: 'ios',
|
|
204
|
+
projectRoot: './my-app',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Run build with caching
|
|
208
|
+
const result = await runWithCache({
|
|
209
|
+
platform: 'ios',
|
|
210
|
+
buildCommand: 'xcodebuild ...',
|
|
211
|
+
outputPaths: ['./ios/build'],
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Run tests with Metro
|
|
215
|
+
const exitCode = await withMetro('ios', async () => {
|
|
216
|
+
return runDetoxTests({
|
|
217
|
+
config: 'ios.sim.debug',
|
|
218
|
+
projectRoot: './my-app',
|
|
219
|
+
recordLogs: 'failing',
|
|
220
|
+
retries: 0,
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Example Workflow
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
name: Native Tests
|
|
229
|
+
|
|
230
|
+
on:
|
|
231
|
+
push:
|
|
232
|
+
branches: [main]
|
|
233
|
+
pull_request:
|
|
234
|
+
|
|
235
|
+
jobs:
|
|
236
|
+
build-ios:
|
|
237
|
+
runs-on: macos-14
|
|
238
|
+
outputs:
|
|
239
|
+
cache-key: ${{ steps.fingerprint.outputs.cache-key }}
|
|
240
|
+
steps:
|
|
241
|
+
- uses: actions/checkout@v4
|
|
242
|
+
|
|
243
|
+
- name: Generate Fingerprint
|
|
244
|
+
uses: tamagui/tamagui/code/packages/native-ci/actions/fingerprint@main
|
|
245
|
+
id: fingerprint
|
|
246
|
+
with:
|
|
247
|
+
platform: ios
|
|
248
|
+
project-root: ./my-app
|
|
249
|
+
|
|
250
|
+
- name: Check Build Cache
|
|
251
|
+
uses: actions/cache/restore@v4
|
|
252
|
+
id: cache
|
|
253
|
+
with:
|
|
254
|
+
path: ./my-app/ios/build
|
|
255
|
+
key: ${{ steps.fingerprint.outputs.cache-key }}
|
|
256
|
+
lookup-only: true
|
|
257
|
+
|
|
258
|
+
- name: Build iOS App
|
|
259
|
+
if: steps.cache.outputs.cache-hit != 'true'
|
|
260
|
+
run: |
|
|
261
|
+
cd my-app
|
|
262
|
+
npx expo prebuild --platform ios
|
|
263
|
+
xcodebuild -workspace ios/*.xcworkspace ...
|
|
264
|
+
|
|
265
|
+
- name: Save Build Cache
|
|
266
|
+
if: steps.cache.outputs.cache-hit != 'true'
|
|
267
|
+
uses: actions/cache/save@v4
|
|
268
|
+
with:
|
|
269
|
+
path: ./my-app/ios/build
|
|
270
|
+
key: ${{ steps.fingerprint.outputs.cache-key }}
|
|
271
|
+
|
|
272
|
+
test-ios:
|
|
273
|
+
needs: build-ios
|
|
274
|
+
runs-on: macos-14
|
|
275
|
+
steps:
|
|
276
|
+
- uses: actions/checkout@v4
|
|
277
|
+
|
|
278
|
+
- name: Restore Build
|
|
279
|
+
uses: actions/cache/restore@v4
|
|
280
|
+
with:
|
|
281
|
+
path: ./my-app/ios/build
|
|
282
|
+
key: ${{ needs.build-ios.outputs.cache-key }}
|
|
283
|
+
|
|
284
|
+
- name: Run Tests
|
|
285
|
+
uses: tamagui/tamagui/code/packages/native-ci/actions/test-detox-ios@main
|
|
286
|
+
with:
|
|
287
|
+
working-directory: ./my-app
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## How Fingerprinting Works
|
|
291
|
+
|
|
292
|
+
1. **Pre-fingerprint hash**: Quick hash of `yarn.lock`, `app.json`, etc.
|
|
293
|
+
2. **KV cache lookup**: Check if we've seen this pre-hash before
|
|
294
|
+
3. **Full fingerprint**: If not cached, run `@expo/fingerprint` for accurate native dependency detection
|
|
295
|
+
4. **Cache build artifacts**: Use fingerprint as cache key
|
|
296
|
+
|
|
297
|
+
This 2-level approach means:
|
|
298
|
+
- Cache hits are instant (no fingerprint generation needed)
|
|
299
|
+
- Rebuilds only happen when native dependencies actually change
|
|
300
|
+
- Works across CI runs with KV persistence
|
|
301
|
+
|
|
302
|
+
## Architecture
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
src/
|
|
306
|
+
├── constants.ts # Shared constants and types
|
|
307
|
+
├── fingerprint.ts # Fingerprint generation
|
|
308
|
+
├── cache.ts # KV store and local cache
|
|
309
|
+
├── runner.ts # Build runner with caching
|
|
310
|
+
├── metro.ts # Metro bundler utilities
|
|
311
|
+
├── detox.ts # Detox test utilities
|
|
312
|
+
├── android.ts # Android-specific utilities
|
|
313
|
+
├── cli.ts # CLI entry point
|
|
314
|
+
├── run-detox-ios.ts # iOS test runner script
|
|
315
|
+
└── run-detox-android.ts # Android test runner script
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT
|
package/action.yml
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: 'Native CI - Fingerprint Cache'
|
|
2
|
+
description: 'Generate Expo fingerprints and manage native build caching'
|
|
3
|
+
author: 'Tamagui'
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
platform:
|
|
7
|
+
description: 'Platform to generate fingerprint for (ios or android)'
|
|
8
|
+
required: true
|
|
9
|
+
project-root:
|
|
10
|
+
description: 'Path to the Expo project root'
|
|
11
|
+
required: false
|
|
12
|
+
default: '.'
|
|
13
|
+
cache-prefix:
|
|
14
|
+
description: 'Prefix for cache keys'
|
|
15
|
+
required: false
|
|
16
|
+
default: 'native-build'
|
|
17
|
+
kv-url:
|
|
18
|
+
description: 'Redis KV REST API URL for fingerprint caching'
|
|
19
|
+
required: false
|
|
20
|
+
kv-token:
|
|
21
|
+
description: 'Redis KV REST API token'
|
|
22
|
+
required: false
|
|
23
|
+
pre-hash-files:
|
|
24
|
+
description: 'Comma-separated list of files for pre-fingerprint hash'
|
|
25
|
+
required: false
|
|
26
|
+
default: 'yarn.lock,app.json'
|
|
27
|
+
|
|
28
|
+
outputs:
|
|
29
|
+
fingerprint:
|
|
30
|
+
description: 'The generated fingerprint hash'
|
|
31
|
+
value: ${{ steps.fingerprint.outputs.fingerprint }}
|
|
32
|
+
cache-key:
|
|
33
|
+
description: 'The cache key to use for this build'
|
|
34
|
+
value: ${{ steps.fingerprint.outputs.cache-key }}
|
|
35
|
+
pre-fingerprint-hash:
|
|
36
|
+
description: 'Quick pre-fingerprint hash'
|
|
37
|
+
value: ${{ steps.pre-hash.outputs.pre-fingerprint-hash }}
|
|
38
|
+
cache-hit:
|
|
39
|
+
description: 'Whether the fingerprint was found in cache'
|
|
40
|
+
value: ${{ steps.check-cache.outputs.cache-hit }}
|
|
41
|
+
|
|
42
|
+
runs:
|
|
43
|
+
using: 'composite'
|
|
44
|
+
steps:
|
|
45
|
+
- name: Calculate Pre-Fingerprint Hash
|
|
46
|
+
id: pre-hash
|
|
47
|
+
shell: bash
|
|
48
|
+
working-directory: ${{ inputs.project-root }}
|
|
49
|
+
run: |
|
|
50
|
+
FILES="${{ inputs.pre-hash-files }}"
|
|
51
|
+
HASH=$(echo "$FILES" | tr ',' '\n' | xargs -I{} cat {} 2>/dev/null | sha256sum | cut -c1-16)
|
|
52
|
+
echo "pre-fingerprint-hash=$HASH" >> $GITHUB_OUTPUT
|
|
53
|
+
|
|
54
|
+
- name: Check KV Cache for Fingerprint
|
|
55
|
+
id: kv-lookup
|
|
56
|
+
if: inputs.kv-url != '' && inputs.kv-token != ''
|
|
57
|
+
shell: bash
|
|
58
|
+
env:
|
|
59
|
+
KV_URL: ${{ inputs.kv-url }}
|
|
60
|
+
KV_TOKEN: ${{ inputs.kv-token }}
|
|
61
|
+
run: |
|
|
62
|
+
KEY="${{ inputs.platform }}-${{ inputs.cache-prefix }}-pre-${{ steps.pre-hash.outputs.pre-fingerprint-hash }}"
|
|
63
|
+
RESULT=$(curl -s "$KV_URL/get/$KEY" -H "Authorization: Bearer $KV_TOKEN" | jq -r '.result')
|
|
64
|
+
if [ "$RESULT" != "null" ]; then
|
|
65
|
+
echo "fingerprint=$RESULT" >> $GITHUB_OUTPUT
|
|
66
|
+
echo "found=true" >> $GITHUB_OUTPUT
|
|
67
|
+
# Extend TTL
|
|
68
|
+
curl -s -X POST "$KV_URL/EXPIRE/$KEY/2592000" -H "Authorization: Bearer $KV_TOKEN" > /dev/null
|
|
69
|
+
else
|
|
70
|
+
echo "found=false" >> $GITHUB_OUTPUT
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
- name: Generate Fingerprint
|
|
74
|
+
id: fingerprint
|
|
75
|
+
if: steps.kv-lookup.outputs.found != 'true'
|
|
76
|
+
shell: bash
|
|
77
|
+
working-directory: ${{ inputs.project-root }}
|
|
78
|
+
run: |
|
|
79
|
+
FINGERPRINT=$(npx @expo/fingerprint fingerprint:generate --platform ${{ inputs.platform }} | jq -r '.hash')
|
|
80
|
+
echo "fingerprint=$FINGERPRINT" >> $GITHUB_OUTPUT
|
|
81
|
+
echo "cache-key=${{ inputs.cache-prefix }}-${{ inputs.platform }}-$FINGERPRINT" >> $GITHUB_OUTPUT
|
|
82
|
+
|
|
83
|
+
- name: Use Cached Fingerprint
|
|
84
|
+
if: steps.kv-lookup.outputs.found == 'true'
|
|
85
|
+
shell: bash
|
|
86
|
+
run: |
|
|
87
|
+
echo "fingerprint=${{ steps.kv-lookup.outputs.fingerprint }}" >> $GITHUB_OUTPUT
|
|
88
|
+
echo "cache-key=${{ inputs.cache-prefix }}-${{ inputs.platform }}-${{ steps.kv-lookup.outputs.fingerprint }}" >> $GITHUB_OUTPUT
|
|
89
|
+
|
|
90
|
+
- name: Save Fingerprint to KV
|
|
91
|
+
if: inputs.kv-url != '' && inputs.kv-token != '' && steps.kv-lookup.outputs.found != 'true'
|
|
92
|
+
shell: bash
|
|
93
|
+
env:
|
|
94
|
+
KV_URL: ${{ inputs.kv-url }}
|
|
95
|
+
KV_TOKEN: ${{ inputs.kv-token }}
|
|
96
|
+
run: |
|
|
97
|
+
KEY="${{ inputs.platform }}-${{ inputs.cache-prefix }}-pre-${{ steps.pre-hash.outputs.pre-fingerprint-hash }}"
|
|
98
|
+
curl -s -X POST "$KV_URL/SETEX/$KEY/2592000/${{ steps.fingerprint.outputs.fingerprint }}" -H "Authorization: Bearer $KV_TOKEN" > /dev/null
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: 'Native CI - Fingerprint'
|
|
2
|
+
description: 'Generate Expo fingerprint with 2-level caching (pre-hash + full fingerprint)'
|
|
3
|
+
author: 'Tamagui'
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
platform:
|
|
7
|
+
description: 'Platform (ios or android)'
|
|
8
|
+
required: true
|
|
9
|
+
project-root:
|
|
10
|
+
description: 'Path to the Expo project root'
|
|
11
|
+
default: '.'
|
|
12
|
+
cache-prefix:
|
|
13
|
+
description: 'Prefix for cache keys'
|
|
14
|
+
default: 'native-build'
|
|
15
|
+
kv-url:
|
|
16
|
+
description: 'Redis KV REST API URL for fingerprint caching (optional)'
|
|
17
|
+
required: false
|
|
18
|
+
kv-token:
|
|
19
|
+
description: 'Redis KV REST API token (optional)'
|
|
20
|
+
required: false
|
|
21
|
+
pre-hash-files:
|
|
22
|
+
description: 'Comma-separated list of files for pre-fingerprint hash'
|
|
23
|
+
default: 'yarn.lock,package-lock.json,app.json'
|
|
24
|
+
|
|
25
|
+
outputs:
|
|
26
|
+
fingerprint:
|
|
27
|
+
description: 'The generated fingerprint hash'
|
|
28
|
+
value: ${{ steps.result.outputs.fingerprint }}
|
|
29
|
+
cache-key:
|
|
30
|
+
description: 'The cache key to use for this build'
|
|
31
|
+
value: ${{ steps.result.outputs.cache-key }}
|
|
32
|
+
pre-fingerprint-hash:
|
|
33
|
+
description: 'Quick pre-fingerprint hash'
|
|
34
|
+
value: ${{ steps.pre-hash.outputs.hash }}
|
|
35
|
+
cache-hit:
|
|
36
|
+
description: 'Whether fingerprint was found in KV cache'
|
|
37
|
+
value: ${{ steps.kv-lookup.outputs.found || 'false' }}
|
|
38
|
+
|
|
39
|
+
runs:
|
|
40
|
+
using: 'composite'
|
|
41
|
+
steps:
|
|
42
|
+
- name: Calculate Pre-Fingerprint Hash
|
|
43
|
+
id: pre-hash
|
|
44
|
+
shell: bash
|
|
45
|
+
working-directory: ${{ inputs.project-root }}
|
|
46
|
+
run: |
|
|
47
|
+
FILES="${{ inputs.pre-hash-files }}"
|
|
48
|
+
HASH=$(echo "$FILES" | tr ',' '\n' | while read f; do
|
|
49
|
+
[ -f "$f" ] && cat "$f"
|
|
50
|
+
done | sha256sum | cut -c1-16)
|
|
51
|
+
echo "hash=$HASH" >> $GITHUB_OUTPUT
|
|
52
|
+
echo "Pre-fingerprint hash: $HASH"
|
|
53
|
+
|
|
54
|
+
- name: Check KV Cache for Fingerprint
|
|
55
|
+
id: kv-lookup
|
|
56
|
+
if: inputs.kv-url != '' && inputs.kv-token != ''
|
|
57
|
+
shell: bash
|
|
58
|
+
env:
|
|
59
|
+
KV_URL: ${{ inputs.kv-url }}
|
|
60
|
+
KV_TOKEN: ${{ inputs.kv-token }}
|
|
61
|
+
run: |
|
|
62
|
+
KEY="${{ inputs.platform }}-${{ inputs.cache-prefix }}-pre-${{ steps.pre-hash.outputs.hash }}"
|
|
63
|
+
echo "Looking up KV key: $KEY"
|
|
64
|
+
RESULT=$(curl -s "$KV_URL/get/$KEY" -H "Authorization: Bearer $KV_TOKEN" | jq -r '.result // empty')
|
|
65
|
+
if [ -n "$RESULT" ] && [ "$RESULT" != "null" ]; then
|
|
66
|
+
echo "Found cached fingerprint: $RESULT"
|
|
67
|
+
echo "fingerprint=$RESULT" >> $GITHUB_OUTPUT
|
|
68
|
+
echo "found=true" >> $GITHUB_OUTPUT
|
|
69
|
+
# Extend TTL to 30 days
|
|
70
|
+
curl -s -X POST "$KV_URL/EXPIRE/$KEY/2592000" -H "Authorization: Bearer $KV_TOKEN" > /dev/null
|
|
71
|
+
else
|
|
72
|
+
echo "No cached fingerprint found"
|
|
73
|
+
echo "found=false" >> $GITHUB_OUTPUT
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
- name: Generate Fingerprint
|
|
77
|
+
id: generate
|
|
78
|
+
if: steps.kv-lookup.outputs.found != 'true'
|
|
79
|
+
shell: bash
|
|
80
|
+
working-directory: ${{ inputs.project-root }}
|
|
81
|
+
run: |
|
|
82
|
+
echo "Generating ${{ inputs.platform }} fingerprint..."
|
|
83
|
+
FINGERPRINT=$(npx @expo/fingerprint fingerprint:generate --platform ${{ inputs.platform }} | jq -r '.hash')
|
|
84
|
+
echo "Generated fingerprint: $FINGERPRINT"
|
|
85
|
+
echo "fingerprint=$FINGERPRINT" >> $GITHUB_OUTPUT
|
|
86
|
+
|
|
87
|
+
- name: Save Fingerprint to KV
|
|
88
|
+
if: inputs.kv-url != '' && inputs.kv-token != '' && steps.kv-lookup.outputs.found != 'true' && steps.generate.outputs.fingerprint != ''
|
|
89
|
+
shell: bash
|
|
90
|
+
env:
|
|
91
|
+
KV_URL: ${{ inputs.kv-url }}
|
|
92
|
+
KV_TOKEN: ${{ inputs.kv-token }}
|
|
93
|
+
run: |
|
|
94
|
+
KEY="${{ inputs.platform }}-${{ inputs.cache-prefix }}-pre-${{ steps.pre-hash.outputs.hash }}"
|
|
95
|
+
echo "Saving fingerprint to KV: $KEY"
|
|
96
|
+
curl -s -X POST "$KV_URL/SETEX/$KEY/2592000/${{ steps.generate.outputs.fingerprint }}" -H "Authorization: Bearer $KV_TOKEN" > /dev/null
|
|
97
|
+
|
|
98
|
+
- name: Set Result
|
|
99
|
+
id: result
|
|
100
|
+
shell: bash
|
|
101
|
+
run: |
|
|
102
|
+
if [ "${{ steps.kv-lookup.outputs.found }}" = "true" ]; then
|
|
103
|
+
FP="${{ steps.kv-lookup.outputs.fingerprint }}"
|
|
104
|
+
else
|
|
105
|
+
FP="${{ steps.generate.outputs.fingerprint }}"
|
|
106
|
+
fi
|
|
107
|
+
echo "fingerprint=$FP" >> $GITHUB_OUTPUT
|
|
108
|
+
echo "cache-key=${{ inputs.cache-prefix }}-${{ inputs.platform }}-$FP" >> $GITHUB_OUTPUT
|
|
109
|
+
echo "Final fingerprint: $FP"
|
|
110
|
+
echo "Cache key: ${{ inputs.cache-prefix }}-${{ inputs.platform }}-$FP"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: 'Native CI - Android Detox Tests'
|
|
2
|
+
description: 'Run Detox E2E tests on Android emulator'
|
|
3
|
+
author: 'Tamagui'
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
project-root:
|
|
7
|
+
description: 'Path to the project root'
|
|
8
|
+
default: '.'
|
|
9
|
+
working-directory:
|
|
10
|
+
description: 'Working directory for running tests'
|
|
11
|
+
default: '.'
|
|
12
|
+
config:
|
|
13
|
+
description: 'Detox configuration name'
|
|
14
|
+
default: 'android.emu.ci.debug'
|
|
15
|
+
record-logs:
|
|
16
|
+
description: 'Record logs mode: none, failing, all'
|
|
17
|
+
default: 'all'
|
|
18
|
+
retries:
|
|
19
|
+
description: 'Number of test retries (0 = fail fast)'
|
|
20
|
+
default: '0'
|
|
21
|
+
api-level:
|
|
22
|
+
description: 'Android API level'
|
|
23
|
+
default: '30'
|
|
24
|
+
emulator-options:
|
|
25
|
+
description: 'Emulator options'
|
|
26
|
+
default: '-no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none'
|
|
27
|
+
|
|
28
|
+
runs:
|
|
29
|
+
using: 'composite'
|
|
30
|
+
steps:
|
|
31
|
+
- name: Setup Bun
|
|
32
|
+
uses: oven-sh/setup-bun@v2
|
|
33
|
+
with:
|
|
34
|
+
bun-version: latest
|
|
35
|
+
|
|
36
|
+
- name: Setup Java
|
|
37
|
+
uses: actions/setup-java@v4
|
|
38
|
+
with:
|
|
39
|
+
distribution: 'temurin'
|
|
40
|
+
java-version: '17'
|
|
41
|
+
|
|
42
|
+
- name: Install Detox CLI
|
|
43
|
+
shell: bash
|
|
44
|
+
run: npm install -g detox-cli
|
|
45
|
+
|
|
46
|
+
- name: Start Android Emulator and Run Tests
|
|
47
|
+
uses: reactivecircus/android-emulator-runner@v2
|
|
48
|
+
with:
|
|
49
|
+
api-level: ${{ inputs.api-level }}
|
|
50
|
+
target: google_apis
|
|
51
|
+
arch: x86_64
|
|
52
|
+
profile: pixel_6
|
|
53
|
+
avd-name: test
|
|
54
|
+
ram-size: 4096M
|
|
55
|
+
heap-size: 1024M
|
|
56
|
+
force-avd-creation: false
|
|
57
|
+
emulator-boot-timeout: 900
|
|
58
|
+
disable-animations: true
|
|
59
|
+
emulator-options: ${{ inputs.emulator-options }}
|
|
60
|
+
working-directory: ${{ inputs.working-directory }}
|
|
61
|
+
pre-emulator-launch-script: |
|
|
62
|
+
adb start-server
|
|
63
|
+
script: |
|
|
64
|
+
SCRIPT_PATH=$(node -e "console.log(require.resolve('@tamagui/native-ci/src/run-detox-android.ts'))")
|
|
65
|
+
bun run "$SCRIPT_PATH" \
|
|
66
|
+
--headless \
|
|
67
|
+
--project-root "$PWD" \
|
|
68
|
+
--config "${{ inputs.config }}" \
|
|
69
|
+
--record-logs "${{ inputs.record-logs }}" \
|
|
70
|
+
--retries "${{ inputs.retries }}"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: 'Native CI - iOS Detox Tests'
|
|
2
|
+
description: 'Run Detox E2E tests on iOS simulator'
|
|
3
|
+
author: 'Tamagui'
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
project-root:
|
|
7
|
+
description: 'Path to the project root'
|
|
8
|
+
default: '.'
|
|
9
|
+
working-directory:
|
|
10
|
+
description: 'Working directory for running tests'
|
|
11
|
+
default: '.'
|
|
12
|
+
config:
|
|
13
|
+
description: 'Detox configuration name'
|
|
14
|
+
default: 'ios.sim.debug'
|
|
15
|
+
record-logs:
|
|
16
|
+
description: 'Record logs mode: none, failing, all'
|
|
17
|
+
default: 'all'
|
|
18
|
+
retries:
|
|
19
|
+
description: 'Number of test retries (0 = fail fast)'
|
|
20
|
+
default: '0'
|
|
21
|
+
simulator:
|
|
22
|
+
description: 'iOS simulator device type'
|
|
23
|
+
default: 'iPhone 15'
|
|
24
|
+
app-path:
|
|
25
|
+
description: 'Path to the built app (optional, uses DETOX_IOS_APP_PATH env var)'
|
|
26
|
+
required: false
|
|
27
|
+
|
|
28
|
+
runs:
|
|
29
|
+
using: 'composite'
|
|
30
|
+
steps:
|
|
31
|
+
- name: Setup Bun
|
|
32
|
+
uses: oven-sh/setup-bun@v2
|
|
33
|
+
with:
|
|
34
|
+
bun-version: latest
|
|
35
|
+
|
|
36
|
+
- name: Install Detox CLI
|
|
37
|
+
shell: bash
|
|
38
|
+
run: npm install -g detox-cli
|
|
39
|
+
|
|
40
|
+
- name: Install applesimutils
|
|
41
|
+
shell: bash
|
|
42
|
+
run: |
|
|
43
|
+
brew tap wix/brew
|
|
44
|
+
brew install applesimutils
|
|
45
|
+
|
|
46
|
+
- name: Boot iOS Simulator
|
|
47
|
+
shell: bash
|
|
48
|
+
run: |
|
|
49
|
+
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries | .[] | select(.key | contains("iOS")) | .value[] | select(.name == "${{ inputs.simulator }}") | .udid' | head -1)
|
|
50
|
+
if [ -n "$DEVICE_ID" ]; then
|
|
51
|
+
echo "Booting simulator: ${{ inputs.simulator }} ($DEVICE_ID)"
|
|
52
|
+
xcrun simctl boot "$DEVICE_ID" || true
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
- name: Run Detox Tests
|
|
56
|
+
shell: bash
|
|
57
|
+
working-directory: ${{ inputs.working-directory }}
|
|
58
|
+
env:
|
|
59
|
+
DETOX_IOS_APP_PATH: ${{ inputs.app-path }}
|
|
60
|
+
run: |
|
|
61
|
+
SCRIPT_PATH=$(node -e "console.log(require.resolve('@tamagui/native-ci/src/run-detox-ios.ts'))")
|
|
62
|
+
bun run "$SCRIPT_PATH" \
|
|
63
|
+
--project-root "$PWD" \
|
|
64
|
+
--config "${{ inputs.config }}" \
|
|
65
|
+
--record-logs "${{ inputs.record-logs }}" \
|
|
66
|
+
--retries "${{ inputs.retries }}"
|