@radhya/mach 2.0.30 → 2.0.32
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 +41 -1
- package/dist/{chunk-6LOQKG2J.js → chunk-ZPETQEMG.js} +18 -18
- package/dist/{credentials-P3555DXZ.js → credentials-STB7QAPB.js} +1 -1
- package/dist/index.js +154 -148
- package/docs/ota-implementation-plan.md +1004 -0
- package/expo-plugin/index.js +43 -1
- package/package.json +3 -2
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
# Mach OTA Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Build a Mach-owned over-the-air update system for Expo and vanilla React Native apps without depending on EAS Update or App Center.
|
|
6
|
+
|
|
7
|
+
Mach should own the update publish command, storage, release metadata, channel routing, runtime compatibility, dashboard history, rollback, and download URLs. Expo may be used as an open-source client/runtime only when the project is Expo or explicitly chooses `expo-updates`.
|
|
8
|
+
|
|
9
|
+
## Current State Check
|
|
10
|
+
|
|
11
|
+
### CLI
|
|
12
|
+
|
|
13
|
+
Existing files:
|
|
14
|
+
|
|
15
|
+
- `src/index.ts` registers `mach update`.
|
|
16
|
+
- `src/commands/update.ts` implements the current OTA prototype.
|
|
17
|
+
- `src/lib/config.ts` has a small `update` block with `channel` and `url`.
|
|
18
|
+
- `README.md` lists `mach update`.
|
|
19
|
+
|
|
20
|
+
Current `mach update` flow:
|
|
21
|
+
|
|
22
|
+
1. Reads `mach.config.json`.
|
|
23
|
+
2. Defaults channel to `production`.
|
|
24
|
+
3. Uses `package.json` version as `runtimeVersion`.
|
|
25
|
+
4. Runs `npx expo export --platform all`.
|
|
26
|
+
5. Zips `dist/`.
|
|
27
|
+
6. Uploads the zip with `/builds/source-upload-url`, which stores it as `builds/<updateId>/source.zip`.
|
|
28
|
+
7. Calls `/updates/publish` twice, once for Android and once for iOS, both pointing `manifestKey` at the uploaded zip key.
|
|
29
|
+
|
|
30
|
+
Problems:
|
|
31
|
+
|
|
32
|
+
- It assumes every project is Expo.
|
|
33
|
+
- It uses `expo export` for vanilla React Native, which is wrong.
|
|
34
|
+
- It uploads a zip but tells the API it is a manifest.
|
|
35
|
+
- It does not generate or upload individual immutable assets.
|
|
36
|
+
- It does not produce a valid Expo Updates manifest.
|
|
37
|
+
- It does not support vanilla RN client setup.
|
|
38
|
+
- It does not support setup, verify, list, rollback, promote, rollout, or dashboard visibility.
|
|
39
|
+
- It does not use build profile or latest successful Mach build runtime.
|
|
40
|
+
- It has no native-change guard.
|
|
41
|
+
- It uses `update` as the public command name, which is too generic.
|
|
42
|
+
|
|
43
|
+
### API
|
|
44
|
+
|
|
45
|
+
Existing files:
|
|
46
|
+
|
|
47
|
+
- `mach-api/api/src/index.ts` defines `TABLE_UPDATES`.
|
|
48
|
+
- `GET /api/manifest` returns an Expo-style manifest response.
|
|
49
|
+
- `POST /updates/publish` stores update metadata.
|
|
50
|
+
|
|
51
|
+
Current API behavior:
|
|
52
|
+
|
|
53
|
+
1. Manifest route reads `expo-runtime-version`, `expo-channel-name`, and `expo-platform`.
|
|
54
|
+
2. It scans `MachUpdates` by channel, platform, and runtime version.
|
|
55
|
+
3. It sorts by `createdAt`.
|
|
56
|
+
4. It reads `latestUpdate.manifestKey` from S3.
|
|
57
|
+
5. It parses the S3 object body as JSON.
|
|
58
|
+
|
|
59
|
+
Problems:
|
|
60
|
+
|
|
61
|
+
- It does not filter by `projectId`, so updates can collide across projects.
|
|
62
|
+
- It uses `ScanCommand`, not a query-friendly table/index.
|
|
63
|
+
- It reads a zip uploaded by the CLI and tries to parse it as JSON.
|
|
64
|
+
- It has no `GET /updates`, rollback, promote, disable, delete, or asset download routes.
|
|
65
|
+
- It has no `MachUpdates` table setup script.
|
|
66
|
+
- It does not support directives such as rollback-to-embedded for Expo.
|
|
67
|
+
- It does not support a vanilla RN update protocol.
|
|
68
|
+
- It does not expose low-cost immutable public asset URLs cleanly.
|
|
69
|
+
|
|
70
|
+
### Dashboard
|
|
71
|
+
|
|
72
|
+
Existing dashboard docs only mention `mach update` in the command list. There is no OTA updates screen, update history, rollback UI, rollout UI, channel UI, or runtime compatibility explanation.
|
|
73
|
+
|
|
74
|
+
### Existing Mach Patterns To Reuse
|
|
75
|
+
|
|
76
|
+
Deep-link automation already solved several UX problems that OTA should copy:
|
|
77
|
+
|
|
78
|
+
- Compact Mach config.
|
|
79
|
+
- Command flags have highest precedence.
|
|
80
|
+
- Auto-detect framework when config is missing.
|
|
81
|
+
- Prompt only for missing values.
|
|
82
|
+
- Expo config plugin with packaged plugin preferred.
|
|
83
|
+
- Local fallback plugin only when package plugin cannot be used.
|
|
84
|
+
- `setup` runs `verify` at the end.
|
|
85
|
+
- Dynamic Expo config is patched only when safe.
|
|
86
|
+
|
|
87
|
+
Build framework adapters also provide a good pattern:
|
|
88
|
+
|
|
89
|
+
- `src/lib/builds/frameworks/expo.ts`
|
|
90
|
+
- `src/lib/builds/frameworks/react-native.ts`
|
|
91
|
+
- `src/lib/builds/frameworks/types.ts`
|
|
92
|
+
|
|
93
|
+
OTA should introduce a parallel adapter boundary rather than growing `src/commands/update.ts`.
|
|
94
|
+
|
|
95
|
+
## External Constraints
|
|
96
|
+
|
|
97
|
+
Expo:
|
|
98
|
+
|
|
99
|
+
- `expo-updates` can point at a custom update server by setting `updates.url` and `runtimeVersion`.
|
|
100
|
+
- The custom server must implement the Expo Updates protocol.
|
|
101
|
+
- Expo update manifests include `id`, `createdAt`, `runtimeVersion`, `launchAsset`, `assets`, `metadata`, and `extra`.
|
|
102
|
+
- Runtime version is the compatibility boundary between native binary and JS/assets.
|
|
103
|
+
|
|
104
|
+
Vanilla React Native:
|
|
105
|
+
|
|
106
|
+
- OTA can update JavaScript bundles and static assets only.
|
|
107
|
+
- OTA must not be published when native files, native dependencies, pods, Gradle, permissions, entitlements, or manifest settings changed without a new binary.
|
|
108
|
+
- Vanilla RN should not require Expo unless the user explicitly chooses `expo-updates`.
|
|
109
|
+
|
|
110
|
+
CodePush/App Center:
|
|
111
|
+
|
|
112
|
+
- App Center features, including CodePush service, were retired after March 31, 2025.
|
|
113
|
+
- Microsoft published standalone CodePush server/client compatibility material, but those repos are archived.
|
|
114
|
+
- Mach should not depend on App Center or treat CodePush as the core architecture.
|
|
115
|
+
|
|
116
|
+
References:
|
|
117
|
+
|
|
118
|
+
- https://docs.expo.dev/versions/latest/sdk/updates/
|
|
119
|
+
- https://docs.expo.dev/technical-specs/expo-updates-1/
|
|
120
|
+
- https://docs.expo.dev/eas-update/runtime-versions/
|
|
121
|
+
- https://learn.microsoft.com/en-us/appcenter/retirement
|
|
122
|
+
|
|
123
|
+
## Product Decision
|
|
124
|
+
|
|
125
|
+
Use a new command namespace:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
mach ota
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Keep legacy compatibility:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
mach update
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`mach update` should become a deprecated alias for:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
mach ota publish
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Show a warning:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
mach update is deprecated. Use mach ota publish.
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Command Surface
|
|
150
|
+
|
|
151
|
+
Primary commands:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
mach ota setup
|
|
155
|
+
mach ota publish
|
|
156
|
+
mach ota verify
|
|
157
|
+
mach ota list
|
|
158
|
+
mach ota rollback
|
|
159
|
+
mach ota promote
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Useful flags:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
mach ota setup --profile production
|
|
166
|
+
mach ota setup --platform ios
|
|
167
|
+
mach ota setup --platform android
|
|
168
|
+
mach ota setup --engine expo-updates
|
|
169
|
+
mach ota setup --engine mach-react-native
|
|
170
|
+
mach ota setup --yes
|
|
171
|
+
|
|
172
|
+
mach ota publish --channel production
|
|
173
|
+
mach ota publish --profile production
|
|
174
|
+
mach ota publish --platform all
|
|
175
|
+
mach ota publish --message "Fix login crash"
|
|
176
|
+
mach ota publish --runtime-version 1.2.0
|
|
177
|
+
mach ota publish --rollout 10
|
|
178
|
+
mach ota publish --mandatory
|
|
179
|
+
mach ota publish --skip-native-check
|
|
180
|
+
|
|
181
|
+
mach ota verify
|
|
182
|
+
mach ota verify --live
|
|
183
|
+
mach ota verify --native-check
|
|
184
|
+
|
|
185
|
+
mach ota list --channel production
|
|
186
|
+
mach ota rollback --channel production
|
|
187
|
+
mach ota rollback --to <updateId>
|
|
188
|
+
mach ota promote --from staging --to production --update-id <updateId>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`--skip-native-check` should be dangerous and explicit. It should require `--yes` in CI or confirmation interactively.
|
|
192
|
+
|
|
193
|
+
## Config Shape
|
|
194
|
+
|
|
195
|
+
Use a new `ota` block for clarity. Read legacy `update` as a backward-compatible alias, but migrate it during `mach ota setup`.
|
|
196
|
+
|
|
197
|
+
Minimal generated config:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"framework": "expo",
|
|
202
|
+
"projectId": "project-id",
|
|
203
|
+
"ota": {
|
|
204
|
+
"channel": "production"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Full shape:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"ota": {
|
|
214
|
+
"engine": "auto",
|
|
215
|
+
"channel": "production",
|
|
216
|
+
"runtimeVersionPolicy": "nativeFingerprint",
|
|
217
|
+
"runtimeVersion": null,
|
|
218
|
+
"url": "https://mach-api.securejs.in/api/ota/expo/project-id",
|
|
219
|
+
"checkAutomatically": "ON_LOAD",
|
|
220
|
+
"fallbackToCacheTimeout": 0,
|
|
221
|
+
"codeSigning": {
|
|
222
|
+
"enabled": false,
|
|
223
|
+
"keyId": "root"
|
|
224
|
+
},
|
|
225
|
+
"reactNative": {
|
|
226
|
+
"entryFile": "index.js",
|
|
227
|
+
"iosBundleOutput": "main.jsbundle",
|
|
228
|
+
"androidBundleOutput": "index.android.bundle"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Keep config compact. Do not duplicate values already available elsewhere:
|
|
235
|
+
|
|
236
|
+
- `projectId` from root config.
|
|
237
|
+
- platform package/bundle identifiers from root `android` and `ios` config.
|
|
238
|
+
- profile-specific platform settings from `build.<profile>`.
|
|
239
|
+
- Expo `runtimeVersion` from app config when already present.
|
|
240
|
+
|
|
241
|
+
Precedence:
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
command flags
|
|
245
|
+
> ota config
|
|
246
|
+
> legacy update config
|
|
247
|
+
> selected build profile
|
|
248
|
+
> root mach.config.json
|
|
249
|
+
> Expo app config/native files/package.json
|
|
250
|
+
> prompts/defaults
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Engine Detection
|
|
254
|
+
|
|
255
|
+
`ota.engine` should be optional.
|
|
256
|
+
|
|
257
|
+
Supported values:
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
auto
|
|
261
|
+
expo-updates
|
|
262
|
+
mach-react-native
|
|
263
|
+
codepush-compatible
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Initial implementation should support:
|
|
267
|
+
|
|
268
|
+
- `expo-updates`
|
|
269
|
+
- `mach-react-native`
|
|
270
|
+
|
|
271
|
+
Later implementation may add:
|
|
272
|
+
|
|
273
|
+
- `codepush-compatible`
|
|
274
|
+
|
|
275
|
+
Detection rules:
|
|
276
|
+
|
|
277
|
+
1. If command has `--engine`, use it.
|
|
278
|
+
2. Else if `ota.engine` exists and is not `auto`, use it.
|
|
279
|
+
3. Else if `framework` is `expo`, prefer `expo-updates`.
|
|
280
|
+
4. Else if `framework` is `react-native`, prefer `mach-react-native`.
|
|
281
|
+
5. Else inspect `package.json`, Expo config files, `ios/`, and `android/`.
|
|
282
|
+
6. If ambiguous and interactive, ask.
|
|
283
|
+
7. If ambiguous and non-interactive, fail with a clear message.
|
|
284
|
+
|
|
285
|
+
Do not require users to set `engine` in normal projects.
|
|
286
|
+
|
|
287
|
+
## Runtime Version Strategy
|
|
288
|
+
|
|
289
|
+
Runtime version is required for OTA.
|
|
290
|
+
|
|
291
|
+
Definition:
|
|
292
|
+
|
|
293
|
+
```text
|
|
294
|
+
The native runtime compatibility identifier for a build. An OTA update may only run on app binaries with the same runtime version.
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Mach should support these policies:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
manual use ota.runtimeVersion or --runtime-version
|
|
301
|
+
appVersion use app version
|
|
302
|
+
nativeVersion use app version plus iOS build number or Android version code
|
|
303
|
+
nativeFingerprint hash native-affecting files and dependency metadata
|
|
304
|
+
latestBuild use latest successful Mach build runtime for profile/channel
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Default:
|
|
308
|
+
|
|
309
|
+
```text
|
|
310
|
+
nativeFingerprint for new Mach OTA setup
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Why:
|
|
314
|
+
|
|
315
|
+
- `package.json` version is not enough.
|
|
316
|
+
- app version is easy but can be forgotten.
|
|
317
|
+
- native fingerprint is safer because native-affecting changes create a new runtime.
|
|
318
|
+
- latest successful Mach build should be used when publishing after a Mach build.
|
|
319
|
+
|
|
320
|
+
Runtime version resolution:
|
|
321
|
+
|
|
322
|
+
1. `--runtime-version`
|
|
323
|
+
2. `ota.runtimeVersion`
|
|
324
|
+
3. Expo platform-specific `runtimeVersion`
|
|
325
|
+
4. Expo top-level `runtimeVersion`
|
|
326
|
+
5. latest successful Mach build runtime for `projectId`, `profile`, `platform`
|
|
327
|
+
6. computed `runtimeVersionPolicy`
|
|
328
|
+
7. fail with setup guidance
|
|
329
|
+
|
|
330
|
+
Build records must be extended to store:
|
|
331
|
+
|
|
332
|
+
```json
|
|
333
|
+
{
|
|
334
|
+
"runtimeVersions": {
|
|
335
|
+
"ios": "rv-ios",
|
|
336
|
+
"android": "rv-android"
|
|
337
|
+
},
|
|
338
|
+
"nativeFingerprints": {
|
|
339
|
+
"ios": "hash",
|
|
340
|
+
"android": "hash"
|
|
341
|
+
},
|
|
342
|
+
"ota": {
|
|
343
|
+
"enabled": true,
|
|
344
|
+
"engine": "expo-updates",
|
|
345
|
+
"channel": "production"
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Native Change Guard
|
|
351
|
+
|
|
352
|
+
Before publish, Mach should compare the current native fingerprint to the latest compatible build fingerprint.
|
|
353
|
+
|
|
354
|
+
Native-affecting files include:
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
package.json
|
|
358
|
+
package-lock.json
|
|
359
|
+
yarn.lock
|
|
360
|
+
pnpm-lock.yaml
|
|
361
|
+
bun.lockb
|
|
362
|
+
app.json
|
|
363
|
+
app.config.js
|
|
364
|
+
app.config.ts
|
|
365
|
+
expo-plugin files
|
|
366
|
+
ios/Podfile
|
|
367
|
+
ios/Podfile.lock
|
|
368
|
+
ios/*.xcodeproj/project.pbxproj
|
|
369
|
+
ios/**/Info.plist
|
|
370
|
+
ios/**/*.entitlements
|
|
371
|
+
android/build.gradle
|
|
372
|
+
android/settings.gradle
|
|
373
|
+
android/gradle.properties
|
|
374
|
+
android/app/build.gradle
|
|
375
|
+
android/app/src/main/AndroidManifest.xml
|
|
376
|
+
react-native.config.js
|
|
377
|
+
metro.config.js
|
|
378
|
+
babel.config.js
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Dependency classification:
|
|
382
|
+
|
|
383
|
+
- Any added/removed package that contains native code should block OTA.
|
|
384
|
+
- Expo modules, React Native modules, Firebase, maps, camera, permissions, notifications, and other native libraries should be treated as native-affecting.
|
|
385
|
+
- Pure JS dependencies can be allowed.
|
|
386
|
+
|
|
387
|
+
Initial implementation can be conservative:
|
|
388
|
+
|
|
389
|
+
- If lockfile or native config changed since latest runtime and Mach cannot prove it is safe, block OTA.
|
|
390
|
+
- Print the files that changed.
|
|
391
|
+
- Recommend `mach build`.
|
|
392
|
+
|
|
393
|
+
## Storage Model
|
|
394
|
+
|
|
395
|
+
Use Cloudflare R2 as the default OTA artifact store. R2 avoids the large egress
|
|
396
|
+
cost that OTA assets can create on S3, while Mach API keeps private R2
|
|
397
|
+
credentials server-side and issues short-lived upload URLs to the CLI. S3 can
|
|
398
|
+
remain a fallback for self-hosted/internal deployments, but managed Mach OTA
|
|
399
|
+
should use R2.
|
|
400
|
+
|
|
401
|
+
Prefix:
|
|
402
|
+
|
|
403
|
+
```text
|
|
404
|
+
ota/<projectId>/updates/<updateId>/
|
|
405
|
+
ota/<projectId>/<platform>/<runtimeVersion>/<channel>/manifest.json
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Objects:
|
|
409
|
+
|
|
410
|
+
```text
|
|
411
|
+
ota/<projectId>/updates/<updateId>/<platform>/manifest.json
|
|
412
|
+
ota/<projectId>/updates/<updateId>/<platform>/metadata.json
|
|
413
|
+
ota/<projectId>/updates/<updateId>/<platform>/main.jsbundle
|
|
414
|
+
ota/<projectId>/updates/<updateId>/<platform>/assets/<hash>.<ext>
|
|
415
|
+
ota/<projectId>/<platform>/<runtimeVersion>/<channel>/manifest.json
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Rules:
|
|
419
|
+
|
|
420
|
+
- Assets are immutable.
|
|
421
|
+
- Assets should use content hash names.
|
|
422
|
+
- Manifest points to stable public R2 custom-domain URLs.
|
|
423
|
+
- R2 credentials are never shipped to mobile apps.
|
|
424
|
+
- R2 lifecycle rules can expire old inactive updates after a configurable retention period.
|
|
425
|
+
- Active updates must not be deleted while clients can request them.
|
|
426
|
+
|
|
427
|
+
Cost posture:
|
|
428
|
+
|
|
429
|
+
- No EAS Update cost.
|
|
430
|
+
- No App Center cost.
|
|
431
|
+
- No extra server if using existing Mach API.
|
|
432
|
+
- No managed OTA SaaS cost.
|
|
433
|
+
- Use DynamoDB `PAY_PER_REQUEST`.
|
|
434
|
+
- Use R2 public custom domain for assets and manifest pointers.
|
|
435
|
+
- Use Cloudflare cache/WAF controls on the custom domain when needed.
|
|
436
|
+
|
|
437
|
+
The only unavoidable costs are usage-based:
|
|
438
|
+
|
|
439
|
+
- R2 storage and operations.
|
|
440
|
+
- DynamoDB reads/writes.
|
|
441
|
+
- Mach API traffic.
|
|
442
|
+
|
|
443
|
+
## DynamoDB Model
|
|
444
|
+
|
|
445
|
+
Create a proper `MachUpdates` table.
|
|
446
|
+
|
|
447
|
+
Recommended key:
|
|
448
|
+
|
|
449
|
+
```text
|
|
450
|
+
PK: projectId
|
|
451
|
+
SK: updateId
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Recommended GSIs:
|
|
455
|
+
|
|
456
|
+
```text
|
|
457
|
+
ChannelRuntimeIndex
|
|
458
|
+
PK: projectChannelRuntimePlatform
|
|
459
|
+
SK: createdAt
|
|
460
|
+
|
|
461
|
+
ChannelIndex
|
|
462
|
+
PK: projectChannel
|
|
463
|
+
SK: createdAt
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Derived keys:
|
|
467
|
+
|
|
468
|
+
```text
|
|
469
|
+
projectChannel = <projectId>#<channel>
|
|
470
|
+
projectChannelRuntimePlatform = <projectId>#<channel>#<runtimeVersion>#<platform>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Dashboard counters are aggregate fields on the update record. Do not write one
|
|
474
|
+
row per user or one row per download:
|
|
475
|
+
|
|
476
|
+
```text
|
|
477
|
+
statsTotalIosDownloaded
|
|
478
|
+
statsTotalIosApplied
|
|
479
|
+
statsTotalIosFailed
|
|
480
|
+
statsTotalAndroidDownloaded
|
|
481
|
+
statsTotalAndroidApplied
|
|
482
|
+
statsTotalAndroidFailed
|
|
483
|
+
statsDailyYYYYMMDDIosDownloaded
|
|
484
|
+
statsDailyYYYYMMDDAndroidDownloaded
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
This keeps dashboard counts accurate enough, private, and very cheap while
|
|
488
|
+
still allowing daily charts.
|
|
489
|
+
|
|
490
|
+
Item:
|
|
491
|
+
|
|
492
|
+
```json
|
|
493
|
+
{
|
|
494
|
+
"projectId": "project-id",
|
|
495
|
+
"updateId": "uuid",
|
|
496
|
+
"channel": "production",
|
|
497
|
+
"platform": "ios",
|
|
498
|
+
"engine": "expo-updates",
|
|
499
|
+
"runtimeVersion": "runtime",
|
|
500
|
+
"runtimeVersionPolicy": "nativeFingerprint",
|
|
501
|
+
"nativeFingerprint": "hash",
|
|
502
|
+
"manifestKey": "ota/project/update/manifest.json",
|
|
503
|
+
"launchAssetKey": "ota/project/update/ios/main.jsbundle",
|
|
504
|
+
"assetKeys": [],
|
|
505
|
+
"message": "Fix login crash",
|
|
506
|
+
"commitSha": "abc123",
|
|
507
|
+
"branch": "main",
|
|
508
|
+
"status": "active",
|
|
509
|
+
"rollout": 100,
|
|
510
|
+
"mandatory": false,
|
|
511
|
+
"createdAt": 1783750000000,
|
|
512
|
+
"createdBy": "user-id",
|
|
513
|
+
"promotedFrom": null,
|
|
514
|
+
"disabledAt": null,
|
|
515
|
+
"rollbackOf": null
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Statuses:
|
|
520
|
+
|
|
521
|
+
```text
|
|
522
|
+
draft
|
|
523
|
+
active
|
|
524
|
+
disabled
|
|
525
|
+
rolled_back
|
|
526
|
+
deleted
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Only `active` updates are served to clients.
|
|
530
|
+
|
|
531
|
+
## API Plan
|
|
532
|
+
|
|
533
|
+
Replace the current generic manifest route with project-scoped routes.
|
|
534
|
+
|
|
535
|
+
Publish and management:
|
|
536
|
+
|
|
537
|
+
```text
|
|
538
|
+
POST /projects/:projectId/ota/upload-url
|
|
539
|
+
POST /projects/:projectId/ota/publish
|
|
540
|
+
GET /projects/:projectId/ota/updates
|
|
541
|
+
GET /projects/:projectId/ota/updates/:updateId
|
|
542
|
+
POST /projects/:projectId/ota/updates/:updateId/promote
|
|
543
|
+
POST /projects/:projectId/ota/channels/:channel/rollback
|
|
544
|
+
PATCH /projects/:projectId/ota/updates/:updateId
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Client manifest routes:
|
|
548
|
+
|
|
549
|
+
```text
|
|
550
|
+
GET /api/ota/expo/:projectId
|
|
551
|
+
GET /api/ota/react-native/:projectId
|
|
552
|
+
GET /api/ota/assets/:projectId/:updateId/:assetName
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Expo manifest request:
|
|
556
|
+
|
|
557
|
+
- Read `expo-runtime-version`.
|
|
558
|
+
- Read `expo-platform`.
|
|
559
|
+
- Read `expo-channel-name` or configured request header.
|
|
560
|
+
- Query `ChannelRuntimeIndex`.
|
|
561
|
+
- Apply rollout gating.
|
|
562
|
+
- Return Expo Updates protocol response.
|
|
563
|
+
- Return `204` if no update is available.
|
|
564
|
+
|
|
565
|
+
Vanilla RN manifest request:
|
|
566
|
+
|
|
567
|
+
- Read app id, channel, runtime version, platform, current update id.
|
|
568
|
+
- Query the same update table.
|
|
569
|
+
- Return Mach RN manifest.
|
|
570
|
+
- Return no-op when already current.
|
|
571
|
+
|
|
572
|
+
Important fix:
|
|
573
|
+
|
|
574
|
+
- Never point `manifestKey` to a zip.
|
|
575
|
+
- Store and serve a real manifest JSON.
|
|
576
|
+
- Store bundle/assets separately.
|
|
577
|
+
|
|
578
|
+
## Expo Engine
|
|
579
|
+
|
|
580
|
+
Engine id:
|
|
581
|
+
|
|
582
|
+
```text
|
|
583
|
+
expo-updates
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Setup:
|
|
587
|
+
|
|
588
|
+
1. Ensure project has `expo-updates`.
|
|
589
|
+
2. Configure Expo app with Mach update URL.
|
|
590
|
+
3. Configure runtime version.
|
|
591
|
+
4. Configure request headers for channel if needed.
|
|
592
|
+
5. Add/use `@radhya/mach/expo-plugin` when possible.
|
|
593
|
+
6. Avoid writing any EAS Update URL.
|
|
594
|
+
7. Warn when existing `updates.url` points to `https://u.expo.dev`.
|
|
595
|
+
8. Ask before replacing the URL, unless `--yes`.
|
|
596
|
+
|
|
597
|
+
Expo config plugin should apply:
|
|
598
|
+
|
|
599
|
+
```js
|
|
600
|
+
config.updates = {
|
|
601
|
+
...(config.updates || {}),
|
|
602
|
+
url: `https://mach-api.securejs.in/api/ota/expo/${projectId}`,
|
|
603
|
+
checkAutomatically: ota.checkAutomatically || 'ON_LOAD',
|
|
604
|
+
fallbackToCacheTimeout: ota.fallbackToCacheTimeout || 0,
|
|
605
|
+
}
|
|
606
|
+
config.runtimeVersion = resolvedRuntimeVersion
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Publishing:
|
|
610
|
+
|
|
611
|
+
1. Resolve project/channel/platform/profile/runtime.
|
|
612
|
+
2. Run native-change guard.
|
|
613
|
+
3. Export bundles/assets with Expo tooling.
|
|
614
|
+
4. Convert export output into Expo Updates manifest.
|
|
615
|
+
5. Upload launch asset and assets individually.
|
|
616
|
+
6. Save manifest JSON.
|
|
617
|
+
7. Publish update metadata.
|
|
618
|
+
8. Print dashboard link and next verification command.
|
|
619
|
+
|
|
620
|
+
Open implementation question:
|
|
621
|
+
|
|
622
|
+
- Validate exact Expo export output across current SDK versions before coding. The implementation must parse the actual export metadata rather than assume folder shape.
|
|
623
|
+
|
|
624
|
+
## Vanilla React Native Engine
|
|
625
|
+
|
|
626
|
+
Engine id:
|
|
627
|
+
|
|
628
|
+
```text
|
|
629
|
+
mach-react-native
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Setup:
|
|
633
|
+
|
|
634
|
+
1. Install or verify a Mach RN OTA client package.
|
|
635
|
+
2. Patch iOS native loader to ask Mach client for a JS bundle path before falling back to embedded bundle.
|
|
636
|
+
3. Patch Android native loader similarly.
|
|
637
|
+
4. Store Mach update URL, channel, runtime version, and app id in native config.
|
|
638
|
+
5. Do not require Expo.
|
|
639
|
+
|
|
640
|
+
Possible package names:
|
|
641
|
+
|
|
642
|
+
```text
|
|
643
|
+
@radhya/mach-react-native
|
|
644
|
+
@radhya/mach-ota-react-native
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
The client package should support:
|
|
648
|
+
|
|
649
|
+
- check for update
|
|
650
|
+
- download update
|
|
651
|
+
- install on next restart
|
|
652
|
+
- reload now
|
|
653
|
+
- rollback to previous working update
|
|
654
|
+
- mandatory update handling
|
|
655
|
+
- current update id reporting
|
|
656
|
+
- binary embedded fallback
|
|
657
|
+
|
|
658
|
+
Publishing:
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output build/mach-ota/ios/main.jsbundle --assets-dest build/mach-ota/ios/assets
|
|
662
|
+
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output build/mach-ota/android/index.android.bundle --assets-dest build/mach-ota/android/res
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
The exact command should use project config:
|
|
666
|
+
|
|
667
|
+
- `ota.reactNative.entryFile`
|
|
668
|
+
- custom Metro config when present
|
|
669
|
+
- platform selection
|
|
670
|
+
- Hermes bytecode decision later
|
|
671
|
+
|
|
672
|
+
Manifest shape:
|
|
673
|
+
|
|
674
|
+
```json
|
|
675
|
+
{
|
|
676
|
+
"id": "update-id",
|
|
677
|
+
"platform": "ios",
|
|
678
|
+
"channel": "production",
|
|
679
|
+
"runtimeVersion": "runtime",
|
|
680
|
+
"mandatory": false,
|
|
681
|
+
"rollout": 100,
|
|
682
|
+
"createdAt": "2026-07-11T00:00:00.000Z",
|
|
683
|
+
"bundle": {
|
|
684
|
+
"url": "https://mach-api.securejs.in/api/ota/assets/...",
|
|
685
|
+
"hash": "sha256"
|
|
686
|
+
},
|
|
687
|
+
"assets": [
|
|
688
|
+
{
|
|
689
|
+
"url": "https://mach-api.securejs.in/api/ota/assets/...",
|
|
690
|
+
"hash": "sha256",
|
|
691
|
+
"path": "assets/image.png"
|
|
692
|
+
}
|
|
693
|
+
]
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## CodePush Compatibility
|
|
698
|
+
|
|
699
|
+
Do not make CodePush the core.
|
|
700
|
+
|
|
701
|
+
Later, add optional compatibility:
|
|
702
|
+
|
|
703
|
+
```json
|
|
704
|
+
{
|
|
705
|
+
"ota": {
|
|
706
|
+
"engine": "codepush-compatible"
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
This can help existing vanilla RN users migrate, but it should adapt Mach's update records to a CodePush-like client protocol. It should not depend on App Center service.
|
|
712
|
+
|
|
713
|
+
## Rollout Logic
|
|
714
|
+
|
|
715
|
+
Rollout should be deterministic per installation.
|
|
716
|
+
|
|
717
|
+
Inputs:
|
|
718
|
+
|
|
719
|
+
```text
|
|
720
|
+
projectId
|
|
721
|
+
channel
|
|
722
|
+
updateId
|
|
723
|
+
installationId
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Algorithm:
|
|
727
|
+
|
|
728
|
+
1. Hash inputs.
|
|
729
|
+
2. Convert hash to bucket 0-99.
|
|
730
|
+
3. Serve update if bucket is less than rollout percentage.
|
|
731
|
+
|
|
732
|
+
The client needs a stable anonymous installation id. For Expo, use headers if practical or store in app code with `expo-updates` request headers later. For initial implementation, rollout may be server-side by device id only for Mach RN and disabled for Expo until stable headers are solved.
|
|
733
|
+
|
|
734
|
+
## Rollback
|
|
735
|
+
|
|
736
|
+
Expo rollback:
|
|
737
|
+
|
|
738
|
+
- Prefer serving a directive compatible with Expo Updates protocol for rollback-to-embedded when rolling back all updates for a runtime.
|
|
739
|
+
- For normal rollback to a previous update, mark newer update disabled and serve the previous active update.
|
|
740
|
+
|
|
741
|
+
Vanilla RN rollback:
|
|
742
|
+
|
|
743
|
+
- Mark update disabled.
|
|
744
|
+
- Client should keep previous working update locally.
|
|
745
|
+
- Server can return previous active update or no-op.
|
|
746
|
+
|
|
747
|
+
Dashboard rollback should:
|
|
748
|
+
|
|
749
|
+
1. Show current active update.
|
|
750
|
+
2. Show compatible previous updates.
|
|
751
|
+
3. Confirm affected channel/platform/runtime.
|
|
752
|
+
4. Record who performed rollback.
|
|
753
|
+
|
|
754
|
+
## Security
|
|
755
|
+
|
|
756
|
+
Initial:
|
|
757
|
+
|
|
758
|
+
- Only authenticated developers can publish updates.
|
|
759
|
+
- Client manifest routes are public but project-scoped.
|
|
760
|
+
- Asset URLs are immutable and can be public or signed.
|
|
761
|
+
- Never expose secrets in manifests.
|
|
762
|
+
|
|
763
|
+
Phase 2:
|
|
764
|
+
|
|
765
|
+
- Add manifest code signing.
|
|
766
|
+
- Store public certificate in app config.
|
|
767
|
+
- Store private signing key in Mach secrets.
|
|
768
|
+
- Sign manifest/directives on API or CLI.
|
|
769
|
+
- Verify signatures in Expo when configured.
|
|
770
|
+
- Add Mach RN client signature verification.
|
|
771
|
+
|
|
772
|
+
## Dashboard Plan
|
|
773
|
+
|
|
774
|
+
Add an OTA tab or project subpage:
|
|
775
|
+
|
|
776
|
+
```text
|
|
777
|
+
Project -> OTA Updates
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
Views:
|
|
781
|
+
|
|
782
|
+
- Channels.
|
|
783
|
+
- Active update per channel/platform/runtime.
|
|
784
|
+
- Update history.
|
|
785
|
+
- Runtime versions.
|
|
786
|
+
- Rollout percentage.
|
|
787
|
+
- Publish source: CLI user, commit, branch.
|
|
788
|
+
- Rollback/promote actions.
|
|
789
|
+
- Setup instructions.
|
|
790
|
+
|
|
791
|
+
Build detail page:
|
|
792
|
+
|
|
793
|
+
- Show runtime versions generated by the build.
|
|
794
|
+
- Show whether OTA is enabled for that build.
|
|
795
|
+
- Show compatible update channel.
|
|
796
|
+
|
|
797
|
+
Docs:
|
|
798
|
+
|
|
799
|
+
- Add a full OTA docs page.
|
|
800
|
+
- Explain that Mach does not use EAS Update.
|
|
801
|
+
- Explain Expo custom server setup.
|
|
802
|
+
- Explain vanilla RN Mach client setup.
|
|
803
|
+
- Explain runtime version and native-change guard.
|
|
804
|
+
- Explain cost model.
|
|
805
|
+
|
|
806
|
+
## Cost Plan
|
|
807
|
+
|
|
808
|
+
No new fixed-cost service is required for v1.
|
|
809
|
+
|
|
810
|
+
Use:
|
|
811
|
+
|
|
812
|
+
- existing Mach API
|
|
813
|
+
- Cloudflare R2 for OTA bundles/assets/manifests
|
|
814
|
+
- DynamoDB on-demand table
|
|
815
|
+
- existing dashboard
|
|
816
|
+
|
|
817
|
+
Avoid:
|
|
818
|
+
|
|
819
|
+
- EAS Update
|
|
820
|
+
- App Center
|
|
821
|
+
- new always-on worker
|
|
822
|
+
- proxying OTA asset downloads through Mach API
|
|
823
|
+
|
|
824
|
+
Controls:
|
|
825
|
+
|
|
826
|
+
- Store immutable assets once by hash.
|
|
827
|
+
- Reuse identical assets across updates later.
|
|
828
|
+
- Retention policy for inactive updates.
|
|
829
|
+
- Compression for JS bundles.
|
|
830
|
+
- Warn about large assets.
|
|
831
|
+
- Dashboard storage usage per project.
|
|
832
|
+
|
|
833
|
+
## Implementation Files
|
|
834
|
+
|
|
835
|
+
CLI:
|
|
836
|
+
|
|
837
|
+
```text
|
|
838
|
+
src/commands/ota.ts
|
|
839
|
+
src/lib/ota/types.ts
|
|
840
|
+
src/lib/ota/config.ts
|
|
841
|
+
src/lib/ota/detect.ts
|
|
842
|
+
src/lib/ota/runtime.ts
|
|
843
|
+
src/lib/ota/native-fingerprint.ts
|
|
844
|
+
src/lib/ota/verify.ts
|
|
845
|
+
src/lib/ota/engines/index.ts
|
|
846
|
+
src/lib/ota/engines/expo-updates.ts
|
|
847
|
+
src/lib/ota/engines/mach-react-native.ts
|
|
848
|
+
src/lib/ota/expo-plugin.ts
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Packaged Expo plugin:
|
|
852
|
+
|
|
853
|
+
```text
|
|
854
|
+
expo-plugin/index.js
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Add OTA behavior to the existing plugin or split:
|
|
858
|
+
|
|
859
|
+
```text
|
|
860
|
+
@radhya/mach/expo-plugin
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
The plugin can configure both deep links and OTA from `mach.config.json`.
|
|
864
|
+
|
|
865
|
+
API:
|
|
866
|
+
|
|
867
|
+
```text
|
|
868
|
+
api/src/index.ts
|
|
869
|
+
api/scripts/create-updates-table.js
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
Dashboard:
|
|
873
|
+
|
|
874
|
+
```text
|
|
875
|
+
app/(tabs)/ota.tsx
|
|
876
|
+
app/(tabs)/project/[id].tsx
|
|
877
|
+
app/(tabs)/build/[id].tsx
|
|
878
|
+
app/(tabs)/docs.tsx
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
Docs:
|
|
882
|
+
|
|
883
|
+
```text
|
|
884
|
+
docs/ota-implementation-plan.md
|
|
885
|
+
docs/ota.md
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
## Phased Delivery
|
|
889
|
+
|
|
890
|
+
### Phase 0: Disable Broken Path Safely
|
|
891
|
+
|
|
892
|
+
- Keep `mach update` available, but do not let it pretend success if the API cannot serve the update.
|
|
893
|
+
- Add warning that current OTA is experimental until `mach ota` is ready.
|
|
894
|
+
- Avoid publishing zip-as-manifest records.
|
|
895
|
+
|
|
896
|
+
### Phase 1: Core Model And API
|
|
897
|
+
|
|
898
|
+
- Create `MachUpdates` table script with query-friendly indexes.
|
|
899
|
+
- Add project-scoped publish/list/get routes.
|
|
900
|
+
- Add R2 upload URLs for OTA objects.
|
|
901
|
+
- Store real manifest metadata.
|
|
902
|
+
- Add project-scoped Expo manifest route.
|
|
903
|
+
- Return `204` for no update.
|
|
904
|
+
|
|
905
|
+
### Phase 2: Runtime Version And Build Integration
|
|
906
|
+
|
|
907
|
+
- Add runtime version computation to Mach build.
|
|
908
|
+
- Store runtime versions and native fingerprints on build records.
|
|
909
|
+
- Add native fingerprint helper in CLI.
|
|
910
|
+
- Add latest-compatible-build resolution.
|
|
911
|
+
- Add native-change guard.
|
|
912
|
+
|
|
913
|
+
### Phase 3: Expo OTA
|
|
914
|
+
|
|
915
|
+
- Add `mach ota setup` for Expo.
|
|
916
|
+
- Configure `expo-updates` to use Mach URL.
|
|
917
|
+
- Add `ota` config writing and migration from legacy `update`.
|
|
918
|
+
- Add Expo export parsing.
|
|
919
|
+
- Generate valid Expo manifest.
|
|
920
|
+
- Upload individual immutable assets.
|
|
921
|
+
- Publish and verify.
|
|
922
|
+
- Confirm updates do not appear in Expo dashboard because `eas update` is never called.
|
|
923
|
+
|
|
924
|
+
### Phase 4: Vanilla React Native OTA
|
|
925
|
+
|
|
926
|
+
- Create Mach RN OTA client package.
|
|
927
|
+
- Add `mach ota setup` native patching for iOS and Android.
|
|
928
|
+
- Add `react-native bundle` export engine.
|
|
929
|
+
- Add Mach RN manifest route.
|
|
930
|
+
- Add install/reload/rollback client behavior.
|
|
931
|
+
- Verify on real iOS/Android app.
|
|
932
|
+
|
|
933
|
+
### Phase 5: Dashboard
|
|
934
|
+
|
|
935
|
+
- Add OTA update list page.
|
|
936
|
+
- Add rollback/promote actions.
|
|
937
|
+
- Add runtime compatibility display.
|
|
938
|
+
- Add docs.
|
|
939
|
+
|
|
940
|
+
### Phase 6: Rollout, Promotion, Signing, Retention
|
|
941
|
+
|
|
942
|
+
- Add rollout percentage.
|
|
943
|
+
- Add channel promotion.
|
|
944
|
+
- Add code signing.
|
|
945
|
+
- Add retention cleanup.
|
|
946
|
+
- Add asset dedupe.
|
|
947
|
+
|
|
948
|
+
## Acceptance Tests
|
|
949
|
+
|
|
950
|
+
Expo:
|
|
951
|
+
|
|
952
|
+
- `mach ota setup` updates Mach config and Expo plugin without EAS URL.
|
|
953
|
+
- Existing `updates.url` pointing to `u.expo.dev` is detected and replaced only with confirmation.
|
|
954
|
+
- `mach ota publish --channel staging` uploads real manifest and assets.
|
|
955
|
+
- Expo client requests Mach endpoint and receives valid Expo Updates manifest.
|
|
956
|
+
- Expo dashboard does not show the update.
|
|
957
|
+
- Native dependency change blocks OTA.
|
|
958
|
+
|
|
959
|
+
Vanilla RN:
|
|
960
|
+
|
|
961
|
+
- `mach ota setup` installs Mach RN client and patches native code.
|
|
962
|
+
- `mach ota publish --platform ios` runs RN bundler and uploads iOS bundle/assets.
|
|
963
|
+
- `mach ota publish --platform android` runs RN bundler and uploads Android bundle/assets.
|
|
964
|
+
- Client downloads update, restarts, and runs new JS.
|
|
965
|
+
- Bad update rolls back to previous working bundle.
|
|
966
|
+
- Native change blocks OTA.
|
|
967
|
+
|
|
968
|
+
API:
|
|
969
|
+
|
|
970
|
+
- Updates are scoped by project.
|
|
971
|
+
- Latest active update is returned for matching project/channel/platform/runtime.
|
|
972
|
+
- No matching update returns no-op/204.
|
|
973
|
+
- Disabled update is not served.
|
|
974
|
+
- Rollback serves previous active update.
|
|
975
|
+
|
|
976
|
+
Dashboard:
|
|
977
|
+
|
|
978
|
+
- Update appears in project OTA history.
|
|
979
|
+
- Runtime version is visible.
|
|
980
|
+
- Rollback action changes served update.
|
|
981
|
+
- Cost/storage data is visible or planned clearly.
|
|
982
|
+
|
|
983
|
+
## Open Questions
|
|
984
|
+
|
|
985
|
+
- Should the first Mach RN client use JSI/TurboModule, classic native module, or a minimal native bridge?
|
|
986
|
+
- Should Expo rollout wait until stable installation-id request headers are implemented?
|
|
987
|
+
- Should OTA assets be public immutable URLs or signed API URLs in v1?
|
|
988
|
+
- Should code signing be mandatory for production channels from day one?
|
|
989
|
+
- Should `ota.runtimeVersionPolicy` default to `nativeFingerprint` for all projects, or `latestBuild` after the first successful Mach build?
|
|
990
|
+
- Should `mach build` automatically create/update OTA channel metadata when `ota.enabled` is true?
|
|
991
|
+
|
|
992
|
+
## Recommended First Implementation Slice
|
|
993
|
+
|
|
994
|
+
Do not start with vanilla RN client code first. Start with the shared core.
|
|
995
|
+
|
|
996
|
+
1. Add `mach ota` namespace and make `mach update` a deprecated alias.
|
|
997
|
+
2. Add `ota` config type and migration from `update`.
|
|
998
|
+
3. Add runtime version and native fingerprint helpers.
|
|
999
|
+
4. Add `MachUpdates` table script and project-scoped API.
|
|
1000
|
+
5. Fix Expo path end to end because `expo-updates` already provides the native client.
|
|
1001
|
+
6. Add dashboard docs and update history.
|
|
1002
|
+
7. Then implement Mach RN client for vanilla React Native.
|
|
1003
|
+
|
|
1004
|
+
This keeps the first usable release smaller, fixes the broken current path, avoids new cost, and leaves a clean adapter boundary for vanilla React Native.
|