Skip to content

Commit 7e4e095

Browse files
sunnylqmclaude
andauthored
perf/fix: dedupe cold-start checks, move IO off UI threads, CI cost cuts (#654)
* ci: pin GitHub Actions to commit SHAs again a9793d6 moved every action back to floating major tags while bumping majors, which left publish.yml (id-token: write) trusting whatever a tag points at on release day. Pin each one to the commit its current major tag resolves to, with the exact release in a trailing comment so Dependabot keeps the SHA/version pairs current (as dependabot.yml says). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(js): tighten native config sync, check cache reuse and retry paths - readNativeCheckCache: judge entry age before parsing anything else, compare configs with the shared canonical equality (C1: nativeConfigsEquivalent in updateFlowCore), and stop reading the cache for the rest of the process once it was seen absent or expired. - markJsCheckCompleted: send once per config after it succeeds. - syncNativeConfig: forget the synced value when ownership moves to 'native' so switching back re-writes; a NATIVE_CONFIG_CONFLICT rejection (host called native configure()) now console.errors an actionable message, goes through emitError/onError and stops retries. - markSuccess: share the in-flight native call between concurrent callers (one native call, one report); cleared once settled. - performDownload: HEAD-rank mirrors once per strategy per download; a failed winner is demoted instead of re-racing every attempt. - provider: the Linking effect reads parseTestPayload through a ref and handles the launch URL once, instead of replaying it on every strategy-callback identity change. - utils: web ping no longer passes an unbound Promise.resolve; inline createNonSecureId replaces nanoid/non-secure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(harmony): move bundle/resource IO off the UI thread; config equivalence and conflict IO (UI thread): - SaveFile: rawfile -> sandbox copy now uses async fileIo (was openSync/ readSync/writeSync on the TurboModule thread); withRawFd serializes same-name getRawFd/closeRawFd so one close cannot pull an fd from under another reader. - pdiff: stream the embedded bundle from getRawFd into the temp origin file instead of getRawFileContent + write (which also wrote the whole underlying ArrayBuffer of the view). - bundleHash: new sha256HexRawFdAsync(fd, offset, length) in pushy.cpp (napi async work, pread loop, short read = reject); in-flight computation shared between JS and the native round. Removed the now-unused sync sha256Hex / sha256HexFile exports. - copySandboxFile: fileIo.copyFile (old loop allocated per chunk and took a short read for EOF). Archive limits: - checkOriginalSize: getOriginalSize failures still refuse the archive, now with their own detail ("archive expansion size unavailable") and log line. - planUnknownLengthFreeSpaceProbe extracted from the download write path. - Documented why the compression ratio is checked on archive totals (zlib exposes no per-entry sizes) and how that differs from archive_limits.h. Configuration: - C1: canonical config equivalence (defaults, trailing-slash strip + dedupe of endpoints/queryUrls, key order ignored); used by hasJsRound and by setNativeConfig (equivalent configs no longer bump generations or drop the response cache). - C2: syncNativeConfig rejects with NATIVE_CONFIG_CONFLICT once the host called configure() in this process (local constant until error_codes.h carries kNativeConfigConflict). - SyncCoordinator: one configuration gate for preflight, host API and round, with a consistently trimmed appKey. - Fixed the misplaced setKv doc comment. Tests: hypium suites for the probe plan, original-size refusal and C1; constant parity now asserts kUnknownLengthFreeSpaceProbeBytes is parsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(e2e): build the iOS app once, share Android setup, tighten triggers e2e-ios: a new e2e-ios-build job does pod install + xcodebuild (or the cached-app bundle refresh) and prepares the local update artifacts once, then uploads the .app and .e2e-artifacts/ios as a tarball (tar keeps the executable bits upload-artifact drops). The core and native legs keep their job id and "e2e-ios (core|native)" check names, download the build and only run Detox. The e2e-cache action's trusted writer moves with the work to e2e-ios-build; legs restore read-only. e2e-android: the setup the three jobs duplicated (CLI checkout, node, bun, caches, Java, KVM, deps, local link) is now the e2e-android-setup composite action. The rn077 job keys its Gradle cache on the project generator and the library build.gradle explicitly; it keeps a plain `bun install` because the generated project has no lockfile to freeze. Pushes trigger on master only (main does not exist). Both: drop scripts/** and root bun.lock from the path filters (no e2e build or test step reads them; package.json stays for codegenConfig) and stop copying scripts/ into the linked package, which npm never ships either. Detox: the per-command --maxWorkers 1 moves into .detoxrc.js as an Android-only configuration override (one AVD cannot boot twice). iOS keeps its two workers on cloned simulators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: path-gate native unit tests, dedupe parity check, notify harmony-build test.yml: a dorny/paths-filter job gates cpp-test (cpp/, android/jni/, scripts/test-*) and the macOS ios-purge-restore-test (ios/, cpp/, its script and fixtures). Skipped jobs report success, so required checks and native-gate (which reads the workflow conclusion) stay satisfied. The fuzz step runs apt-get update before installing clang. lint.yml no longer runs check-native-spec-parity.js a second time; bun lint already ends with it. harmony-build keeps its own run because it does not run bun lint. ci-failure-email also watches harmony-build. Dependabot now scans the local composite actions, which pin SHAs as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(publish): require native-required before publishing The release commit must carry a green native-required status (posted by native-gate.yml) before either publish job can run; pending or not yet posted is waited out for up to 45 minutes. The full-history checkout uses a blobless filter since only tags/commits/trees are read, and the HAR comment now points at the files whitelist instead of the .npmignore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(android): cheaper hashing, IO and JNI; canonical config equality - Bundle hash computed once per process on its own thread (shared by JS getBundleHash and the native round) instead of queueing on the download executor behind cleanup/downloads. - Downloads: 64KB reads with emitCompleteSegments; archive SHA-256 computed while streaming (resume hashes the on-disk prefix first); full packages hash index.bundlejs during unzip. Activation re-hash kept (tamper check). 64KB copy buffers in SafeZipFile/UpdateFileUtils. - JNI: registration failures are logged; result classes, constructors and field IDs cached once in JNI_OnLoad; a missing member logs and makes the native throw instead of defaulting fields. - Cleanup keeps current/last/running (String[] keep) and only renames doomed entries into .trash under versionFilesLock; deletion runs after release. - Native check response cache moved to its own SharedPreferences file (migrated); round-incomplete breadcrumb cleared in the commit itself. - getConstants: launch markers applied asynchronously; bundleSha256 cached at launch resolution. - One parseRunnableConfig for all config gates (appKey trimmed). - Canonical config equivalence (shared contract) for the JS-round dedupe and config-change invalidation; syncNativeConfig rejects with NATIVE_CONFIG_CONFLICT once the host called PushyRuntime.configure(). - Shared OkHttpClient base; stale proguard comment fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: drop dead build/publish leftovers - babel.config.js, @babel/core and @react-native/babel-preset: nothing at the root transpiles with Babel (bun runs the tests, tsc type-checks); every Example app carries its own babel config and preset. - .npmignore: package.json `files` decides what ships and check-packlist.js guards it; npm pack --dry-run lists the same 143 files before and after (only package.json's own size changes). - scripts/verify-android-so.sh: unreferenced wrapper around verify-android-so.js. - scripts/prepublish.ts and its prepublishOnly hook: on CI it rewrote package.json to the tag version (check-release-version.js already fails the job unless they match) and re-ran verify-android-so.js (publish.yml runs it explicitly in both publish jobs). Its local branch only served the also-removed publish:local script; releases go through publish.yml. - nanoid: src is inlining its id generator. generate-flow-vectors.ts takes URL from node:url: with prepublish.ts gone the program's global URL (react-native's declaration) no longer type-checks as a node:fs/node:url path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ios): canonical config equality, lock-free digest check, bounded host waits - Config changes: persistConfiguration and hasJsRound compare configs by their canonical form (defaults filled, key order ignored, trailing '/' stripped and endpoints/queryUrls deduplicated). An equivalent config is a no-op; a real change bumps its own pushyNativeConfigGeneration, which the round commit checks next to pushyResetGeneration (now reset-only). - syncNativeConfig rejects with NATIVE_CONFIG_CONFLICT once the host called +configure:completion: in this process. - One PushyLoadRunnableConfig gate for crash-hold install, round start, prepareBundle and the round itself (same disabled/appKey rules). - switchVersion hashes the bundle outside the state lock (+bundleURL takes it on the main thread); under the lock it re-checks the record and the bundle's inode/size/mtime before switching. - prepareBundle runs the round on its own queue and waits a bounded time: new +prepareBundleWithTimeout:completion:, the old selector waits at most 30 minutes. Expiry reports failed/timeout. - tvOS purged-version restore: budget counts from process start, skipped when offline (nw_path_monitor) or with < 2s left, and never hashes the embedded bundle on that path. - Encoded constants decode lazily into a byte buffer; the URL regex is compiled once; one long-lived NSURLSession; OS version from NSProcessInfo instead of main-thread-only UIDevice; the endpoint loop is shared. - podspec: tvOS deployment target from react-native-tvos (or Expo), 15.1 fallback, instead of the iOS one; link Network on tvOS. - Purge-restore harness: late launch, offline and spent-budget cases plus canonical config equality. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: type-check against react-native 0.85 The root devDependency was still react-native 0.73 while the e2e app (and every supported host) runs 0.85; align react-native and @react-native/typescript-config with Example/e2etest (0.85.2). react and react-test-renderer stay on 18 for the bun:test render tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(harmony): mirror NATIVE_CONFIG_CONFLICT from error_codes.h Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: move the design documents into docs/ NATIVE_CHECKUPDATE_DESIGN, NATIVE_CHECK_FOLLOWUPS, BUNDLEHASH_MIGRATION and NATIVE_HOST_API_MIGRATION leave the repository root. The README links and the two path-style mentions (check-native-spec-parity.js, harmony-build.yml) follow; code comments cite them by name only and need no change. check-packlist.js now forbids docs/ outright instead of matching the old root file names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: import URL from node:url for RN 0.85 type declarations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ios): move the native round into RCTPushyOrchestrator.mm Pure move, no behavior change. RCTPushyOrchestrator (its statics, the crash-hold handler and the HTTP/config helpers) now lives in its own file; the helpers both files need (state lock, defaults, state transitions, key constants, PushyHasCompletedVersionAtPath, bundle hash) switch from static to PUSHY_HIDDEN and are declared in the private RCTPushyInternal.h, together with PushyType, the private RCTPushy interface and the orchestrator interface. The podspec keeps that header out of the public umbrella. The purge-restore harness extracts from both sources (one definition across them) and accepts PUSHY_HIDDEN definitions like static ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ios): use error_codes.h for NATIVE_CONFIG_CONFLICT; guard the orchestrator in the packlist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: state that host configure() requires nativeConfigSource: 'native' Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * revert: keep the documented native-then-JS config handover Host configure() followed by a JS takeover in the default 'javascript' mode is a documented, supported flow (last completed write wins). Rejecting it with NATIVE_CONFIG_CONFLICT would break existing integrations. The underlying harm is already gone: equivalent configs (C1) no longer count as a change, so they neither drop the response cache nor cancel in-flight rounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e-harmony): cover the package diff and bundleHash identity The Harmony suite never exercised the package diff (patching against the bundle embedded in the hap), and nothing checked that the bundleHash the client reports matches that bundle. Both paths now read the rawfile via an fd, so cover them on device: - prepare builds v3 for harmony and, once the hap exists, diffs it with hdiffFromApp; build-harmony-e2e.sh reruns that step after assembleHap - the local server serves pdiff v3 to harmony and records the last bundleHash each platform sent (/control/last-bundle-hash) - the suite asserts bundleHash == sha256(hap rawfile bundle) and walks v2 -> pdiff v3 -> v2-track v4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address CodeRabbit review on #654 - ios: run host prepareBundle calls on a concurrent queue so each timeout counts from its own call (a serial queue queued a timed call behind an earlier call's full wait) - android: a failed trash purge is logged, not a failed cleanup task - android: restore the configure() note that JS writes can replace the config unless nativeConfigSource is 'native' - ci: run the C++ parity job when the TS reference or vector generator changes - deps: react / react-test-renderer 19.2.3 to match react-native 0.85 - js: clarify when the native check cache is marked exhausted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: share one C1 config-equivalence vector file across all four platforms cpp/update_flow_core/tests/config-equivalence-vectors.json (58 cases, not packed: !cpp/**/tests) is now the single contract for native config equivalence. Each case is checked in both argument orders by: - JS + Harmony: src/__tests__/configEquivalenceVectors.test.ts (Harmony's UpdateContext.ts evaluated in a vm with imports stripped) - Android: ConfigEquivalenceTest (locates the file by walking up from the module dir, works in-repo and from node_modules) - iOS: the purge-restore harness's config_equivalence case The vectors exposed two outliers, fixed to the agreed rule: - JS did not dedupe non-string entries of endpoints/queryUrls ([1,1] stayed [1,1]); Android, iOS and Harmony dedupe by JSON value. - Android accepted trailing content after the object ('{"a":1} x' and two concatenated objects) because JSONTokener.nextValue() stops after the first value; JS, iOS and Harmony reject it as invalid JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): record the bundleHash of every checkUpdate, not the last non-empty one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b33cc5c commit 7e4e095

100 files changed

Lines changed: 5378 additions & 3295 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: E2E Android setup
2+
description: >-
3+
Shared setup for the Android Detox jobs: toolchains, caches, KVM, the e2e
4+
project's dependencies and the local react-native-update link. Runs after
5+
the react-native-update checkout (this action lives in it).
6+
inputs:
7+
project-dir:
8+
description: E2E app directory, relative to the workspace.
9+
default: Example/e2etest
10+
checkout-cli:
11+
description: Check out and build react-native-update-cli (needed to prepare update artifacts).
12+
default: 'true'
13+
bun-cache-scope:
14+
description: Extra bun cache key segment for jobs whose dependency set differs.
15+
default: ''
16+
bun-cache-key-suffix:
17+
description: Extra hash appended to the bun cache key.
18+
default: ''
19+
gradle-cache-dependency-path:
20+
description: Files whose hash keys the Gradle cache; they must exist before the e2e project is created.
21+
default: |
22+
Example/e2etest/bun.lock
23+
Example/e2etest/android/*.gradle
24+
Example/e2etest/android/**/*.gradle
25+
Example/e2etest/android/gradle/wrapper/gradle-wrapper.properties
26+
create-project:
27+
description: Command that generates the e2e project before its dependencies are installed.
28+
default: ''
29+
frozen-lockfile:
30+
description: Install with --frozen-lockfile (false only for generated projects that have no lockfile).
31+
default: 'true'
32+
runs:
33+
using: composite
34+
steps:
35+
- name: Checkout react-native-update-cli
36+
if: inputs.checkout-cli == 'true'
37+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38+
with:
39+
repository: reactnativecn/react-native-update-cli
40+
path: react-native-update-cli
41+
42+
- name: Setup Node.js
43+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
44+
with:
45+
node-version: 24
46+
47+
- name: Setup Bun
48+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
49+
with:
50+
bun-version: 1.4.0
51+
52+
- name: Cache Bun dependencies
53+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
54+
with:
55+
path: ~/.bun/install/cache
56+
key: ${{ runner.os }}-bun-e2e${{ inputs.bun-cache-scope }}-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }}${{ inputs.bun-cache-key-suffix }}
57+
restore-keys: |
58+
${{ runner.os }}-bun-e2e${{ inputs.bun-cache-scope }}-
59+
${{ runner.os }}-bun-e2e-
60+
61+
- name: Setup Java
62+
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
63+
with:
64+
distribution: temurin
65+
java-version: '17'
66+
cache: gradle
67+
cache-dependency-path: ${{ inputs.gradle-cache-dependency-path }}
68+
69+
- name: Enable KVM
70+
shell: bash
71+
run: |
72+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
73+
sudo udevadm control --reload-rules
74+
sudo udevadm trigger --name-match=kvm
75+
76+
- name: Create e2e project
77+
if: inputs.create-project != ''
78+
shell: bash
79+
env:
80+
CREATE_PROJECT: ${{ inputs.create-project }}
81+
run: eval "$CREATE_PROJECT"
82+
83+
- name: Install e2e project dependencies
84+
shell: bash
85+
working-directory: ${{ inputs.project-dir }}
86+
env:
87+
FROZEN: ${{ inputs.frozen-lockfile }}
88+
run: |
89+
if [ "$FROZEN" = 'true' ]; then
90+
bun install --frozen-lockfile
91+
else
92+
bun install
93+
fi
94+
95+
- name: Link local react-native-update
96+
# Mirrors what an npm install ships (package.json `files`), plus cpp/
97+
# for the iOS/Android native builds; scripts/ is dev-only and unused
98+
# by any build step.
99+
shell: bash
100+
env:
101+
E2E_DIR: ${{ inputs.project-dir }}
102+
run: |
103+
set -euo pipefail
104+
export LOCAL_RNU_VERSION="$(node -p "require('./${E2E_DIR}/node_modules/react-native-update/package.json').version")"
105+
export LOCAL_RNU_DIR="$PWD/$E2E_DIR/.e2e-artifacts/local-react-native-update"
106+
rm -rf "$LOCAL_RNU_DIR" "$E2E_DIR/node_modules/react-native-update"
107+
mkdir -p "$LOCAL_RNU_DIR"
108+
cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/
109+
rsync -a --exclude='.git' src ios android cpp "$LOCAL_RNU_DIR"/
110+
node -e "const fs = require('fs'); const file = process.env.LOCAL_RNU_DIR + '/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');"
111+
ln -s ../.e2e-artifacts/local-react-native-update "$E2E_DIR/node_modules/react-native-update"
112+
113+
- name: Install react-native-update-cli dependencies
114+
if: inputs.checkout-cli == 'true'
115+
shell: bash
116+
working-directory: react-native-update-cli
117+
run: bun install --frozen-lockfile && bun run build

‎.github/actions/e2e-cache/action.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Bounded content-addressed E2E cache
2-
description: Restore shared content caches; only mainline core saves a daily snapshot.
2+
description: Restore shared content caches; only the mainline iOS build job saves a daily snapshot.
33
inputs:
44
path:
55
description: Content-addressed cache directory, never a built app.
@@ -8,7 +8,7 @@ inputs:
88
description: Cache schema/toolchain identity.
99
required: true
1010
write:
11-
description: The core matrix leg may write on a trusted master push.
11+
description: The e2e-ios-build job may write on a trusted master push.
1212
default: 'false'
1313
runs:
1414
using: composite
@@ -17,16 +17,16 @@ runs:
1717
shell: bash
1818
run: echo "day=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT"
1919
- name: Restore read-only snapshot
20-
if: "!(inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios')"
21-
uses: actions/cache/restore@v6
20+
if: "!(inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios-build')"
21+
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
2222
with:
2323
path: ${{ inputs.path }}
2424
key: e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}-${{ steps.epoch.outputs.day }}
2525
restore-keys: |
2626
e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}-
2727
- name: Restore and save trusted daily snapshot
28-
if: inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios'
29-
uses: actions/cache@v6
28+
if: inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios-build'
29+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
3030
with:
3131
path: ${{ inputs.path }}
3232
key: e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}-${{ steps.epoch.outputs.day }}

‎.github/dependabot.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ version: 2
22
updates:
33
# Actions are pinned to commit SHAs in the workflows; Dependabot keeps the
44
# SHA + version comment pairs current instead of a human copying hashes.
5+
# The local composite actions pin SHAs too and are not scanned from "/".
56
- package-ecosystem: github-actions
6-
directory: /
7+
directories:
8+
- /
9+
- /.github/actions/*
710
schedule:
811
interval: weekly
912
groups:

‎.github/workflows/ci-failure-email.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ name: CI Failure Issue
22

33
on:
44
workflow_run:
5+
# native-gate is left out on purpose: it only posts a commit status and
6+
# its own run stays green whatever that status is.
57
workflows:
68
- e2e-android
79
- e2e-ios
10+
- harmony-build
811
- lint
912
- Publish Package to npmjs
1013
- test

0 commit comments

Comments
 (0)