Skip to content

feat: add resilient native cold-start update checks - #620

Merged
sunnylqm merged 29 commits into
masterfrom
agent/harden-native-check-update
Aug 11, 2026
Merged

feat: add resilient native cold-start update checks#620
sunnylqm merged 29 commits into
masterfrom
agent/harden-native-check-update

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • extract the update-flow decision layer into a shared C++ implementation with golden-vector parity across Android, iOS, and Harmony
  • add bridge-independent native cold-start update checks so a bad OTA bundle can be replaced even when application JavaScript cannot boot
  • persist the JS-owned endpoint and activation configuration for native use, including per-version forceBoot handling
  • let JS safely reuse native check responses without duplicate requests
  • harden orchestration with rollback snapshots, transactional download completion markers, iOS same-hash download deduplication, request/config-scoped response caching, and malformed-response endpoint fallback

Why

Update checking previously depended on the application bundle reaching JavaScript. A broken hot update could therefore prevent the code responsible for fetching its replacement from running.

This change introduces a small native rescue path that runs after cold start, reuses the same decision semantics on every platform, and activates fixes only under the configured strategy. The final hardening pass addresses races found during review: rollback state could be consumed before the delayed check, interrupted extraction could look complete, iOS native and JS downloads could overwrite each other, cached responses could cross request/config boundaries, and malformed HTTP 2xx bodies could suppress endpoint fallback.

Impact

Applications can recover from a bricked OTA bundle on the next launch without changing the existing JS API or normal update strategy. Native work remains delayed and bounded, failures stay silent, and alert-style strategies continue to leave activation to JavaScript.

Validation

  • bun run lint
  • bun test src/__tests__ — 162 passed
  • ./scripts/test-update-flow-core.sh — 77 vectors passed
  • ./scripts/test-patch-core.sh — 29 tests passed
  • Android :react-native-update:compileReleaseJavaWithJavac
  • iOS react-native-update Pods target, Release simulator build
  • Harmony type check
  • node scripts/verify-android-so.js — arm64-v8a, armeabi-v7a, x86, and x86_64 passed
  • git diff --check

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Added native cold-start update checks across supported platforms with endpoint fallback and delayed background processing.
    • Added cached check results and native configuration synchronization.
    • Improved rollout-aware update selection, patch/full-download ordering, and next-launch activation.
    • Added recovery support for repaired versions after startup failures, including force-boot support.
  • Bug Fixes
    • Completion markers now prevent incomplete downloads from being treated as ready.
    • Improved handling of invalid responses, failures, timeouts, and interruptions without crashing.
  • Tests
    • Added cross-platform parity and sanitizer-tested update-flow coverage.

sunnylqm and others added 17 commits August 7, 2026 17:48
…Core

buildCheckRequestBody, resolveCheckResult, decideDownload, isInRollout,
joinUrls and orderEndpointCandidates now live in src/updateFlowCore.ts
with a pure import closure — no IO, no react-native, identity and
randomness injected as parameters — so the exact same source can later be
evaluated by the native orchestrator (NATIVE_CHECKUPDATE_DESIGN §5).
client.ts, provider.tsx and endpoint.ts consume it in place; the endpoint
candidate ordering (random first pick, configured order as fallback) moves
into the pure layer while the hedged-race engine stays IO-side. Behavior
is unchanged; isInRollout tests no longer need module mocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xt guardian, no override channel

Three decisions land in the design: (1) buildCheckRequest returns a
declarative endpoint plan and the native engine is a sequential fallback,
not a port of the JS hedged race — the cold-start background path is
latency-insensitive; (2) the guardian evaluates as plain-text JS source,
never HBC, removing the bytecode-version coupling and narrowing the engine
spike; (3) the remote override channel (§5.3-5.5) is cut — the decision
layer is the most testable part of the pipeline, the JS-side checkUpdate
remains a full fallback and the server can already route around decision
bugs by shaping responses, so the premium exceeds the exposure. Phase
status in §8 updated: protocol prerequisite lifted, pure-function
extraction done (updateFlowCore.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…latform

The bundled updateFlowCore (6.4KB plain-text JS, zero deps) produces
byte-identical correct output under bare JSC, Hermes (RN 0.73 VM, source
eval, no HBC) and V8, and a 40-line ObjC host evaluates it via JSContext
and calls decideDownload end to end — the iOS orchestrator shape, linking
only system frameworks. Since the source is the shared artifact, the
engine is now chosen per platform: iOS system JavaScriptCore (zero risk,
proven), Harmony system JSVM-API, Android remains the only open spike
(javascriptengine / Hermes linking / QuickJS; worst case Android alone
falls back to a C++ decision layer as a hybrid).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Symbol recon on the prebuilt hermes-android 250829098.0.10 (RN 0.85):
the stable C ABI header (hermes_abi.h) ships in the prefab but its entry
point get_hermes_abi_vtable is not exported from libhermesvm.so; only the
C++ makeHermesRuntime is, which couples the caller to the host's exact
RuntimeConfig layout and requires compiling a matching jsi.cpp (zero JSI
symbols exported). For a prebuilt librnupdate.so shipped across RN
versions that is an unmanageable matrix. Android candidates narrow to
androidx.javascriptengine (needs on-device verification) and QuickJS;
the route revives if upstream ever exports the stable C ABI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arity

cpp/update_flow_core is a 1:1 port of src/updateFlowCore.ts (the reference
implementation): Murmur3_32, IsInRollout, JoinUrls, OrderEndpointCandidates,
BuildCheckRequestBody, ResolveCheckResult and DecideDownload, built on a
minimal flow_json DOM that mirrors the JS semantics parity depends on —
insertion-ordered objects (overwrite keeps position), undefined distinct
from null and skipped by stringify, JS truthiness and strict equality.

Parity is a mechanically enforced contract, not a convention:
scripts/generate-flow-vectors.ts runs the TS reference over a dense input
matrix and writes cpp/update_flow_core/tests/flow_vectors.json (69 cases);
src/__tests__/flowVectors.test.ts fails when the TS side and the committed
vectors disagree, and scripts/test-update-flow-core.sh (wired into the CI
cpp-test job under ASan+UBSan) replays the same vectors against the C++
port. Semantic changes land TS-first, regenerate the vectors, then port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tforms

Records the reasoning chain that closes the runtime-selection question:
engines were only ever a means to a single source of truth; with the host
Hermes ruled out and javascriptengine only viable as a runtime-conditional
optimization, an always-available C++ implementation must exist on Android
anyway, at which point uniform C++ beats both the hybrid (two mechanisms)
and QuickJS-everywhere (200KB x 4 to save one mechanical, vector-guarded
edit). The single-source property is preserved as "one semantics,
mechanically enforced" via the golden-vector contract. The guardian
chapter is demoted to a rejected-route record; remaining work is §8 step 4
(native orchestration).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parser also consumes checkUpdate responses, so malformed input must
fail cleanly: recursive descent is now capped at 64 nesting levels (a
100k-bracket bomb previously walked straight toward the stack limit), and
the test binary gains a robustness suite — 20 malformed inputs that must
set ok=false without crashing (verified under ASan+UBSan in CI), plus
tricky-but-valid cases pinning JSON.parse semantics the port relies on
(duplicate keys keep first position with last value, surrogate-pair
decoding, canonical number reserialization).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…§10)

HandleCheckResponse composes Parse → ResolveCheckResult → DecideDownload
so platform orchestrators go from raw response text to a download decision
(with the resolved info attached for setLocalHashInfo) in one call,
containing zero decision logic; covered by nine composition cases in the
C++ test suite under ASan+UBSan.

The design doc gains §10: the provisioning gap surfaced by step 4 — JS is
the single config source, syncNativeConfig persists {appKey, endpoints,
queryUrls, afterDownload, rnu, rn} and missing config means the native
check silently does not run (which doubles as the rollout gate); the
cold-start flow, JS result-reuse handoff, bounded failure policy (no
retry storms, no version blacklisting — the local-watchdog lesson), and
the per-platform landing order (iOS first).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… check

First brick of NATIVE_CHECKUPDATE_DESIGN §10: the native check runs before
any JS, so its config must already be on disk. JS is the single config
source — after every setOptions the client persists {appKey, endpoints,
queryUrls, afterDownload, rnu, rn} through the new syncNativeConfig native
method (feature-detected, fire-and-forget, skipped on web). afterDownload
folds updateStrategy: silent strategies let the native side activate a
downloaded version for the next launch, alert strategies keep activation
with JS. Absent persisted config disables the native check entirely, which
doubles as the rollout gate: old integrations and first launches see zero
behavior change, and shipping this ahead of the orchestrator lets devices
accumulate config in the field.

All three platforms store the raw JSON (parsed on read by the future
orchestrator) and validate it at write time — a corrupt config would
otherwise silently disable the native check with no signal. Verified:
bun test (155) + tsc + biome, harmony tsc against the DevEco SDK, clang
-fsyntax-only for RCTPushy.mm against e2e Pods, javac of main+oldarch
against the RN 0.85 AAR (newarch mirrors setUuid; covered by CI codegen
builds).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RCTPushyOrchestrator implements NATIVE_CHECKUPDATE_DESIGN §10 on iOS: once
per process, five seconds after +bundleURL (the integration-guaranteed
anchor), a utility-QoS thread reads the JS-provisioned config (absent =
check disabled), builds the request through cpp/update_flow_core, walks
the ordered endpoints sequentially with per-request timeouts (queryUrls
discovery merges remote candidates after a failed round — no hedged race
on this latency-insensitive path), and hands the raw response to
HandleCheckResponse. A download decision drives the existing pipeline
through a bare bridge-less module instance (performUpdate; progress events
are hasListeners-gated), trying each attempt's candidate URLs in order:
diff → pdiff → full. On success the version info persists like
setLocalHashInfo, and under the silent strategies (afterDownload:
setNeedUpdate) the version activates for the next launch — which is the
whole point: a device bricked by a bad hot update pulls the fixed version
before any JS runs. The raw response is cached with a timestamp for the
JS-side reuse step (§10.3). Failures are silent and bounded: one round per
launch, no retry storms, no version blacklisting.

getBundleHash's compute is extracted into PushyBundleHashSync, shared with
the orchestrator's request body. cpp/update_flow_core joins the podspec.
Verified with clang -fsyntax-only against the e2e Pods in both DEBUG=0 and
DEBUG=1 shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NativeCheckOrchestrator mirrors the iOS orchestrator on Android
(NATIVE_CHECKUPDATE_DESIGN §10): once per process, five seconds after
getBundleUrl on a low-priority daemon thread, gated on the JS-provisioned
config (absent = disabled) and on BuildConfig.DEBUG. Decisions come from
cpp/update_flow_core through the new NativeUpdateFlow JNI surface
(string-in/string-out JSON: buildCheckRequestBody, orderEndpointCandidates,
handleCheckResponse), compiled into librnupdate.so — all four prebuilt
ABIs rebuilt and re-verified (exported symbols including the three new
ones, 16KB page alignment). IO reuses what exists: OkHttp for the
sequential endpoint fallback with queryUrls discovery, DownloadTask via
UpdateContext.downloadPatchFromPpk/downloadPatchFromApk/downloadFullUpdate
for diff → pdiff → full attempts, switchVersion for next-launch activation
under the silent strategies, and the raw response cached in
SharedPreferences for JS reuse (§10.3). computeBundleHash becomes
package-private to feed the request body. Verified: javac of main+oldarch
against the RN 0.85 AAR + okhttp classpath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the third platform of NATIVE_CHECKUPDATE_DESIGN §10.
NativeCheckOrchestrator.ts mirrors the iOS/Android orchestrators: once per
process, five seconds after getBundleUrl (dev never reaches it — Metro's
provider bypasses PushyFileJSBundleProvider, which doubles as the debug
gate), reads the JS-provisioned config, and drives the flow through the
decision layer now compiled into librnupdate: flow_json + update_flow_core
join the CMake build and three NAPI exports (buildCheckRequestBody,
orderEndpointCandidates, handleCheckResponse) join pushy.cpp, string-in/
string-out JSON. IO reuses @ohos.net.http for the sequential endpoint
fallback with queryUrls discovery and UpdateContext's download/switch
pipeline for diff → pdiff → full attempts; the raw response caches via
setKv for JS reuse (§10.3). UpdateContext gains hasDownloadedVersion so an
already-downloaded, not-yet-activated version is not re-downloaded every
launch under alert strategies. The har staging script now stages
cpp/update_flow_core alongside patch_core. Verified: DevEco tsc over the
ets sources and the OHOS llvm toolchain syntax pass over pushy.cpp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the double-check window between the native cold-start check and the
JS-side checkUpdate: getNativeCheckCache (all three platforms + spec)
exposes the response the orchestrator persisted, and the JS client reuses
it when fresher than two minutes instead of issuing its own request. The
cache read happens inside the shared response promise, so no await lands
between the dedup window and the lastRespJson assignment (the JS2-1
double-send lesson); absent, stale, or unreadable caches fall through to a
normal network check, and dev builds skip the path entirely. Covered by
three client tests (fresh reuse without network, stale fallthrough,
unreadable cache); full suite 158 green, harmony tsc, clang syntax pass
and javac all re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three orchestrators, the provisioning layer and the §10.3 response
cache reuse have landed; what remains is e2e coverage (the bricked-bundle
end-to-end scenario), README/CHANGELOG and a release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the last gap in the brick rescue: under alert-style strategies (the
default) the native check downloaded the fix but never activated it, and a
bricked device's JS can never do so. The server can now mark a version
config.forceBoot ("boot into this version next launch") and the native
orchestrator activates it regardless of the client's strategy.

Activation policy folds into the pure layer: shouldActivateAfterDownload
(afterDownload === 'setNeedUpdate' || info.config.forceBoot, JS truthiness)
lands in updateFlowCore + the C++ port with 8 new golden vectors (77
total), and HandleCheckResponse gains the afterDownload parameter and
emits `activate` on download decisions — all three orchestrators now read
that single boolean instead of comparing strategy strings. Deliberate
semantics, pinned by composition tests: native-only (the JS interactive
flow is not consulted), the device-local rolledBack guard wins over
forceBoot, and first_time crash protection still applies to the forced
version. librnupdate.so rebuilt for all four ABIs with the new JNI
signature (symbols + 16KB alignment verified); JS suite (158), harmony
tsc, OHOS clang, iOS clang and javac all green. Server-side counterpart
(root-version config passthrough + console toggle) tracked in §10.7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
versions.config belongs to the retired gray-release design and is being
actively scrubbed by the binding transaction; the first draft that revived
it through the binding branches was reverted (legacy rollout leakage, dual
config sources, working against the scrub). forceBoot is a property of a
delivery, so it lives on bindings.config — the per-delivery slot the
upsert API already plumbs — which also self-clears on rebinding and scopes
the directive to a single packageVersion. Client protocol unchanged: the
response config stays synthesized from binding data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds shared TypeScript and C++ update-flow decisions, native cold-start orchestration for iOS, Android, and Harmony, configuration synchronization, response-cache reuse, bounded downloads, completion markers, and cross-language parity tests.

Changes

Unified native update flow

Layer / File(s) Summary
Shared decision core and parity validation
cpp/update_flow_core/*, src/updateFlowCore.ts, scripts/*, src/__tests__/*
Adds JSON handling, rollout logic, endpoint ordering, request and response shaping, download planning, activation rules, generated vectors, native bindings, and sanitizer-tested parity checks.
JavaScript integration and cache reuse
src/client.ts, src/NativePushy.ts, src/endpoint.ts, src/provider.tsx, src/type.ts, src/utils.ts, src/__tests__/*
Synchronizes native configuration, reuses validated cached responses, and executes shared download plans.
Android native orchestration
android/..., scripts/verify-android-so.js
Adds JNI bindings, cold-start checks, deadline-aware downloads, completion markers, and native configuration and cache bridge methods.
Harmony native orchestration
harmony/pushy/..., scripts/build-harmony-har.js
Adds native bindings, cold-start checks, deadline-aware downloads, completion markers, path validation, monotonic deadlines, and configuration and cache bridge methods.
iOS native orchestration
ios/RCTPushy/...
Adds cold-start orchestration, cached response handling, download deduplication, completion markers, rollback scheduling, bundle-hash caching, and deadline-aware downloader support.
Implementation follow-up records
NATIVE_CHECKUPDATE_DESIGN.md, NATIVE_CHECK_FOLLOWUPS.md
Records the implemented native-flow design, platform decisions, validation baseline, follow-up conclusions, and release checks.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JavaScriptClient
  participant NativeUpdateFlow
  participant NativeOrchestrator
  participant DownloadTask
  participant NativeStorage

  JavaScriptClient->>NativeStorage: syncNativeConfig
  NativeOrchestrator->>NativeUpdateFlow: build request and handle response
  NativeOrchestrator->>DownloadTask: execute ordered download plan
  DownloadTask->>NativeStorage: write completion marker
  NativeOrchestrator->>NativeStorage: persist response cache
  JavaScriptClient->>NativeStorage: getNativeCheckCache
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: resilient native cold-start update checks across supported platforms.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/harden-native-check-update

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sunnylqm
sunnylqm marked this pull request as ready for review August 9, 2026 14:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (15)
ios/RCTPushy/RCTPushy.mm (1)

1470-1511: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a bound on the total number of endpoint attempts.

Each attempt blocks for up to 15 seconds (10 s request timeout plus the 5 s semaphore backstop). The configured endpoints list and the discovered remote list are both unbounded, so a long list keeps the utility thread busy for minutes. A simple attempt counter, or a total deadline for the whole round, keeps the cold-start check bounded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/RCTPushy/RCTPushy.mm` around lines 1470 - 1511, Bound the total endpoint
attempts in the update-check flow by adding a shared counter or overall deadline
across both loops over config.Get("endpoints") and remote.elements(). Stop
processing when the limit is reached, while preserving deduplication via tried
and returning immediately for the first valid PushyIsValidCheckResponse result.
harmony/pushy/src/main/ets/PushyTurboModule.ts (1)

189-205: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the orchestrator key constants instead of string literals.

NativeCheckOrchestrator.ts defines KEY_CONFIG = 'nativeConfig' and KEY_RESP_CACHE = 'nativeCheckResp'. This module repeats both literals. A rename in one file then silently breaks the read/write pairing. The Android side avoids this by referencing NativeCheckOrchestrator.KEY_RESP_CACHE. Export the two constants from NativeCheckOrchestrator.ts and import them here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harmony/pushy/src/main/ets/PushyTurboModule.ts` around lines 189 - 205,
Export KEY_CONFIG and KEY_RESP_CACHE from NativeCheckOrchestrator.ts, then
import and use them in syncNativeConfig and getNativeCheckCache instead of the
'nativeConfig' and 'nativeCheckResp' string literals. Preserve the existing
persistence and fallback behavior.
scripts/build-harmony-har.js (1)

242-245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an ensureFileExists precondition for the update-flow core.

Every other staged native source in syncHarmonyNativeSources has an ensureFileExists check before the copy block, including patch_core.cpp at lines 213-218. The new update_flow_core copy has none.

If cpp/update_flow_core/update_flow_core.cpp is absent or renamed, this script stages an incomplete directory. harmony/pushy/src/main/cpp/CMakeLists.txt then falls back to the staged path at lines 20-23 and the build fails later with a compiler or linker error. An explicit guard reports the real cause here.

♻️ Proposed change
   ensureFileExists(
     path.join(patchCoreDir, 'patch_core.cpp'),
     `Missing shared patch core source: ${relativeToProject(
       path.join(patchCoreDir, 'patch_core.cpp'),
     )}`,
   );
+  ensureFileExists(
+    path.join(updateFlowCoreDir, 'update_flow_core.cpp'),
+    `Missing shared update flow core source: ${relativeToProject(
+      path.join(updateFlowCoreDir, 'update_flow_core.cpp'),
+    )}`,
+  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-harmony-har.js` around lines 242 - 245, Add an ensureFileExists
precondition for cpp/update_flow_core/update_flow_core.cpp in
syncHarmonyNativeSources before the copyPath call targeting
updateHarmonyNativeStageDir/update_flow_core, matching the existing
patch_core.cpp guard pattern. Keep the existing copy operation unchanged.
harmony/pushy/src/main/ets/DownloadTask.ts (1)

15-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The .pushy-complete marker filename has no shared definition. The writer and the reader each carry their own copy of the literal. If one copy changes, hasDownloadedVersion returns false for every completed version and the native orchestrator re-downloads an already-ready update on every cold start. Android avoids this with the single UpdateContext.VERSION_COMPLETE_FILE constant.

  • harmony/pushy/src/main/ets/DownloadTask.ts#L15-L16: export VERSION_COMPLETE_FILE_NAME instead of keeping it module-private.
  • harmony/pushy/src/main/ets/UpdateContext.ts#L539-L550: import the exported constant and build the marker path from it instead of the inline .pushy-complete literal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harmony/pushy/src/main/ets/DownloadTask.ts` around lines 15 - 16, Export
VERSION_COMPLETE_FILE_NAME from DownloadTask.ts, then import and reuse it in
UpdateContext.ts when constructing the completed-version marker path, replacing
the inline '.pushy-complete' literal in the relevant marker lookup.
harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts (1)

180-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Validate hash as a safe path component here, like Android does.

This code only rejects an empty hash. The Android orchestrator calls UpdateContext.isSafePathComponent(hash) at the same point and returns early.

Path safety is still preserved on Harmony: hasDownloadedVersion wraps assertSafePathComponent in a try/catch, and createTaskParams throws before any path is built. So a hostile hash cannot escape rootDir. The observable difference is diagnostics and wasted work: an unsafe hash produces one throw per candidate URL inside performAttempts, each logged only at debug level, and the round ends with a generic "not downloaded" outcome.

Add the explicit check so the reason is logged once and the platforms behave the same.

♻️ Proposed change
   const hash = decision.hash ?? '';
-  if (!hash) {
+  if (!hash || !isSafePathComponent(hash)) {
+    logger.debug(TAG, `rejected hash from decision: ${hash}`);
     return;
   }

Export a non-throwing isSafePathComponent predicate next to the existing assertSafePathComponent helper and import it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts` around lines 180 -
183, Validate the resolved hash in the Harmony orchestrator using a non-throwing
path-safety predicate before continuing past the empty-hash guard. Export that
predicate alongside assertSafePathComponent, import it into
NativeCheckOrchestrator, and return early through the existing diagnostic path
when the hash is unsafe, matching Android’s behavior.
android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java (2)

218-221: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a call timeout to the check client.

connectTimeout and readTimeout bound single socket operations only. A server that sends one byte at a time keeps the connection open indefinitely, so this thread never reaches the next endpoint. DownloadTask.HTTP_CLIENT already sets callTimeout for the same reason.

♻️ Proposed change
     private static final OkHttpClient httpClient = new OkHttpClient.Builder()
         .connectTimeout(10, TimeUnit.SECONDS)
         .readTimeout(10, TimeUnit.SECONDS)
+        .callTimeout(30, TimeUnit.SECONDS)
         .build();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`
around lines 218 - 221, Add a finite callTimeout to the OkHttpClient builder in
NativeCheckOrchestrator’s httpClient, matching the timeout configuration used by
DownloadTask.HTTP_CLIENT, while preserving the existing connectTimeout and
readTimeout settings.

319-387: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Bound the whole attempt loop, not each attempt.

Each URL gets its own 600-second latch. A decision that carries several attempts with several URLs each can keep this thread and the single-threaded download executor busy for a very long time. The executor is shared with JavaScript-triggered downloads, so those queue behind it.

Consider a single deadline for performAttempts and pass the remaining time to latch.await.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`
around lines 319 - 387, Update performAttempts to establish one overall deadline
for the entire attempts-and-URLs loop, rather than resetting the 600-second
timeout for each URL. Before each latch.await call, calculate the remaining time
and await only that duration; return false when the deadline is exhausted, while
preserving success, interruption, and existing download behavior.
android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java (1)

266-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use NativeCheckOrchestrator.KEY_CONFIG instead of the string literal.

getNativeCheckCache on line 236 reads through NativeCheckOrchestrator.KEY_RESP_CACHE. This writer hardcodes "nativeConfig". The reader is NativeCheckOrchestrator.runOnce, which uses KEY_CONFIG. A rename of the constant would silently disable the native check.

♻️ Proposed change
             public void run() {
-                updateContext.setKv("nativeConfig", config);
+                updateContext.setKv(NativeCheckOrchestrator.KEY_CONFIG, config);
                 promise.resolve(true);
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java`
around lines 266 - 273, Update the native configuration write inside the
StateSerialRunner operation to use NativeCheckOrchestrator.KEY_CONFIG instead of
the hardcoded "nativeConfig" key, keeping the existing promise resolution
behavior unchanged.
android/src/main/java/cn/reactnative/modules/update/UpdateContext.java (1)

566-566: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use one source for the rolled-back version snapshot.

Line 566 passes launchState.rolledBackVersion. Lines 582 and 586 pass rolledBackVersion(), which re-reads SharedPreferences. Both values agree today, because applyState persists the launch state whenever didRollback is true. The two forms still express different assumptions, and a future change to the persist condition would make them diverge silently.

Read launchState.rolledBackVersion into a local variable once and pass it at all three call sites.

Also applies to: 582-587

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java` at
line 566, In the update flow surrounding NativeCheckOrchestrator.schedule,
capture launchState.rolledBackVersion in a local variable once, then pass that
variable at the call sites currently using launchState.rolledBackVersion and
rolledBackVersion(). Remove the repeated SharedPreferences read while preserving
the existing scheduling behavior.
src/client.ts (1)

267-267: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Skip the native write when the serialized config did not change.

setOptions runs on every option update, including each re-created client and each provider-driven option change. syncNativeConfig then issues a bridge call and a native key-value write every time, even when the serialized config is identical. On Android the write goes through a serialized file operation.

Cache the last successfully synced JSON string and compare before writing.

♻️ Proposed refactor
+  private lastSyncedConfigJson?: string;
+
   private syncNativeConfig = () => {
     const configJson = this.getNativeConfigJson();
-    if (!configJson) {
+    if (!configJson || configJson === this.lastSyncedConfigJson) {
       return;
     }
+    this.lastSyncedConfigJson = configJson;
     Promise.resolve(PushyModule.syncNativeConfig(configJson)).catch(
       (e: any) => {
+        this.lastSyncedConfigJson = undefined;
         log('syncNativeConfig failed:', e?.message || e);
       }
     );
   };

Also applies to: 300-310

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client.ts` at line 267, Update the config synchronization flow around
syncNativeConfig and its call sites in setOptions to cache the last successfully
synced serialized JSON string, skip the native bridge/write when the new
serialization matches it, and update the cache only after a successful sync.
NATIVE_CHECKUPDATE_DESIGN.md (1)

315-318: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the fenced code blocks.

markdownlint reports MD040 for the blocks at Line 315 and Line 332. Both blocks contain pseudo-code. Use ```text to satisfy the rule.

📝 Proposed fix
-```
+```text
 { appKey, endpoints: server.main, queryUrls, afterDownload: 'none' | 'setNeedUpdate',
   disabled?: boolean, rnu, rn }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NATIVE_CHECKUPDATE_DESIGN.md` around lines 315 - 318, Update the fenced
pseudo-code blocks in NATIVE_CHECKUPDATE_DESIGN.md to specify the text language,
including the blocks containing the appKey configuration and the corresponding
block near the later referenced section. Use text fences while preserving their
contents.

Source: Linters/SAST tools

scripts/test-update-flow-core.sh (1)

11-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silence the intentional word splitting for shellcheck.

$SANITIZE_FLAGS must split into separate compiler arguments, so the missing quotes are correct here. POSIX sh has no arrays, so the split is the only option. Add a directive so SC2086 does not stay in the lint output.

♻️ Proposed change
+# shellcheck disable=SC2086  # SANITIZE_FLAGS must split into separate args
 c++ \
   -std=c++17 \

SC1007 on Line 4 is a false positive. CDPATH= cd -- ... is the intended idiom for a one-command environment assignment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-update-flow-core.sh` around lines 11 - 25, Add a shellcheck
directive adjacent to the intentional $SANITIZE_FLAGS expansion in the compiler
invocation to suppress SC2086, while preserving its unquoted word splitting.
Also suppress SC1007 for the existing CDPATH= cd -- idiom on the relevant line
without changing either command’s behavior.

Source: Linters/SAST tools

cpp/update_flow_core/flow_json.cpp (1)

443-469: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

ParseNumber is more lenient than JSON.parse.

The scanner collects any run of digits, ., e, E, +, -, then delegates to strtod. strtod accepts forms that RFC 8259 and JSON.parse reject, for example 1., 01, and .5. The header describes the parser as strict. The practical effect is a divergence: a malformed response can produce a native decision while the JavaScript path throws on the same bytes.

The risk is small because the server produces the responses. If you want the two paths to agree on rejection, validate the token shape before strtod.

♻️ Optional: token-shape validation
     char* end = nullptr;
     std::string token = text_.substr(start, pos_ - start);
+    // JSON grammar: -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?
+    if (!IsJsonNumberToken(token)) {
+      ok_ = false;
+      return Value::Undefined();
+    }
     double n = std::strtod(token.c_str(), &end);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/update_flow_core/flow_json.cpp` around lines 443 - 469, Make ParseNumber
enforce strict JSON number syntax before converting the token: require an
optional leading minus, a zero or nonzero digit sequence without leading zeros,
an optional fractional part containing at least one digit, and an optional
exponent with an optional sign and at least one digit. Reject malformed shapes
such as 1., 01, and .5 by setting ok_ false and returning Value::Undefined(),
while preserving valid-number conversion through strtod.
src/updateFlowCore.ts (1)

200-219: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Guard the upToDate comparisons against missing hashes.

Line 203 and line 217 compare optional values. If both sides are undefined, the strict comparison succeeds and the function reports upToDate. A malformed server payload that omits hash then suppresses the update instead of falling through. The native orchestrator replays this decision before JavaScript starts, so the suppression also applies to the cold-start rescue path.

The generator pins this behavior in a golden vector, so change the vectors together with the code if you accept the guard.

♻️ Proposed guard
-      if (expVersion.hash === identity.currentVersion) {
+      if (expVersion.hash && expVersion.hash === identity.currentVersion) {
         return { upToDate: true };
       }
@@
-  if (rootResult.update && rootResult.hash === identity.currentVersion) {
+  if (
+    rootResult.update &&
+    rootResult.hash &&
+    rootResult.hash === identity.currentVersion
+  ) {
     return { upToDate: true };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/updateFlowCore.ts` around lines 200 - 219, Guard both upToDate
comparisons in the update flow around expVersion.hash and rootResult.hash so
equality is evaluated only when the relevant hash is present. Ensure missing
hashes fall through to normal update handling rather than returning upToDate,
and update the generator golden vectors if required by the changed behavior.
scripts/generate-flow-vectors.ts (1)

279-290: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Convert the file URL with fileURLToPath instead of using pathname.

URL.pathname keeps percent-encoding for file system paths. If the repository path contains a space or non-ASCII character, Bun.write(...) can target the wrong path. Use Bun.fileURLToPath(new URL(...)) for this destination.

♻️ Proposed fix
-  const outPath = new URL(
-    '../cpp/update_flow_core/tests/flow_vectors.json',
-    import.meta.url
-  ).pathname;
+  const outPath = Bun.fileURLToPath(
+    new URL('../cpp/update_flow_core/tests/flow_vectors.json', import.meta.url)
+  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-flow-vectors.ts` around lines 279 - 290, Update the
destination path construction in the import.meta.main block to convert the file
URL with Bun.fileURLToPath(new URL(...)) instead of reading URL.pathname, then
continue passing the decoded filesystem path to Bun.write and the existing log.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`:
- Around line 261-317: Normalize endpoint base URLs before constructing
check-update requests to prevent duplicate slashes. Update runCheckRequest’s
ordered and remote endpoint handling so each base is canonicalized before
deduplication and concatenation, or apply the normalization in the client
persistence path; preserve existing endpoint ordering and fallback behavior.

In `@cpp/update_flow_core/tests/update_flow_core_test.cpp`:
- Around line 276-306: Validate that the `cases` value loaded before the vector
loop is an array and non-empty before iterating. If the shape is invalid or
empty, report the failure, increment `failures`, and preserve the existing
nonzero exit path so `RunParserRobustness()` and `RunHandleCheckResponse()`
results are still included.

In `@cpp/update_flow_core/update_flow_core.cpp`:
- Around line 130-134: Update the random index calculation around randomSample
to validate and clamp non-finite or out-of-range values before converting to
size_t. Ensure NaN, infinity, and values producing an index outside the valid
range resolve to the same safe boundary behavior as the existing first
calculation, while preserving normal sampling for finite in-range values.

In `@cpp/update_flow_core/update_flow_jni.cpp`:
- Around line 15-17: Update ToJString to decode the UTF-8 produced by Stringify
into UTF-16 jchars and call env->NewString with the decoded characters and
length; do not pass the raw std::string to NewStringUTF, preserving correct
handling of non-ASCII code points such as emoji.

In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 1259-1272: Update the request task flow around the dataTask
completion handler and dispatch_semaphore_wait: retain the created
NSURLSessionDataTask, check that response is an NSHTTPURLResponse before reading
statusCode, and only return result after a successful semaphore wait. When the
wait times out, cancel the task and return without reading the handler-written
result; keep the completion handler’s result local to the successful completion
path to avoid a timeout race.

In `@NATIVE_CHECKUPDATE_DESIGN.md`:
- Line 111: Update both references in NATIVE_CHECKUPDATE_DESIGN.md at lines 111
and 284: replace the stale “69” counts for generated/golden vectors with the
current total of 77 test cases across 8 functions, or remove the numeric claims
and refer to the generated vector set.

In `@src/client.ts`:
- Around line 280-298: Introduce one client accessor for the effective package
version, using overridePackageVersion when present and packageVersion otherwise,
and reuse it wherever package identity is sent or resolved. In
src/client.ts:280-298, include that value in getNativeConfigJson; in
src/provider.tsx:226-230, pass the client’s effective version to
resolveCheckResult instead of the imported packageVersion. Ensure both sites use
the same accessor and no direct raw-version lookup remains there.

In `@src/updateFlowCore.ts`:
- Around line 261-299: Update decideDownload so a non-development release with
no resolved artifact URLs does not return a download plan with attempts: [];
return the appropriate none decision (or otherwise fail before execution) while
preserving devNoop behavior for development builds. Document or enforce the
empty-plan handling in the executors that consume DownloadDecision, using
decideDownload and the relevant execution symbols.

---

Nitpick comments:
In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`:
- Around line 218-221: Add a finite callTimeout to the OkHttpClient builder in
NativeCheckOrchestrator’s httpClient, matching the timeout configuration used by
DownloadTask.HTTP_CLIENT, while preserving the existing connectTimeout and
readTimeout settings.
- Around line 319-387: Update performAttempts to establish one overall deadline
for the entire attempts-and-URLs loop, rather than resetting the 600-second
timeout for each URL. Before each latch.await call, calculate the remaining time
and await only that duration; return false when the deadline is exhausted, while
preserving success, interruption, and existing download behavior.

In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java`:
- Line 566: In the update flow surrounding NativeCheckOrchestrator.schedule,
capture launchState.rolledBackVersion in a local variable once, then pass that
variable at the call sites currently using launchState.rolledBackVersion and
rolledBackVersion(). Remove the repeated SharedPreferences read while preserving
the existing scheduling behavior.

In `@android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java`:
- Around line 266-273: Update the native configuration write inside the
StateSerialRunner operation to use NativeCheckOrchestrator.KEY_CONFIG instead of
the hardcoded "nativeConfig" key, keeping the existing promise resolution
behavior unchanged.

In `@cpp/update_flow_core/flow_json.cpp`:
- Around line 443-469: Make ParseNumber enforce strict JSON number syntax before
converting the token: require an optional leading minus, a zero or nonzero digit
sequence without leading zeros, an optional fractional part containing at least
one digit, and an optional exponent with an optional sign and at least one
digit. Reject malformed shapes such as 1., 01, and .5 by setting ok_ false and
returning Value::Undefined(), while preserving valid-number conversion through
strtod.

In `@harmony/pushy/src/main/ets/DownloadTask.ts`:
- Around line 15-16: Export VERSION_COMPLETE_FILE_NAME from DownloadTask.ts,
then import and reuse it in UpdateContext.ts when constructing the
completed-version marker path, replacing the inline '.pushy-complete' literal in
the relevant marker lookup.

In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts`:
- Around line 180-183: Validate the resolved hash in the Harmony orchestrator
using a non-throwing path-safety predicate before continuing past the empty-hash
guard. Export that predicate alongside assertSafePathComponent, import it into
NativeCheckOrchestrator, and return early through the existing diagnostic path
when the hash is unsafe, matching Android’s behavior.

In `@harmony/pushy/src/main/ets/PushyTurboModule.ts`:
- Around line 189-205: Export KEY_CONFIG and KEY_RESP_CACHE from
NativeCheckOrchestrator.ts, then import and use them in syncNativeConfig and
getNativeCheckCache instead of the 'nativeConfig' and 'nativeCheckResp' string
literals. Preserve the existing persistence and fallback behavior.

In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 1470-1511: Bound the total endpoint attempts in the update-check
flow by adding a shared counter or overall deadline across both loops over
config.Get("endpoints") and remote.elements(). Stop processing when the limit is
reached, while preserving deduplication via tried and returning immediately for
the first valid PushyIsValidCheckResponse result.

In `@NATIVE_CHECKUPDATE_DESIGN.md`:
- Around line 315-318: Update the fenced pseudo-code blocks in
NATIVE_CHECKUPDATE_DESIGN.md to specify the text language, including the blocks
containing the appKey configuration and the corresponding block near the later
referenced section. Use text fences while preserving their contents.

In `@scripts/build-harmony-har.js`:
- Around line 242-245: Add an ensureFileExists precondition for
cpp/update_flow_core/update_flow_core.cpp in syncHarmonyNativeSources before the
copyPath call targeting updateHarmonyNativeStageDir/update_flow_core, matching
the existing patch_core.cpp guard pattern. Keep the existing copy operation
unchanged.

In `@scripts/generate-flow-vectors.ts`:
- Around line 279-290: Update the destination path construction in the
import.meta.main block to convert the file URL with Bun.fileURLToPath(new
URL(...)) instead of reading URL.pathname, then continue passing the decoded
filesystem path to Bun.write and the existing log.

In `@scripts/test-update-flow-core.sh`:
- Around line 11-25: Add a shellcheck directive adjacent to the intentional
$SANITIZE_FLAGS expansion in the compiler invocation to suppress SC2086, while
preserving its unquoted word splitting. Also suppress SC1007 for the existing
CDPATH= cd -- idiom on the relevant line without changing either command’s
behavior.

In `@src/client.ts`:
- Line 267: Update the config synchronization flow around syncNativeConfig and
its call sites in setOptions to cache the last successfully synced serialized
JSON string, skip the native bridge/write when the new serialization matches it,
and update the cache only after a successful sync.

In `@src/updateFlowCore.ts`:
- Around line 200-219: Guard both upToDate comparisons in the update flow around
expVersion.hash and rootResult.hash so equality is evaluated only when the
relevant hash is present. Ensure missing hashes fall through to normal update
handling rather than returning upToDate, and update the generator golden vectors
if required by the changed behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 737dfbd2-4374-4af0-a965-d3447fcc3c1f

📥 Commits

Reviewing files that changed from the base of the PR and between beb6a7a and f679e11.

⛔ Files ignored due to path filters (4)
  • android/lib/arm64-v8a/librnupdate.so is excluded by !**/*.so
  • android/lib/armeabi-v7a/librnupdate.so is excluded by !**/*.so
  • android/lib/x86/librnupdate.so is excluded by !**/*.so
  • android/lib/x86_64/librnupdate.so is excluded by !**/*.so
📒 Files selected for processing (46)
  • .github/workflows/test.yml
  • NATIVE_CHECKUPDATE_DESIGN.md
  • android/jni/Android.mk
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
  • android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
  • android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
  • android/src/newarch/cn/reactnative/modules/update/UpdateModule.java
  • android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java
  • cpp/update_flow_core/flow_json.cpp
  • cpp/update_flow_core/flow_json.h
  • cpp/update_flow_core/tests/flow_vectors.json
  • cpp/update_flow_core/tests/update_flow_core_test.cpp
  • cpp/update_flow_core/update_flow_core.cpp
  • cpp/update_flow_core/update_flow_core.h
  • cpp/update_flow_core/update_flow_jni.cpp
  • harmony/pushy/src/main/cpp/CMakeLists.txt
  • harmony/pushy/src/main/cpp/pushy.cpp
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
  • harmony/pushy/src/main/ets/NativePatchCore.ts
  • harmony/pushy/src/main/ets/PushyTurboModule.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • ios/RCTPushy/RCTPushy.mm
  • package.json
  • react-native-update.podspec
  • scripts/build-harmony-har.js
  • scripts/generate-flow-vectors.ts
  • scripts/test-update-flow-core.sh
  • scripts/verify-android-so.js
  • src/NativePushy.ts
  • src/__tests__/client.test.ts
  • src/__tests__/flowVectors.test.ts
  • src/__tests__/isInRollout.test.ts
  • src/__tests__/resolveCheckResult.test.ts
  • src/__tests__/setup.ts
  • src/__tests__/updateFlowCore.test.ts
  • src/client.ts
  • src/endpoint.ts
  • src/isInRollout.ts
  • src/provider.tsx
  • src/resolveCheckResult.ts
  • src/type.ts
  • src/updateFlowCore.ts
  • src/utils.ts
💤 Files with no reviewable changes (2)
  • src/resolveCheckResult.ts
  • src/isInRollout.ts

Comment thread cpp/update_flow_core/tests/update_flow_core_test.cpp
Comment thread cpp/update_flow_core/update_flow_core.cpp Outdated
Comment thread cpp/update_flow_core/update_flow_jni.cpp
Comment thread ios/RCTPushy/RCTPushy.mm
Comment thread NATIVE_CHECKUPDATE_DESIGN.md Outdated
Comment thread src/client.ts Outdated
Comment thread src/updateFlowCore.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/update_flow_core/flow_json.cpp (1)

482-506: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Serialize non-finite Number values as null.

std::strtod("1e309", ...) parses 1e309 as infinity and the parser accepts it. AppendNumber() then emits inf, which is not valid JSON. JSON does not support Infinity; encode parsed non-finite values as null, or reject range-overflow results during parsing.

Proposed fix
 void AppendNumber(double n, std::string* out) {
+  if (!std::isfinite(n)) {
+    out->append("null");
+    return;
+  }
   if (std::isfinite(n) && n == std::floor(n) &&
       std::fabs(n) <= 9007199254740992.0) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/update_flow_core/flow_json.cpp` around lines 482 - 506, Update the number
parsing return path after std::strtod in the relevant JSON parser method to
detect non-finite values and return Value::Null() so inputs such as 1e309
serialize as valid JSON null. Preserve normal finite-number parsing and existing
malformed-token handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`:
- Around line 397-404: The timeout path in performAttempts must cancel and join
the DownloadTask before returning false, ensuring no timed-out task can still
write when runOnce publishes nativeCheckResp. Update the latch timeout handling
around the download round to stop the scheduled task, wait for its termination,
and only then allow the response cache to be stored.

---

Outside diff comments:
In `@cpp/update_flow_core/flow_json.cpp`:
- Around line 482-506: Update the number parsing return path after std::strtod
in the relevant JSON parser method to detect non-finite values and return
Value::Null() so inputs such as 1e309 serialize as valid JSON null. Preserve
normal finite-number parsing and existing malformed-token handling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e803e595-2013-4c36-bd9b-9db69194ee92

📥 Commits

Reviewing files that changed from the base of the PR and between f679e11 and 181952a.

⛔ Files ignored due to path filters (4)
  • android/lib/arm64-v8a/librnupdate.so is excluded by !**/*.so
  • android/lib/armeabi-v7a/librnupdate.so is excluded by !**/*.so
  • android/lib/x86/librnupdate.so is excluded by !**/*.so
  • android/lib/x86_64/librnupdate.so is excluded by !**/*.so
📒 Files selected for processing (24)
  • NATIVE_CHECKUPDATE_DESIGN.md
  • android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
  • cpp/update_flow_core/flow_json.cpp
  • cpp/update_flow_core/tests/flow_vectors.json
  • cpp/update_flow_core/tests/update_flow_core_test.cpp
  • cpp/update_flow_core/update_flow_core.cpp
  • cpp/update_flow_core/update_flow_jni.cpp
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
  • harmony/pushy/src/main/ets/PathUtils.ts
  • harmony/pushy/src/main/ets/PushyTurboModule.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • ios/RCTPushy/RCTPushy.mm
  • scripts/build-harmony-har.js
  • scripts/generate-flow-vectors.ts
  • scripts/test-update-flow-core.sh
  • src/__tests__/client.test.ts
  • src/__tests__/provider.render.test.tsx
  • src/__tests__/resolveCheckResult.test.ts
  • src/__tests__/updateFlowCore.test.ts
  • src/client.ts
  • src/provider.tsx
  • src/updateFlowCore.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
  • src/provider.tsx
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • scripts/build-harmony-har.js
  • harmony/pushy/src/main/ets/PushyTurboModule.ts
  • scripts/test-update-flow-core.sh
  • harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
  • src/tests/client.test.ts
  • cpp/update_flow_core/update_flow_core.cpp
  • src/tests/resolveCheckResult.test.ts
  • src/updateFlowCore.ts
  • ios/RCTPushy/RCTPushy.mm
  • NATIVE_CHECKUPDATE_DESIGN.md

Comment thread android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java Outdated
Everything still open after two review rounds of the hardening branch,
ordered by priority with current state, risk and suggested fixes: the
Android/Harmony duplicate-download race (P1, the failure path can delete
an installed version), the response-cache ts anchor and the per-platform
deadline parity gap, the two semantic costs of iOS download joining, the
.pushy-complete migration decision, the schedule-unreachable-on-throw
path, two recorded-only P4 items, and the non-code release checklist
(e2e, docs, 10.51.0, server-side activation steps).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
NATIVE_CHECK_FOLLOWUPS.md (1)

91-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

确保救援检测只调度一次。

如果现有正常出口仍调用 schedule,在 finally 中无条件调度会为成功解析路径再创建一次检查。该重复检查可能重新触发下载。请将调度集中到一个公共出口,或使用 scheduled 状态仅在异常且尚未调度时执行兜底。为正常解析、回滚异常和状态解析异常分别增加测试。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NATIVE_CHECK_FOLLOWUPS.md` around lines 91 - 93,
将状态解析流程的调度集中到唯一出口,确保正常解析、回滚异常和状态解析异常都只调用一次 schedule;若保留 finally 兜底,则增加 scheduled
状态,仅在异常且尚未调度时以空快照(rolledBackVersion 为 null)执行。三端同步修改,并补充覆盖三种路径的测试。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 5-7: 将文档中的 P1 发布要求改为硬性门槛:发版前必须修复,若接受风险则明确记录审批与回滚方案。补充方案 3
的原子性要求,围绕 hasCompletedVersion(hash) 使用 get-or-create 锁,避免检查、任务执行、标记写入与
cleanUpAfterFailure 之间的 TOCTOU;采用方案 2 时将检查和清理置于同一临界域内。

---

Nitpick comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 91-93: 将状态解析流程的调度集中到唯一出口,确保正常解析、回滚异常和状态解析异常都只调用一次 schedule;若保留
finally 兜底,则增加 scheduled 状态,仅在异常且尚未调度时以空快照(rolledBackVersion 为
null)执行。三端同步修改,并补充覆盖三种路径的测试。
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef59e3f9-d8c3-446e-83b5-fb1ea023442a

📥 Commits

Reviewing files that changed from the base of the PR and between 181952a and 3229cd8.

📒 Files selected for processing (1)
  • NATIVE_CHECK_FOLLOWUPS.md

Comment thread NATIVE_CHECK_FOLLOWUPS.md Outdated
181952a's re-review surfaced ten more confirmed items, six introduced by
the fixes themselves; the open ones now live here: the config-sync revert
race, the download-round deadline treated as one cross-platform design
(Harmony missing it entirely, iOS still per-URL, Android checking after
launch and starving the full-download budget), the dead alert for
hash-less rollout entries, the silently swallowed noArtifact decline, and
two small P4s (slash-variant duplicate requests, missing Harmony
whole-call timeout).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
NATIVE_CHECK_FOLLOWUPS.md (1)

7-8: ⚠️ Potential issue | 🟠 Major

将 P1 改为硬性发版门槛。

当前文字仍允许带着已安装版本被删除的风险发版。发版前必须修复 P1;如果接受风险,请记录明确的审批和回滚方案。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NATIVE_CHECK_FOLLOWUPS.md` around lines 7 - 8, Update the release risk
guidance in NATIVE_CHECK_FOLLOWUPS.md so P1 is a mandatory release gate: it must
be fixed before release, or require documented risk approval and a rollback
plan. Keep the existing P2–P4 handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 50-53: Update the slow-CDN condition in the “复评补充(181952a 引入/暴露)”
section to state that the CDN only needs to return one byte within every
60-second interval to bypass the inactivity watchdog, preserving the existing
boundary meaning.

---

Duplicate comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 7-8: Update the release risk guidance in NATIVE_CHECK_FOLLOWUPS.md
so P1 is a mandatory release gate: it must be fixed before release, or require
documented risk approval and a rollback plan. Keep the existing P2–P4 handling
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 169ec69a-2bff-4a72-a2f1-20da41485854

📥 Commits

Reviewing files that changed from the base of the PR and between 3229cd8 and f33bd6a.

📒 Files selected for processing (1)
  • NATIVE_CHECK_FOLLOWUPS.md

Comment thread NATIVE_CHECK_FOLLOWUPS.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
ios/RCTPushy/RCTPushyDownloader.mm (1)

48-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the comment to describe the caller-supplied resource timeout.

The comment still explains a fixed total-duration cap. Line 54 now takes the value from timeoutInterval. State that the caller supplies the cap and that RCTPushy.mm passes 600 by default or the remaining cold-start deadline budget.

📝 Suggested wording
     // Avoid hanging forever on a stalled connection (default resource timeout
     // is 7 days). The 30s idle timeout matches Android's readTimeout and is
-    // what actually catches a stalled transfer; the total-duration cap matches
-    // Android's 10min callTimeout — 300s made a 30MB full package on a slow
-    // (<100KB/s) network fail on iOS while succeeding on Android.
+    // what actually catches a stalled transfer. The caller supplies the
+    // total-duration cap: RCTPushy passes 600s by default (matching Android's
+    // 10min callTimeout — 300s made a 30MB full package on a slow, <100KB/s
+    // network fail on iOS while succeeding on Android), or the remaining
+    // cold-start deadline budget for orchestrated downloads.
     sessionConfig.timeoutIntervalForRequest = 30;
     sessionConfig.timeoutIntervalForResource = MAX(1, timeoutInterval);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/RCTPushy/RCTPushyDownloader.mm` around lines 48 - 54, Update the timeout
comment above sessionConfig.timeoutIntervalForResource to describe the
caller-supplied resource timeout: note that RCTPushy.mm passes 600 seconds by
default or the remaining cold-start deadline budget, while retaining the
explanation of the 30-second idle timeout.
harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts (1)

426-433: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the unknown-attempt-type classification with Android.

Harmony treats any type that is not diff or pdiff as a full attempt. Android treats only "full" as a full attempt, so an unrecognized type there shares the incremental budget and only breaks the inner loop instead of returning false. The three platforms currently disagree for any attempt type the server may add later.

Match one rule across the platforms. The simplest option is to test for the full type explicitly here.

♻️ Optional alignment
-    const isFullAttempt =
-      type !== DOWNLOAD_TYPE_DIFF && type !== DOWNLOAD_TYPE_PDIFF;
+    const isFullAttempt = type === DOWNLOAD_TYPE_FULL;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts` around lines 426 -
433, Update the full-attempt classification in the surrounding download-attempt
logic to recognize only DOWNLOAD_TYPE_FULL explicitly, matching Android’s
behavior. Ensure unrecognized types use incrementalDeadline and follow the
existing inner-loop break path rather than being treated as full attempts;
retain the existing diff/pdiff handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client.ts`:
- Around line 340-344: Update syncNativeConfig so that when
getNativeConfigJson() returns no config, it also clears the persisted
nativeConfig or stores an explicit disabled state through the existing native
bridge, matching the behavior across Android, Harmony, and iOS. Preserve the
current early-return behavior after ensuring stale native configuration cannot
remain active.

In `@src/provider.tsx`:
- Around line 235-247: Update the malformed-hash handling in the provider
response flow to preserve expired APK results: only rewrite the result to `{
upToDate: true }` for non-expired update responses, while retaining
`info.expired` and `downloadUrl` when `ExpiredCheckResult` indicates expiration
without a hash. Add coverage for an expired response with optional update data
and no hash, verifying the APK download path remains active.

---

Nitpick comments:
In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts`:
- Around line 426-433: Update the full-attempt classification in the surrounding
download-attempt logic to recognize only DOWNLOAD_TYPE_FULL explicitly, matching
Android’s behavior. Ensure unrecognized types use incrementalDeadline and follow
the existing inner-loop break path rather than being treated as full attempts;
retain the existing diff/pdiff handling.

In `@ios/RCTPushy/RCTPushyDownloader.mm`:
- Around line 48-54: Update the timeout comment above
sessionConfig.timeoutIntervalForResource to describe the caller-supplied
resource timeout: note that RCTPushy.mm passes 600 seconds by default or the
remaining cold-start deadline budget, while retaining the explanation of the
30-second idle timeout.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7a9556e-68a1-4279-8ff7-bcab4af977b5

📥 Commits

Reviewing files that changed from the base of the PR and between f33bd6a and 283dfd4.

📒 Files selected for processing (17)
  • NATIVE_CHECKUPDATE_DESIGN.md
  • NATIVE_CHECK_FOLLOWUPS.md
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
  • android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java
  • android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • harmony/pushy/src/main/ets/DownloadTaskParams.ts
  • harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • ios/RCTPushy/RCTPushy.mm
  • ios/RCTPushy/RCTPushyDownloader.h
  • ios/RCTPushy/RCTPushyDownloader.mm
  • src/__tests__/client.test.ts
  • src/__tests__/provider.render.test.tsx
  • src/client.ts
  • src/provider.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java

Comment thread src/client.ts
Comment thread src/provider.tsx
sunnylqm and others added 2 commits August 10, 2026 08:31
The 13 closures in 283dfd4 all verified; nine second-order items remain,
concentrated in the unified deadline model (joiners inheriting the
orchestrator's nearly-exhausted budget, Harmony awaits escaping the
budget entirely, wall-clock vs monotonic anchors), plus telemetry
inflation, the artifact-less dead-alert variant, deferred-download UX,
duplicated progress events and a duplicated completion predicate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ios/RCTPushy/RCTPushy.mm (1)

1534-1536: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against a nil body before it reaches the cache dictionary.

[NSString stringWithUTF8String:] returns nil when the C string is not valid UTF-8. If body becomes nil, the flow continues: PushyHttpRequest sends a POST with no HTTPBody, and persistResponseCache:request:config:responseAt: then builds the dictionary literal at Line 1607 with a nil value for @"request". That raises NSInvalidArgumentException. scheduleFromColdStart catches it, so the whole cold-start check is lost instead of failing on the single bad input.

Return early when body is nil.

🛠️ Proposed fix
     std::string bodyJson =
         flowjson::Stringify(updateflow::BuildCheckRequestBody(input));
     NSString *body = [NSString stringWithUTF8String:bodyJson.c_str()];
+    if (body == nil) {
+        RCTLogWarn(@"RCTPushy -- native check: request body is not valid UTF-8");
+        return;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/RCTPushy/RCTPushy.mm` around lines 1534 - 1536, In the request-body
construction near BuildCheckRequestBody, check whether NSString body is nil
immediately after string conversion and return early if so. Prevent the nil
value from reaching PushyHttpRequest or
persistResponseCache:request:config:responseAt:, while preserving the existing
flow for valid UTF-8 bodies.
android/src/main/java/cn/reactnative/modules/update/UpdateContext.java (1)

624-631: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align switchVersion with the completion predicate.

switchVersion() accepts any existing index.bundlejs path, while hasCompletedVersion() requires regular index.bundlejs and .pushy-complete. This can activate an incomplete version after a failure between bundle creation and marker creation. Preserve pre-marker versions with an explicit compatibility path before tightening this check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java`
around lines 624 - 631, Update switchVersion to use the same completion
requirements as hasCompletedVersion, requiring both regular index.bundlejs and
VERSION_COMPLETE_FILE before activation. Before enforcing this for new switches,
retain an explicit compatibility path for pre-marker versions so existing
versions without the completion marker remain usable.
🧹 Nitpick comments (1)
android/src/main/java/cn/reactnative/modules/update/DownloadTask.java (1)

506-513: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Apply the same completion guard before cleanup.

The catch block at Line 476 protects a completed directory from deletion by a duplicate task. This handler does not. If createNewFile() throws IOException while another task has already written the marker and the bundle, cleanUpAfterFailure deletes the completed install.

Reuse hasCompletedPatchDirectory() here for consistent ownership handoff.

♻️ Proposed guard
             } catch (Throwable error) {
                 Log.e(UpdateContext.TAG, "failed to mark completed update", error);
-                cleanUpAfterFailure(taskType);
+                if (!hasCompletedPatchDirectory()) {
+                    cleanUpAfterFailure(taskType);
+                }
                 if (params.listener != null) {
                     params.listener.onDownloadFailed(error);
                 }
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/cn/reactnative/modules/update/DownloadTask.java` around
lines 506 - 513, In the catch handler for marking a completed update, check
hasCompletedPatchDirectory() before calling cleanUpAfterFailure(taskType). Skip
failure cleanup when the patch directory is already complete, while preserving
the existing failure logging, listener notification, and return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@android/src/main/java/cn/reactnative/modules/update/DownloadTask.java`:
- Around line 112-121: Update the timeout construction in DownloadTask.run() so
a positive remainingNanos value converts to a callTimeout of at least one
millisecond, preventing sub-millisecond values from reaching OkHttp. Preserve
the existing expiration check for non-positive remainingNanos and continue using
deadlineNanos with System.nanoTime().

In `@src/provider.tsx`:
- Around line 247-258: Update the decision handling around decideDownload in the
update flow so every applicable decision.action === 'none' result, including
reason 'rolledBack', marks the update as up to date before it can reach the
alert path. Keep client.reportInvalidUpdateOnce('noArtifact', ...) restricted to
the noArtifact reason, while preserving the existing behavior for other decision
actions.

---

Outside diff comments:
In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java`:
- Around line 624-631: Update switchVersion to use the same completion
requirements as hasCompletedVersion, requiring both regular index.bundlejs and
VERSION_COMPLETE_FILE before activation. Before enforcing this for new switches,
retain an explicit compatibility path for pre-marker versions so existing
versions without the completion marker remain usable.

In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 1534-1536: In the request-body construction near
BuildCheckRequestBody, check whether NSString body is nil immediately after
string conversion and return early if so. Prevent the nil value from reaching
PushyHttpRequest or persistResponseCache:request:config:responseAt:, while
preserving the existing flow for valid UTF-8 bodies.

---

Nitpick comments:
In `@android/src/main/java/cn/reactnative/modules/update/DownloadTask.java`:
- Around line 506-513: In the catch handler for marking a completed update,
check hasCompletedPatchDirectory() before calling cleanUpAfterFailure(taskType).
Skip failure cleanup when the patch directory is already complete, while
preserving the existing failure logging, listener notification, and return
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: db1b5735-c1f7-4c43-86af-08a3e050b425

📥 Commits

Reviewing files that changed from the base of the PR and between f33bd6a and 845de20.

📒 Files selected for processing (18)
  • NATIVE_CHECKUPDATE_DESIGN.md
  • NATIVE_CHECK_FOLLOWUPS.md
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
  • android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java
  • android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • harmony/pushy/src/main/ets/DownloadTaskParams.ts
  • harmony/pushy/src/main/ets/MonotonicClock.ts
  • harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • ios/RCTPushy/RCTPushy.mm
  • ios/RCTPushy/RCTPushyDownloader.h
  • ios/RCTPushy/RCTPushyDownloader.mm
  • src/__tests__/client.test.ts
  • src/__tests__/provider.render.test.tsx
  • src/client.ts
  • src/provider.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • ios/RCTPushy/RCTPushyDownloader.mm
  • ios/RCTPushy/RCTPushyDownloader.h
  • android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java
  • android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
  • src/tests/client.test.ts
  • harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • harmony/pushy/src/main/ets/DownloadTask.ts
  • src/client.ts
  • NATIVE_CHECKUPDATE_DESIGN.md

Comment thread src/provider.tsx

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/provider.render.test.tsx`:
- Around line 185-198: Add an assertion immediately after renderProvider(client)
in the rolled-back update test to verify client.checkUpdate was called, while
preserving the existing assertions that no invalid-update report, download, or
alert occurs.
🪄 Autofix

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cca14af7-2bc6-41aa-82cc-c0a2272c8632

📥 Commits

Reviewing files that changed from the base of the PR and between 845de20 and 0f4651e.

📒 Files selected for processing (6)
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • ios/RCTPushy/RCTPushy.mm
  • src/__tests__/provider.render.test.tsx
  • src/__tests__/setup.ts
  • src/provider.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/tests/setup.ts
  • src/provider.tsx
  • android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
  • ios/RCTPushy/RCTPushy.mm

Comment thread src/__tests__/provider.render.test.tsx
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • src/__tests__/provider.render.test.tsx

Commit: 5f5d0cb9bce25b67a46ef1780ffdf0f9ad3db4ca

The changes have been pushed to the agent/harden-native-check-update branch.

Time taken: 5m 25s

coderabbitai Bot and others added 3 commits August 11, 2026 09:29
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
The third round's nine closures all verified. Ten items remain, three of
them merge-relevant: the native check ignoring checkStrategy and undoing
resetToPackagedBundle (the root cause of the now-red e2e-ios, confirmed
across two runs failing different tests — a race signature — while master
is green), the strict deadline comparison that makes the iOS same-type
join path unreachable, and Harmony's STARTUP clock counting deep sleep
where the other two platforms stop. The rest are follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three merge-gate fixes from the fourth review round.

The cold-start check ignored checkStrategy entirely and had no interaction
with resetToPackagedBundle, so an app that had switched automatic checks
off still got silent version switches, and a reset could be undone by a
check that was already in flight — the root cause of the red e2e-ios,
where the beforeEach reset lost its BINARY_BASE assertion to a check that
re-activated the version it had just removed (two runs failed different
tests, the signature of a race; master is green). afterDownload now
requires checkStrategy != null, so the check still runs and still
downloads (a bricked device stays rescuable) but activation waits for JS
or for the server's explicit forceBoot. A reset generation counter,
mirrored on all three platforms, is sampled before the round starts and
compared before activation and before publishing the response cache, so a
reset that lands mid-round always wins; the reset also drops the cached
response that still advertised the removed version.

The iOS registry compared absolute deadlines with a strict >, and a JS
caller always computes now+600 a few seconds after the owner did, so the
same-type join path was unreachable and every second caller serialized
behind the owner. It now compares remaining budget and defers only a
genuinely starved owner (less than half the newcomer's budget left).

Harmony's monotonic clock used TimeType.STARTUP, which counts deep sleep,
while iOS systemUptime and Android nanoTime both stop — a device sleeping
mid-download lost its budget and aborted a rescue the other platforms
complete. Switched to TimeType.ACTIVE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sunnylqm

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

本次推送修复了第四轮评审的三个合并阻断项(详见 NATIVE_CHECK_FOLLOWUPS.md 的「修复说明」):

  1. 原生冷启动检测无视 checkStrategy 且能撞销 resetToPackagedBundle(e2e-ios 变红的根因)——afterDownload 现在要求 checkStrategy != null;并新增三端同构的 reset 代数守卫(开跑前采样,激活与落缓存前比对),reset 同时清掉响应缓存。
  2. iOS 同类型 join 分支不可达——deadline 比较从「绝对时刻严格大于」改为「剩余预算」,仅当 owner 剩余不足新来者一半时才 defer。
  3. Harmony 单调钟语义——TimeType.STARTUP(计深度睡眠)改为 TimeType.ACTIVE,与 iOS systemUptime / Android nanoTime 对齐。

请重点复核:reset 代数守卫在三端的采样/比对点是否完备(是否还有第四个应当拦截的落点)、iOS 剩余预算比较的阈值是否会在极端时序下反向误判、以及 checkStrategy: null 下仍允许 forceBoot 激活是否符合救砖意图。

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Candidate files =='
fd -a '^(NATIVE_CHECK_FOLLOWUPS\.md|NativeCheckOrchestrator\.java|UpdateContext\.java|RCTPushy\.mm|NativeCheckOrchestrator\.ts|UpdateContext\.ts|NativePatchCore\.ts|DownloadTask\.ts|MonotonicClock\.ts)$' .

printf '%s\n' '== Symbol locations =='
rg -n -i -C 4 \
  'reset(ToPackagedBundle)?|generation|nativeCheckResp|KEY_RESP_CACHE|scheduleNativeCheck|afterDownload|checkStrategy|forceBoot|inFlight|defer|deadline|remaining' \
  NATIVE_CHECK_FOLLOWUPS.md \
  android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java \
  android/src/main/java/cn/reactnative/modules/update/UpdateContext.java \
  ios/RCTPushy/RCTPushy.mm \
  harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts \
  harmony/pushy/src/main/ets/UpdateContext.ts \
  harmony/pushy/src/main/ets/DownloadTask.ts \
  harmony/pushy/src/main/ets/MonotonicClock.ts \
  2>/dev/null || true

printf '%s\n' '== Native activation policy definition and callers =='
rg -n -C 4 \
  'ShouldActivateAfterDownload|shouldActivateAfterDownload|forceBoot|afterDownload' \
  cpp/update_flow_core src android ios harmony \
  2>/dev/null || true

Length of output: 147089


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Android: reset, native orchestration, and state writes =='
sed -n '145,225p' android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
sed -n '485,525p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
rg -n -C 4 'void switchVersion|switchVersion\(' \
  android/src/main/java/cn/reactnative/modules/update/UpdateContext.java

printf '%s\n' '== Harmony: reset, native orchestration, and state writes =='
sed -n '180,270p' harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
sed -n '421,475p' harmony/pushy/src/main/ets/UpdateContext.ts
rg -n -C 4 'switchVersion\(' harmony/pushy/src/main/ets/UpdateContext.ts

printf '%s\n' '== iOS: reset, orchestration, cache persistence, and switch operation =='
sed -n '215,235p' ios/RCTPushy/RCTPushy.mm
sed -n '847,902p' ios/RCTPushy/RCTPushy.mm
sed -n '1558,1645p' ios/RCTPushy/RCTPushy.mm
rg -n -C 5 'persistResponseCache|switchVersion:.*error|PushyWithStateLock' \
  ios/RCTPushy/RCTPushy.mm

printf '%s\n' '== Generation-related tests =='
rg -n -i -C 3 'reset.*(generation|native|cache)|generation.*reset|resetToPackagedBundle' \
  android/src/test android/src/androidTest ios harmony src \
  2>/dev/null || true

Length of output: 51409


@sunnylqm 我发现一个合并阻断项。

  • P1 — reset 代数守卫不是原子的,成功路径还有未守卫的响应缓存写入。
    iOS 在 Line 1614 比对 pushyResetGeneration,然后可能在 Line 1624 调用 switchVersion,并在 Line 1633 无条件调用 persistResponseCache。如果 resetToPackagedBundle 在比对后发生,检查轮次仍可重新激活版本,或重新写入 reset 已清除的 nativeCheckResp。Android 和 Harmony 有相同的成功下载尾路径。三端还在代数比对前写入 hash_<hash> 元数据。
    请将 generation 比对和所有状态副作用放入与 reset 互斥的临界区,或使用等效的 compare-and-commit 机制。该机制必须覆盖:
    1. hash_<hash> 元数据写入。
    2. switchVersion
    3. 成功下载后的最终响应缓存写入。
      reset 必须先使旧 generation 失效,再清理状态。否则 reset 与检查轮次之间仍存在 compare-and-act 窗口。

其余两个重点复核结果如下:

  • iOS 的剩余预算条件正确实现了“owner 剩余预算小于 joiner 一半时 defer”。同类型的正常 JS joiner 不会因较晚创建 deadline 而反向进入 deferred 分支。等值时 join,这符合阈值定义。
  • checkStrategy: null 时保留 forceBoot 激活符合救砖意图。forceBoot 是服务端按版本的显式指令。DecideDownload 仍先阻止本机已回滚版本,因此 forceBoot 不会绕过 rollback guard。

✏️ Learnings added
Learnt from: sunnylqm
URL: https://github.com/reactnativecn/react-native-update/pull/620

Timestamp: 2026-08-11T10:18:55.231Z
Learning: For the native cold-start update flow on Android, iOS, and Harmony, `checkStrategy: null` disables client-selected automatic activation but does not disable the native rescue check. A server-controlled per-version `VersionInfo.config.forceBoot` directive may still activate a downloaded rescue version. The local `rolledBackVersion` guard must take precedence over `forceBoot`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

The first version of the reset guard was compare-and-act: the generation
was checked, then switchVersion and the response-cache write followed as
separate steps, and the hash_<hash> version info was written before the
check ran at all. A reset landing inside that window could still be
overwritten — the window shrank from minutes to microseconds but never
closed.

Version info, activation and the response cache now land through one
commitNativeCheckResult(expectedGeneration, ...) entry point that
re-checks the generation and performs every write under the same
mutual exclusion resetToPackagedBundle uses, and the reset invalidates
the generation before it starts clearing state. iOS reuses the existing
state lock (switchVersion grew a lock-free core so the commit can hold
the non-reentrant lock across all three writes), Android shares a commit
lock, and Harmony documents why ArkTS's single thread plus an
await-free commit is already atomic. All three call sites — nothing to
do, download failed, download succeeded — go through the same entry
point, so no write escapes the guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sunnylqm

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

P1 已修复(ffa606f)。你的判断成立:初版守卫是 compare-and-act,且 hash_<hash> 元信息写在守卫之前——窗口从分钟级缩到微秒级但没有真正关闭。

现在改为一次原子提交:版本元信息 + 激活 + 响应缓存合并为 commitNativeCheckResult(expectedGeneration, ...),内部先复核代数再落全部写入;resetToPackagedBundle 与之互斥,并先失效代数再清状态(按你指出的顺序)。互斥手段按端选取:

  • iOS:复用既有 PushyWithStateLock。因为 os_unfair_lock 不可重入,把 switchVersion 拆出无锁核 PushySwitchVersionLocked,让提交能在同一次加锁内完成三处写入。
  • Android:新增共享 commitLockresetToPackagedBundle 与提交同锁。
  • Harmony:ArkTS 单线程且提交方法内无 await,天然原子——已在代码注释里写明理由,而非默认读者会推导。

三个调用点(nothing-to-do、未下载、下载成功)现在全部走同一入口,没有写入能绕过守卫。顺带把 Harmony 的 nativeCheckResp 字面量收敛到共享常量 KEY_RESP_CACHE

验证:JS 178 项、Biome/tsc/DevEco strict、77 金标向量 + ASan/UBSan、.so 符号与 16KB 对齐、iOS clang(DEBUG=0/1)、Android javac(main+oldarch)、OHOS 工具链语法检查。

请复核:iOS 无锁核的拆分是否在所有调用路径上都保持了原有语义(尤其 ignoreRollback 的重置时机),以及 Android commitLockStateSerialRunner 串行执行器之间是否存在锁序问题。

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Commit paths =='
git show --stat --oneline ffa606f
git show --format= --name-only ffa606f

printf '%s\n' '== Relevant symbol map =='
rg -n -C 4 \
  'PushySwitchVersionLocked|PushyWithStateLock|switchVersion|ignoreRollback|commitNativeCheckResult|commitLock|StateSerialRunner|resetToPackagedBundle' \
  ios/RCTPushy/RCTPushy.mm \
  android/src/main/java/cn/reactnative/modules/update/UpdateContext.java \
  android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java \
  android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java \
  2>/dev/null || true

printf '%s\n' '== Structural outlines =='
ast-grep outline ios/RCTPushy/RCTPushy.mm --items all --match 'Pushy.*(Switch|StateLock)|commitNativeCheckResult|resetToPackagedBundle' 2>/dev/null || true
ast-grep outline android/src/main/java/cn/reactnative/modules/update --items all --type method,class --match 'commitNativeCheckResult|resetToPackagedBundle|StateSerialRunner' 2>/dev/null || true

printf '%s\n' '== Focused change diff =='
git show --format= --unified=80 ffa606f -- \
  ios/RCTPushy/RCTPushy.mm \
  android/src/main/java/cn/reactnative/modules/update/UpdateContext.java \
  android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java \
  android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java

Length of output: 109041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Android StateSerialRunner implementation =='
sed -n '1,180p' android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java

printf '%s\n' '== Android StateSerialRunner call sites =='
rg -n -C 8 'StateSerialRunner\.run\s*\(' android/src/main/java/cn/reactnative/modules/update

printf '%s\n' '== Android reset and state-method call paths =='
rg -n -C 10 \
  '\.resetToPackagedBundle\s*\(|resetToPackagedBundle\s*\(|\.switchVersion\s*\(|\.setKv\s*\(|\.markSuccess\s*\(|\.clearFirstTime\s*\(|\.clearRollbackMark\s*\(' \
  android/src/main/java/cn/reactnative/modules/update

printf '%s\n' '== Android commit and reset complete method ranges =='
sed -n '360,430p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
sed -n '470,535p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
sed -n '650,705p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java

printf '%s\n' '== iOS locked-helper references and calling-method ranges =='
rg -n -C 12 'PushySwitchVersionLocked\s*\(' ios/RCTPushy/RCTPushy.mm
sed -n '1218,1245p' ios/RCTPushy/RCTPushy.mm
sed -n '1650,1700p' ios/RCTPushy/RCTPushy.mm

Length of output: 56286


@sunnylqm 复核通过。未发现新的合并阻断项。

iOS

PushySwitchVersionLocked 保持了原有状态转换顺序:

  1. 读取当前状态。
  2. 调用 pushy::state::SwitchVersion
  3. 写入新状态。
  4. ignoreRollback 设为 false

原有 -[RCTPushy switchVersion:error:] 仍先验证 hash,再获取 PushyWithStateLock,然后调用无锁核心。无锁核心只有两个调用点,且两个调用点都已持有状态锁。commitRoundWithGeneration 也在同一把锁内调用该核心。因此,ignoreRollback 的重置时机和原有语义一致。

Android

未发现 commitLockStateSerialRunner 的锁序死锁。

StateSerialRunner 是单线程 Executor,不是调用者持有的锁。它不等待已提交任务完成。resetToPackagedBundle 在该执行器任务中获取 commitLock。原生检查提交直接获取 commitLock,但不提交任务到 StateSerialRunner,也不等待该执行器。因此不存在以下循环:

  • 线程 A 持有 commitLock 并等待 StateSerialRunner
  • 线程 B 在 StateSerialRunner 中等待 commitLock

commitNativeCheckResultresetToPackagedBundle 通过同一把 commitLock 串行化。reset 会先递增 generation,再清理状态。提交会在锁内复核 generation,再写入版本元信息、激活状态和响应缓存。该顺序关闭了此前的 compare-and-act 窗口。


✏️ Learnings added
Learnt from: sunnylqm
URL: https://github.com/reactnativecn/react-native-update/pull/620

Timestamp: 2026-08-11T11:18:02.760Z
Learning: In Android native cold-start update handling, `StateSerialRunner` is a single-thread executor rather than a lock. `resetToPackagedBundle()` runs in a `StateSerialRunner` task and acquires `UpdateContext.commitLock`. `commitNativeCheckResult()` acquires `commitLock` directly and does not wait for `StateSerialRunner`, so these paths have no lock-order cycle.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@sunnylqm
sunnylqm merged commit 6229055 into master Aug 11, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant