diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml index 4550da74c..fc5f7905b 100644 --- a/.github/workflows/package-publish.yml +++ b/.github/workflows/package-publish.yml @@ -11,6 +11,14 @@ on: options: - next - latest + only_workspace: + # A failed package cannot be retried by re-running the whole release: + # the 39 that already published reject with E403 and the run goes red + # before proving anything about the one that matters. + description: "Publish only this workspace, e.g. @onekeyfe/react-native-bundle-crypto. Leave empty to publish every package." + required: false + default: "" + type: string jobs: package-publish: @@ -28,10 +36,31 @@ jobs: - name: Install Package run: corepack enable && yarn install - name: Publish packages (4 concurrent) + if: inputs.only_workspace == '' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: yarn version:publish --tag "${{ inputs.npm_dist_tag }}" + NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} + run: yarn version:publish --tag "$NPM_DIST_TAG" - name: Publish NativeList after shared build dependencies + if: inputs.only_workspace == '' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} + run: yarn workspace @onekeyfe/react-native-native-list release --tag "$NPM_DIST_TAG" + - name: Publish a single workspace + if: inputs.only_workspace != '' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: yarn workspace @onekeyfe/react-native-native-list release --tag "${{ inputs.npm_dist_tag }}" + NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} + ONLY_WORKSPACE: ${{ inputs.only_workspace }} + run: yarn workspace "$ONLY_WORKSPACE" release --tag "$NPM_DIST_TAG" + # npm reports success the moment it accepts a tarball, which is not the + # same as the version becoming available. 3.0.137 went out green with + # @onekeyfe/react-native-bundle-crypto staged but never committed: the + # version was undownloadable AND unrepublishable, and nothing in this + # workflow noticed. Fail the run that produced it instead. + - name: Verify the published versions are on the registry + env: + NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} + ONLY_WORKSPACE: ${{ inputs.only_workspace }} + run: node scripts/verify-published.mjs "$NPM_DIST_TAG" "$ONLY_WORKSPACE" diff --git a/CHANGELOG.md b/CHANGELOG.md index ee25a4f7f..423cd1a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [3.0.139] - 2026-09-16 + +### Chores +- **release tooling**: The publish workflow trusted npm's exit code, so a package that npm accepted but never made available still turned the run green — exactly how `@onekeyfe/react-native-bundle-crypto@3.0.137` shipped as a silent partial release. Added `scripts/verify-published.mjs`, which reads each published version back from the registry origin (`?write=true`, since the read-through CDN can serve hours-stale documents and would otherwise fail healthy releases) and requires both that the version is listed and that the dist-tag points at it, polling for up to 10 minutes before failing the run. +- **release tooling**: Added an `only_workspace` input to the publish workflow. Retrying one failed package previously meant re-running the whole release, where the packages that already published reject with E403 and turn the run red before proving anything about the one that mattered. +- Bump all 40 publishable packages to 3.0.139. Package contents are unchanged from 3.0.138 — this release exercises the new post-publish verification on a real run. + +## [3.0.138] - 2026-09-16 + +### Bug Fixes +- **bundle-crypto (iOS)**: Drop the x86_64 simulator architecture from the vendored `Gopenpgp.xcframework`. The package was 14.9 MB packed / 42.5 MB unpacked — 9x the next largest package here and ~100x the median — because the framework carries three ~10 MB gomobile static archives, one of them a fat simulator slice holding both arm64 and x86_64. Now 10.7 MB packed / 30.7 MB unpacked. The slice directory is renamed to match its contents and the xcframework manifest updated; the podspec vendors the whole xcframework, so nothing else referenced the old name. **Intel Mac simulator builds are no longer supported by this module**; Apple Silicon simulator, device, and Mac Catalyst are unchanged. Symbol stripping is not an alternative — `strip -S` rejects the gomobile archives with "string table not at the end of the file". + +### Chores +- Bump all 40 publishable packages to 3.0.138. 3.0.137 shipped for 39 of them, but npm left `@onekeyfe/react-native-bundle-crypto@3.0.137` in a staged-but-never-committed state — undownloadable, and permanently rejecting republishing with `409 Cannot publish over previously staged version`. The version number is unrecoverable, so the whole set moves to 3.0.138 to stay in lockstep. + +## [3.0.137] - 2026-09-16 + +### Bug Fixes +- **split-bundle-loader (Android)**: Stop the builtin segment extractor from racing itself. The main and background runtimes resolve the same segment independently, and `extractSemaphore` throttles I/O rather than excluding concurrent work on one path, so both could extract the same segment at once into a shared `.tmp` — two `O_TRUNC` writers on one inode, with `renameTo` failing for whichever thread lost. That loser reported `SPLIT_BUNDLE_NOT_FOUND` for a file that was already on disk and complete, and the JS loader caches that code as a permanent failure, so a millisecond-wide race blanked a route for the rest of the process. Seen on the first launch after an APK replace, where the install-stamp wipe forces every segment to re-extract at once. Extraction is now serialized per path, each attempt writes a uniquely named temp file (so no two writers can publish a partially zeroed HBC, across processes too), and a failed rename re-checks the destination before reporting the segment missing. + +### Chores +- Bump all 40 publishable packages to 3.0.137. Published for 39 of them; `@onekeyfe/react-native-bundle-crypto@3.0.137` never became available (see 3.0.138). + ## [3.0.136] - 2026-09-15 ### Bug Fixes diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json index 1743e1c0e..34bd5cb63 100644 --- a/native-modules/native-logger/package.json +++ b/native-modules/native-logger/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-logger", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-native-logger", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-aes-crypto/package.json b/native-modules/react-native-aes-crypto/package.json index d0558d1d2..d46997fbe 100644 --- a/native-modules/react-native-aes-crypto/package.json +++ b/native-modules/react-native-aes-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-aes-crypto", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-aes-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json index c62b21a0d..a6b10056a 100644 --- a/native-modules/react-native-app-update/package.json +++ b/native-modules/react-native-app-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-app-update", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-app-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-async-storage/package.json b/native-modules/react-native-async-storage/package.json index e688bc6e7..cb21b5f83 100644 --- a/native-modules/react-native-async-storage/package.json +++ b/native-modules/react-native-async-storage/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-async-storage", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-async-storage", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 11763b4fc..f4bc39228 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/Info.plist b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/Info.plist index b86616571..44c9b3dee 100644 --- a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/Info.plist +++ b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/Info.plist @@ -8,20 +8,19 @@ BinaryPath Gopenpgp.framework/Gopenpgp LibraryIdentifier - ios-arm64_x86_64-simulator + ios-arm64-simulator LibraryPath Gopenpgp.framework + MinimumOSVersion + 15.5 SupportedArchitectures arm64 - x86_64 SupportedPlatform ios SupportedPlatformVariant simulator - MinimumOSVersion - 15.5 BinaryPath @@ -30,6 +29,8 @@ ios-arm64-maccatalyst LibraryPath Gopenpgp.framework + MinimumOSVersion + 15.5 SupportedArchitectures arm64 @@ -38,8 +39,6 @@ ios SupportedPlatformVariant maccatalyst - MinimumOSVersion - 15.5 BinaryPath @@ -48,23 +47,23 @@ ios-arm64 LibraryPath Gopenpgp.framework + MinimumOSVersion + 15.5 SupportedArchitectures arm64 SupportedPlatform ios - MinimumOSVersion - 15.5 CFBundlePackageType XFWK + MinimumOSVersion + 15.5 RCTNewArchEnabled XCFrameworkFormatVersion 1.0 - MinimumOSVersion - 15.5 diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Gopenpgp similarity index 51% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Gopenpgp index 8cc3a6563..a2f1d244c 100644 Binary files a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp and b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Gopenpgp differ diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Armor.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Armor.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Constants.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Constants.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Mime.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Mime.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Profile.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Profile.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Universe.objc.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/Universe.objc.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/ref.h similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Headers/ref.h diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Info.plist similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Info.plist diff --git a/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap b/native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Modules/module.modulemap similarity index 100% rename from native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap rename to native-modules/react-native-bundle-crypto/ios/Frameworks/Gopenpgp.xcframework/ios-arm64-simulator/Gopenpgp.framework/Modules/module.modulemap diff --git a/native-modules/react-native-bundle-crypto/package.json b/native-modules/react-native-bundle-crypto/package.json index bb45dc670..960901e8c 100644 --- a/native-modules/react-native-bundle-crypto/package.json +++ b/native-modules/react-native-bundle-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-crypto", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-bundle-crypto", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json index cedd599d0..046dd3e8a 100644 --- a/native-modules/react-native-bundle-update/package.json +++ b/native-modules/react-native-bundle-update/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-bundle-update", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-bundle-update", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index d25b57531..ed5b2902b 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-fs/package.json b/native-modules/react-native-cloud-fs/package.json index cb2cb2d0a..a0d483944 100644 --- a/native-modules/react-native-cloud-fs/package.json +++ b/native-modules/react-native-cloud-fs/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-fs", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-cloud-fs TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index 47d9db161..7f703dd14 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index b5b02c58c..44ea721a9 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-dns-lookup/package.json b/native-modules/react-native-dns-lookup/package.json index ccb3785c2..ac495aae9 100644 --- a/native-modules/react-native-dns-lookup/package.json +++ b/native-modules/react-native-dns-lookup/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-dns-lookup", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-dns-lookup", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 901651f2b..348dcbf9f 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index 8dc5d878d..8c84748e7 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index e8478a5de..79a3df90d 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "3.0.136", + "version": "3.0.139", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-info/package.json b/native-modules/react-native-network-info/package.json index 40ab09f37..160e5b380 100644 --- a/native-modules/react-native-network-info/package.json +++ b/native-modules/react-native-network-info/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-info", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-network-info", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-network-throttle/package.json b/native-modules/react-native-network-throttle/package.json index e547dbb3d..dfb5a7ff4 100644 --- a/native-modules/react-native-network-throttle/package.json +++ b/native-modules/react-native-network-throttle/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-network-throttle", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-network-throttle", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-pbkdf2/package.json b/native-modules/react-native-pbkdf2/package.json index 2f201614a..593bd29bd 100644 --- a/native-modules/react-native-pbkdf2/package.json +++ b/native-modules/react-native-pbkdf2/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pbkdf2", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-pbkdf2", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json index 7029ca866..5119a3ccc 100644 --- a/native-modules/react-native-perf-memory/package.json +++ b/native-modules/react-native-perf-memory/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-memory", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-perf-memory", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-stats/package.json b/native-modules/react-native-perf-stats/package.json index 16e86d8ce..60404ef3e 100644 --- a/native-modules/react-native-perf-stats/package.json +++ b/native-modules/react-native-perf-stats/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perf-stats", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-perf-stats", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-ping/package.json b/native-modules/react-native-ping/package.json index ee09748d1..58ecba05a 100644 --- a/native-modules/react-native-ping/package.json +++ b/native-modules/react-native-ping/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-ping", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-ping TurboModule for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-range-downloader/package.json b/native-modules/react-native-range-downloader/package.json index f0525075d..6cb95c1b1 100644 --- a/native-modules/react-native-range-downloader/package.json +++ b/native-modules/react-native-range-downloader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-range-downloader", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-range-downloader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-sni-connect/package.json b/native-modules/react-native-sni-connect/package.json index 95761b11d..41d23fa6e 100644 --- a/native-modules/react-native-sni-connect/package.json +++ b/native-modules/react-native-sni-connect/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-sni-connect", - "version": "3.0.136", + "version": "3.0.139", "description": "A React Native library for SNI-based HTTP requests with DNS caching and request management", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json index 44bd0b7dc..45b75c29e 100644 --- a/native-modules/react-native-splash-screen/package.json +++ b/native-modules/react-native-splash-screen/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-splash-screen", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-splash-screen", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt b/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt index bdaac1476..d394574d3 100644 --- a/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt +++ b/native-modules/react-native-split-bundle-loader/android/src/main/java/com/splitbundleloader/SplitBundleLoaderModule.kt @@ -13,6 +13,7 @@ import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Semaphore import java.util.concurrent.atomic.AtomicBoolean import android.os.Handler @@ -94,6 +95,18 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) : private const val MAX_CONCURRENT_EXTRACTS = 2 private val extractSemaphore = Semaphore(MAX_CONCURRENT_EXTRACTS) + // Per-path extraction locks. The semaphore above is an I/O THROTTLE, + // not mutual exclusion: with N permits, N threads can extract the SAME + // relativePath at once. Main and background runtimes resolve segments + // independently, so two concurrent extractions of one segment is the + // normal case on the first launch after an APK replace (the + // install-stamp wipe empties the whole tree, so every segment misses). + // Serializing per path means exactly one thread extracts and the rest + // wake up on the exists() fast path. Entries are never removed: + // dropping one would let a waiter lock an Object another thread has + // already replaced, and the map is bounded by the segment count. + private val extractPathLocks = ConcurrentHashMap() + // Wipe-on-APK-replace: avoids stale extracted HBC after overwrite install. // `lastUpdateTime` changes on every APK replacement (adb install -r, // Play Store upgrade, sideload, TestFlight-equivalent). If it differs @@ -479,56 +492,101 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) : val extractDir = File(context.filesDir, "$BUILTIN_EXTRACT_DIR/$nativeVersion") val extractedFile = File(extractDir, relativePath) - // #16: If file exists, verify it's not truncated by checking size against asset + // #16: A complete extraction is reusable with no locking at all. Only + // the happy case short-circuits here — a size mismatch deliberately + // falls through to the lock rather than deleting, because deleting + // outside the per-path lock can destroy a file another thread has just + // published (its writer would then hand out a path we unlinked). if (extractedFile.exists()) { val assetSize = getAssetSize(context.assets, relativePath) if (assetSize >= 0 && extractedFile.length() == assetSize) { return extractedFile.absolutePath } - // Truncated or size mismatch — delete and re-extract - SBLLogger.warn("Extracted file size mismatch for $relativePath, re-extracting") - extractedFile.delete() } - // #18: Limit concurrent extractions - extractSemaphore.acquire() - try { - // Double-check after acquiring semaphore (another thread may have extracted) + // Serialize every extraction of THIS path (see extractPathLocks). The + // lock is taken OUTSIDE the semaphore so a thread waiting here never + // sits on a permit it isn't using, and so the re-check below can hand + // back an already-extracted file without spending one. + synchronized(extractPathLocks.computeIfAbsent(relativePath) { Any() }) { + // Re-check under the lock: another thread may have extracted while + // we waited. Nothing in this process can be mid-publish for this + // path now, so a stale file is safe to delete and re-extract. if (extractedFile.exists()) { - return extractedFile.absolutePath + val assetSize = getAssetSize(context.assets, relativePath) + if (assetSize >= 0 && extractedFile.length() == assetSize) { + return extractedFile.absolutePath + } + SBLLogger.warn("Extracted file size mismatch for $relativePath, re-extracting") + extractedFile.delete() } - val assets: AssetManager = context.assets - return try { - // Extract to temp file first, then atomically rename - val tempFile = File(extractedFile.parentFile, "${extractedFile.name}.tmp") - assets.open(relativePath).use { input -> - extractedFile.parentFile?.let { parent -> - if (!parent.exists()) parent.mkdirs() + // #18: Limit concurrent extractions + extractSemaphore.acquire() + // Extract to a temp file first, then atomically rename. The temp + // name must be UNIQUE per attempt: a shared ".tmp" lets two + // writers open the same file with O_TRUNC and interleave into each + // other's stream, so the winner can publish a partially zeroed HBC + // while the loser's renameTo fails on a source that was already + // moved away. extractPathLocks covers this process; the unique name + // keeps it safe across processes too. Because a unique temp file is + // never reused by a later attempt, it MUST be cleaned up on every + // failure path or partial writes accumulate until the next APK + // replace — hence the finally below. + val tempFile = File( + extractedFile.parentFile, + "${extractedFile.name}.${android.os.Process.myTid()}-${System.nanoTime()}.tmp" + ) + try { + val assets: AssetManager = context.assets + return try { + var written = 0L + assets.open(relativePath).use { input -> + extractedFile.parentFile?.let { parent -> + if (!parent.exists()) parent.mkdirs() + } + FileOutputStream(tempFile).use { output -> + val buffer = ByteArray(8192) + var len: Int + while (input.read(buffer).also { len = it } != -1) { + output.write(buffer, 0, len) + written += len + } + } } - FileOutputStream(tempFile).use { output -> - val buffer = ByteArray(8192) - var len: Int - while (input.read(buffer).also { len = it } != -1) { - output.write(buffer, 0, len) + // Atomic rename prevents partial file observation + if (tempFile.renameTo(extractedFile)) { + SBLLogger.info("[extractBuiltin] extracted $relativePath → ${extractedFile.absolutePath} (${extractedFile.length()} bytes)") + extractedFile.absolutePath + } else { + // A failed rename is NOT proof the segment is missing: + // another process may have published the same bytes at + // the destination. Returning null here would turn a + // transient race into SPLIT_BUNDLE_NOT_FOUND, which the + // JS loader caches as a permanent failure. Compare + // against what we just wrote rather than re-reading the + // asset, and require an exact match — accepting an + // unknown size would be laxer than the checks above. + if (extractedFile.exists() && extractedFile.length() == written) { + SBLLogger.warn("[extractBuiltin] rename lost the race for $relativePath, using file published by another writer: ${extractedFile.absolutePath} ($written bytes)") + extractedFile.absolutePath + } else { + SBLLogger.warn("[extractBuiltin] rename failed for $relativePath: ${tempFile.absolutePath} → ${extractedFile.absolutePath}") + null } } + } catch (e: IOException) { + SBLLogger.warn("[extractBuiltin] IOException for $relativePath: ${e.javaClass.simpleName}: ${e.message}") + null } - // Atomic rename prevents partial file observation - if (tempFile.renameTo(extractedFile)) { - SBLLogger.info("[extractBuiltin] extracted $relativePath → ${extractedFile.absolutePath} (${extractedFile.length()} bytes)") - extractedFile.absolutePath - } else { - SBLLogger.warn("[extractBuiltin] rename failed for $relativePath: ${tempFile.absolutePath} → ${extractedFile.absolutePath}") + } finally { + // No-op after a successful rename (the temp file is gone); + // reclaims the partial write on every other path. + if (tempFile.exists()) { tempFile.delete() - null } - } catch (e: IOException) { - SBLLogger.warn("[extractBuiltin] IOException for $relativePath: ${e.javaClass.simpleName}: ${e.message}") - null + extractSemaphore.release() } - } finally { - extractSemaphore.release() } } diff --git a/native-modules/react-native-split-bundle-loader/package.json b/native-modules/react-native-split-bundle-loader/package.json index d0ecba500..1e00ee7bc 100644 --- a/native-modules/react-native-split-bundle-loader/package.json +++ b/native-modules/react-native-split-bundle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-split-bundle-loader", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-split-bundle-loader", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-tcp-socket/package.json b/native-modules/react-native-tcp-socket/package.json index a62da864f..3bceac45e 100644 --- a/native-modules/react-native-tcp-socket/package.json +++ b/native-modules/react-native-tcp-socket/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tcp-socket", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-tcp-socket", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-zip-archive/package.json b/native-modules/react-native-zip-archive/package.json index 53f4cd866..4d860c1a2 100644 --- a/native-modules/react-native-zip-archive/package.json +++ b/native-modules/react-native-zip-archive/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-zip-archive", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-zip-archive Nitro HybridObject for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-auto-size-input/package.json b/native-views/react-native-auto-size-input/package.json index 65edfa605..8d5ccb3e2 100644 --- a/native-views/react-native-auto-size-input/package.json +++ b/native-views/react-native-auto-size-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-auto-size-input", - "version": "3.0.136", + "version": "3.0.139", "description": "Auto-sizing text input with font scaling, prefix and suffix support", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-chart-webview/package.json b/native-views/react-native-chart-webview/package.json index f8163e3a1..c28ef9bfc 100644 --- a/native-views/react-native-chart-webview/package.json +++ b/native-views/react-native-chart-webview/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-chart-webview", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-chart-webview", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-image/package.json b/native-views/react-native-image/package.json index 1023d2418..af9843cb7 100644 --- a/native-views/react-native-image/package.json +++ b/native-views/react-native-image/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-image", - "version": "3.0.136", + "version": "3.0.139", "description": "High-performance native image view for OneKey", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -81,7 +81,7 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@onekeyfe/react-native-skeleton": "3.0.136", + "@onekeyfe/react-native-skeleton": "3.0.139", "react": "*", "react-native": "*", "react-native-nitro-modules": "0.37.0" diff --git a/native-views/react-native-native-list/package.json b/native-views/react-native-native-list/package.json index 2b4ef2f1d..28281aaab 100644 --- a/native-views/react-native-native-list/package.json +++ b/native-views/react-native-native-list/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-list", - "version": "3.0.136", + "version": "3.0.139", "description": "Template-driven native RecyclerView and UICollectionView for React Native", "source": "./src/index.ts", "main": "./lib/module/index.js", @@ -83,8 +83,8 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@onekeyfe/react-native-image": "3.0.136", - "@onekeyfe/react-native-native-logger": "3.0.136", + "@onekeyfe/react-native-image": "3.0.139", + "@onekeyfe/react-native-native-logger": "3.0.139", "react": "*", "react-native": "*", "react-native-nitro-modules": "0.37.0" diff --git a/native-views/react-native-native-sheet/package.json b/native-views/react-native-native-sheet/package.json index 29643c228..d785886ad 100644 --- a/native-views/react-native-native-sheet/package.json +++ b/native-views/react-native-native-sheet/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-native-sheet", - "version": "3.0.136", + "version": "3.0.139", "description": "Native bottom sheet host for arbitrary React Native content", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-pager-view/package.json b/native-views/react-native-pager-view/package.json index ab330f20e..a89146fe5 100644 --- a/native-views/react-native-pager-view/package.json +++ b/native-views/react-native-pager-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-pager-view", - "version": "3.0.136", + "version": "3.0.139", "description": "React Native wrapper for Android and iOS ViewPager", "source": "./src/index.tsx", "main": "./lib/module/index.js", @@ -66,7 +66,7 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@onekeyfe/react-native-native-logger": "3.0.136", + "@onekeyfe/react-native-native-logger": "3.0.139", "react": "*", "react-native": "*" }, diff --git a/native-views/react-native-perp-depth-bar/package.json b/native-views/react-native-perp-depth-bar/package.json index 36919c302..37c3f02fd 100644 --- a/native-views/react-native-perp-depth-bar/package.json +++ b/native-views/react-native-perp-depth-bar/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-perp-depth-bar", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-perp-depth-bar", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-scroll-guard/package.json b/native-views/react-native-scroll-guard/package.json index 3b5f47545..895a54ccd 100644 --- a/native-views/react-native-scroll-guard/package.json +++ b/native-views/react-native-scroll-guard/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-scroll-guard", - "version": "3.0.136", + "version": "3.0.139", "description": "A native view wrapper that prevents parent scrollable containers (PagerView/ViewPager2) from intercepting child scroll gestures", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-segment-slider/package.json b/native-views/react-native-segment-slider/package.json index 32114ac7c..e0fa5d764 100644 --- a/native-views/react-native-segment-slider/package.json +++ b/native-views/react-native-segment-slider/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-segment-slider", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-segment-slider", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index 50ea486eb..be86beabf 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "3.0.136", + "version": "3.0.139", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-views/react-native-tab-view/package.json b/native-views/react-native-tab-view/package.json index a9f6b26ea..6edf25dd4 100644 --- a/native-views/react-native-tab-view/package.json +++ b/native-views/react-native-tab-view/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-tab-view", - "version": "3.0.136", + "version": "3.0.139", "description": "Native Bottom Tabs for React Native (UIKit implementation)", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-text-input/package.json b/native-views/react-native-text-input/package.json index 9831ebe58..9a6a145c4 100644 --- a/native-views/react-native-text-input/package.json +++ b/native-views/react-native-text-input/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-text-input", - "version": "3.0.136", + "version": "3.0.139", "description": "React Native TextInput with native paste events", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/native-views/react-native-text/package.json b/native-views/react-native-text/package.json index 8ce20fc96..81eabfd4d 100644 --- a/native-views/react-native-text/package.json +++ b/native-views/react-native-text/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-text", - "version": "3.0.136", + "version": "3.0.139", "description": "Opt-in native text rendering for React Native", "source": "./src/index.ts", "main": "./lib/module/index.js", diff --git a/scripts/validate-npm-dist-tag.mjs b/scripts/validate-npm-dist-tag.mjs index 5206e7852..cdc01ee01 100644 --- a/scripts/validate-npm-dist-tag.mjs +++ b/scripts/validate-npm-dist-tag.mjs @@ -29,7 +29,7 @@ export function validateNpmDistTag(distTag, releaseWorkspaces) { ); } -async function loadReleaseWorkspaces(repoRoot) { +export async function loadReleaseWorkspaces(repoRoot) { const rootPackage = JSON.parse( await readFile(join(repoRoot, "package.json"), "utf8") ); diff --git a/scripts/verify-published.mjs b/scripts/verify-published.mjs new file mode 100644 index 000000000..01a79e442 --- /dev/null +++ b/scripts/verify-published.mjs @@ -0,0 +1,177 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { loadReleaseWorkspaces } from "./validate-npm-dist-tag.mjs"; + +const REGISTRY = "https://registry.npmjs.org"; + +// npm reports a publish as successful the moment it accepts the tarball, but +// acceptance is not availability: a version can sit staged and never become +// downloadable, and the job still goes green. That happened to +// @onekeyfe/react-native-bundle-crypto@3.0.137 — npm printed +// `+ @onekeyfe/react-native-bundle-crypto@3.0.137`, every workflow step passed, +// and the version never appeared. It could not even be republished afterwards +// ("409 Cannot publish over previously staged version"), so the number was +// burned and the whole set had to move to 3.0.138. This check exists so that a +// release like that fails loudly in the run that produced it. +const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; +const POLL_INTERVAL_MS = 15 * 1000; + +/** + * The workspaces a run was supposed to publish. `onlyWorkspace` mirrors the + * workflow input of the same name, so a single-package retry verifies only + * that package instead of failing on the 39 it did not touch. + */ +export function selectWorkspaces(releaseWorkspaces, onlyWorkspace) { + if (!onlyWorkspace) { + return releaseWorkspaces; + } + const match = releaseWorkspaces.find(({ name }) => name === onlyWorkspace); + if (!match) { + const known = releaseWorkspaces + .map(({ name }) => name) + .sort() + .join(", "); + throw new Error( + `Unknown workspace ${onlyWorkspace}. Publishable workspaces: ${known}` + ); + } + return [match]; +} + +/** + * `?write=true` bypasses npm's read-through CDN. Plain reads (and `npm view`) + * can serve a package document that is hours stale, which makes a healthy + * publish look missing and would fail this check for the wrong reason. + */ +export function packumentUrl(name) { + return `${REGISTRY}/${encodeURIComponent(name)}?write=true`; +} + +/** + * A version counts as published only when the registry lists it AND the + * dist-tag we published under points at it. The tag matters: a version present + * but untagged means consumers resolving by tag still get the old one. + */ +export function checkPackument(packument, version, distTag) { + const versions = packument?.versions ?? {}; + if (!Object.prototype.hasOwnProperty.call(versions, version)) { + return { ok: false, reason: `${version} is absent from the registry` }; + } + const tagged = packument?.["dist-tags"]?.[distTag]; + if (tagged !== version) { + return { + ok: false, + reason: `dist-tag ${distTag} points at ${ + tagged ?? "nothing" + }, expected ${version}`, + }; + } + return { ok: true }; +} + +async function fetchPackument(name) { + const response = await fetch(packumentUrl(name), { + headers: { accept: "application/json" }, + }); + if (!response.ok) { + return null; + } + return response.json(); +} + +const sleep = (ms) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +export async function verifyPublished( + workspaces, + distTag, + { timeoutMs = DEFAULT_TIMEOUT_MS, log = console.log } = {} +) { + const deadline = Date.now() + timeoutMs; + let pending = workspaces; + let lastReasons = new Map(); + + while (pending.length > 0) { + const stillPending = []; + for (const workspace of pending) { + let result; + try { + const packument = await fetchPackument(workspace.name); + result = packument + ? checkPackument(packument, workspace.version, distTag) + : { ok: false, reason: "registry returned no package document" }; + } catch (error) { + // A transient network blip must not fail the release; keep polling. + result = { ok: false, reason: `registry request failed: ${error.message}` }; + } + if (result.ok) { + log(` ok ${workspace.name}@${workspace.version}`); + } else { + lastReasons.set(workspace.name, result.reason); + stillPending.push(workspace); + } + } + pending = stillPending; + if (pending.length === 0) { + break; + } + if (Date.now() >= deadline) { + break; + } + log( + ` waiting ${pending.length} package(s) not visible yet; re-checking in ${ + POLL_INTERVAL_MS / 1000 + }s` + ); + await sleep(POLL_INTERVAL_MS); + } + + return pending.map((workspace) => ({ + name: workspace.name, + version: workspace.version, + reason: lastReasons.get(workspace.name) ?? "unknown", + })); +} + +async function main() { + const [distTag, onlyWorkspaceArg] = process.argv.slice(2); + if (!distTag) { + throw new Error("Usage: node scripts/verify-published.mjs [workspace]"); + } + const onlyWorkspace = onlyWorkspaceArg?.trim() || ""; + + const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); + const releaseWorkspaces = await loadReleaseWorkspaces(repoRoot); + const workspaces = selectWorkspaces(releaseWorkspaces, onlyWorkspace); + + console.log( + `Verifying ${workspaces.length} package(s) on ${REGISTRY} under dist-tag ${distTag}` + ); + const missing = await verifyPublished(workspaces, distTag); + + if (missing.length > 0) { + console.error( + `\n${missing.length} package(s) did not become available:\n` + + missing + .map(({ name, version, reason }) => ` - ${name}@${version}: ${reason}`) + .join("\n") + + "\n\nnpm accepted these publishes but the registry never served them. " + + "The version numbers are likely burned (republishing returns 409 " + + "'Cannot publish over previously staged version'), so the fix is " + + "usually to bump and release again." + ); + process.exitCode = 1; + return; + } + console.log(`\nAll ${workspaces.length} package(s) verified.`); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/scripts/verify-published.test.mjs b/scripts/verify-published.test.mjs new file mode 100644 index 000000000..fe89cd4cb --- /dev/null +++ b/scripts/verify-published.test.mjs @@ -0,0 +1,109 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + checkPackument, + packumentUrl, + selectWorkspaces, + verifyPublished, +} from "./verify-published.mjs"; + +const workspaces = [ + { name: "@onekeyfe/module-a", version: "3.0.1" }, + { name: "@onekeyfe/module-b", version: "3.0.1" }, +]; + +test("verifies every workspace when no single workspace is given", () => { + assert.deepEqual(selectWorkspaces(workspaces, ""), workspaces); + assert.deepEqual(selectWorkspaces(workspaces, undefined), workspaces); +}); + +test("narrows to one workspace for a single-package retry", () => { + assert.deepEqual(selectWorkspaces(workspaces, "@onekeyfe/module-b"), [ + workspaces[1], + ]); +}); + +test("rejects an unknown workspace name rather than verifying nothing", () => { + assert.throws( + () => selectWorkspaces(workspaces, "@onekeyfe/typo"), + /Unknown workspace @onekeyfe\/typo/ + ); +}); + +test("bypasses the CDN so a fresh publish is not read as missing", () => { + assert.equal( + packumentUrl("@onekeyfe/module-a"), + "https://registry.npmjs.org/%40onekeyfe%2Fmodule-a?write=true" + ); +}); + +test("accepts a version that is listed and carries the dist-tag", () => { + const packument = { + versions: { "3.0.1": {} }, + "dist-tags": { latest: "3.0.1" }, + }; + assert.deepEqual(checkPackument(packument, "3.0.1", "latest"), { ok: true }); +}); + +test("rejects the staged-but-never-committed case", () => { + // What @onekeyfe/react-native-bundle-crypto@3.0.137 looked like: npm printed + // a successful publish, the packument never listed the version. + const packument = { + versions: { "3.0.0": {} }, + "dist-tags": { latest: "3.0.0" }, + }; + const result = checkPackument(packument, "3.0.1", "latest"); + assert.equal(result.ok, false); + assert.match(result.reason, /3\.0\.1 is absent from the registry/); +}); + +test("rejects a version that is present but not tagged", () => { + const packument = { + versions: { "3.0.0": {}, "3.0.1": {} }, + "dist-tags": { latest: "3.0.0" }, + }; + const result = checkPackument(packument, "3.0.1", "latest"); + assert.equal(result.ok, false); + assert.match(result.reason, /dist-tag latest points at 3\.0\.0/); +}); + +test("reports every package that never became available", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => ({ + ok: true, + json: async () => ({ versions: {}, "dist-tags": {} }), + }); + try { + const missing = await verifyPublished(workspaces, "latest", { + timeoutMs: 0, + log: () => {}, + }); + assert.deepEqual( + missing.map(({ name }) => name), + ["@onekeyfe/module-a", "@onekeyfe/module-b"] + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("returns nothing missing once every package is visible", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => ({ + ok: true, + json: async () => ({ + versions: { "3.0.1": {} }, + "dist-tags": { latest: "3.0.1" }, + }), + }); + try { + const missing = await verifyPublished(workspaces, "latest", { + timeoutMs: 0, + log: () => {}, + }); + assert.deepEqual(missing, []); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/yarn.lock b/yarn.lock index 6b80d7781..21ee9a03c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3550,7 +3550,7 @@ __metadata: react-test-renderer: "npm:19.2.3" typescript: "npm:^5.9.2" peerDependencies: - "@onekeyfe/react-native-skeleton": 3.0.136 + "@onekeyfe/react-native-skeleton": 3.0.139 react: "*" react-native: "*" react-native-nitro-modules: 0.37.0 @@ -3651,8 +3651,8 @@ __metadata: react-native-nitro-modules: "npm:0.37.0" typescript: "npm:^5.9.2" peerDependencies: - "@onekeyfe/react-native-image": 3.0.136 - "@onekeyfe/react-native-native-logger": 3.0.136 + "@onekeyfe/react-native-image": 3.0.139 + "@onekeyfe/react-native-native-logger": 3.0.139 react: "*" react-native: "*" react-native-nitro-modules: 0.37.0 @@ -3804,7 +3804,7 @@ __metadata: react-native-builder-bob: "npm:^0.40.13" typescript: "npm:^5.9.2" peerDependencies: - "@onekeyfe/react-native-native-logger": 3.0.136 + "@onekeyfe/react-native-native-logger": 3.0.139 react: "*" react-native: "*" languageName: unknown