Skip to content

v10.51.0

Choose a tag to compare

@sunnylqm sunnylqm released this 12 Aug 09:12
· 44 commits to master since this release
260c6fd

Native cold-start check: devices bricked by a bad update can now rescue themselves (NATIVE_CHECKUPDATE_DESIGN §10)

Until now every bit of update logic ran in JS. If a bad hot update killed the JS thread, the device could never send another check request — reinstalling was the only way out, the most embarrassing dead corner of any OTA scheme. This release pushes the check down to native: on all three platforms, a background check runs a few seconds after every cold start, entirely independent of JS, so even a bricked device can pull the fix back down.

  • One decision core in C++: the new cpp/update_flow_core owns rollout matching, endpoint ordering, response parsing and download decisions. The TS side (src/updateFlowCore.ts) is a semantically identical reference implementation; the two are compared case-by-case in CI against 77 golden vectors (the C++ side replayed under ASan/UBSan). Any semantic drift turns CI red — three-platform consistency no longer depends on human eyes
  • JS is the single config source: syncNativeConfig hands appKey/endpoints/strategy to native; no config means no check — this is both the integration gate and the rollout switch
  • Activation authority: checkStrategy decides whether native may activate on its own. With checkStrategy: null, native downloads but never activates
  • forceBoot (the brick-rescue directive): the server can mark a delivery on its binding as force-boot; the native check then overrides the client-side strategy and boots the new version on the next cold start. This is the only channel that still works once JS is dead. Native-only — the JS interactive flow ignores it; the device-local rolledBack guard still wins, and first_time crash protection still applies
  • Concurrency and atomicity: a reset generation guard plus atomic commitNativeCheckResult (version info, activation, and response cache land under one lock), so a check result can never clobber a user-triggered resetToPackagedBundle
  • .pushy-complete marker: distinguishes fully installed directories from half-extracted ones — a partial extraction can never be booted as a usable version
  • JS reuses the native result: the JS check reuses the response the native check just fetched, saving one network round-trip per cold start
  • Monotonic clocks on all three platforms (iOS systemUptime, Android System.nanoTime(), Harmony getUptime(ACTIVE)), immune to user clock changes

New option disableNativeCheck

Turns off the native cold-start check. The cost is giving up the rescue path above (and the response cache JS reuses), in exchange for one fewer background request per cold start. Use it only when that request is itself the problem — traffic/battery budgets, privacy manifests, consent-gated networking.

Orthogonal to checkStrategy: checkStrategy governs whether native may activate; disableNativeCheck governs whether the check runs at all.

Binding-time dependency validation (server + console, already live)

Two hard rules now apply when binding a hot update to a native package; violations reject the binding outright:

  • A bundle built on a different React Native version is rejected
  • A bundle whose react-native-update version is lower than the native package's is rejected

In the console, the forceBoot option only appears when the native package's react-native-update is ≥ 10.51.0 — older clients have no native check, so the directive would never take effect.

CI

  • New e2e suite for the native cold-start check, covering Android new arch, Android old arch (RN 0.77.3) and iOS, verifying "activates only what the server forces, with no JS involved"
  • iOS e2e split into independent core / native jobs, so one timing out no longer masks the other's verdict
  • Fixed two timeouts that were killing healthy work: the artifact-prep script's 120s cap per CLI call (a cold Metro bundle on macOS runners routinely exceeds it) raised to 300s; the iOS job budget raised from 40 to 60 minutes (it ran --retries 1 without budgeting for a full retry)

Upgrade notes: no breaking changes — upgrade directly. The native cold-start check is on by default with no code changes required; using forceBoot for brick rescue requires the native package itself to be on 10.51.0 or later.


原生冷启动检测:被坏包打死的设备也能自救(NATIVE_CHECKUPDATE_DESIGN §10)

此前所有更新逻辑都跑在 JS 里。一旦某个热更包把 JS 打死,设备就再也发不出检查请求,只能等用户重装——这是热更方案最难堪的死角。本版把检查下沉到原生:三端在冷启动几秒后各自跑一次后台检查,完全不经过 JS,坏包装出来了也能把修复包拉回来。

  • 决策层统一到 C++:新增 cpp/update_flow_core,rollout 命中、端点排序、响应解析与下载决策全部由它裁决。TS 侧 src/updateFlowCore.ts 是同语义的参考实现,两者用 77 条 golden vector 在 CI 里逐条比对(C++ 侧在 ASan/UBSan 下重放),语义漂移会直接让 CI 红——不再靠人眼保证三端一致
  • 配置由 JS 单一下发:syncNativeConfig 把 appKey/端点/策略写给原生;没有配置就不检查,这既是接入闸门也是灰度开关
  • 激活权限:checkStrategy 决定原生能否自行激活。checkStrategy: null 时原生只下载不激活
  • forceBoot(救砖指令):服务端可在绑定上标记某次下发为强制启动,原生检查据此越过客户端策略,让设备下次冷启动直接跑新版本。这是 JS 已死时唯一还能生效的通路。仅作用于原生,JS 交互流程忽略它;设备本地的 rolledBack 守卫依然优先,first_time 崩溃保护依然生效
  • 并发与原子性:reset 代际守卫 + 原子提交 commitNativeCheckResult(版本信息、激活、响应缓存同一把锁内落盘),避免检查结果覆盖掉用户刚触发的 resetToPackagedBundle
  • .pushy-complete 标记:区分「装完的目录」与「解压到一半的目录」,半成品不再可能被当成可用版本启动
  • JS 复用原生结果:JS 侧检查直接复用原生刚拿到的响应缓存,冷启动少一次网络请求
  • 三端单调时钟(iOS systemUptime、Android System.nanoTime()、Harmony getUptime(ACTIVE)),不受用户改系统时间影响

新增开关 disableNativeCheck

关掉原生冷启动检查。代价是放弃上面那条救砖通路(以及 JS 能复用的响应缓存),换每次冷启动少一个后台请求。只在这个请求本身构成问题时才用——流量/电量预算、隐私清单、需要用户同意后才能联网等场景。

它与 checkStrategy 正交:checkStrategy 管的是「能不能自行激活」,disableNativeCheck 管的是「查不查」。

绑定期依赖校验(服务端 + 控制台,已上线)

绑定热更包到原生包时新增两条硬规则,不合规直接拒绝绑定:

  • RN 版本不一致的包,拒绝
  • react-native-update 版本比原生包低的包,拒绝

控制台侧,forceBoot 选项仅在原生包的 react-native-update ≥ 10.51.0 时才出现——低于此版本的客户端没有原生检查,发了也不会生效。

CI

  • 原生冷启动检测新增 e2e 用例,覆盖 Android 新架构、Android 旧架构(RN 0.77.3)与 iOS 三种组合,验证「只激活服务端强制的版本,全程不经 JS」
  • iOS e2e 拆成 core / native 两条独立 job,一条超时不再掩盖另一条的结论
  • 修掉两处把健康任务误杀的超时上限:出包脚本对 CLI 调用的 120s 上限(macOS runner 上冷启动 Metro 打包本就超过它)提到 300s;iOS job 的 40 分钟预算提到 60 分钟(此前开着 --retries 1 却不给重试留预算,重试跑到一半被腰斩)

升级提示:本版无破坏性变更,直接升级即可。原生冷启动检测默认开启,无需改代码;若要用 forceBoot 救砖,需要原生包本身已经是 10.51.0 或更高。