fix(macos-test-app): get the label-gated test-macos job green#384
Merged
kraenhansen merged 7 commits intoJul 23, 2026
Merged
Conversation
Two independent failures kept the label-gated `test-macos` job red.
Metro bundle: the babel plugin rewrites `require("*.node")` in the workspace
packages into `require("react-native-node-api").requireNodeAddon(...)`. Those
files live outside the (intentionally non-workspace) macOS app, so Metro
resolves the bare `react-native-node-api` specifier by walking up from the
package directory. npm's hoisted workspaces happened to place it in the
repo-root node_modules; pnpm's isolated node_modules does not, so the rewritten
require failed with "Unable to resolve module react-native-node-api". Add the
app's own node_modules (where its `file:` deps are installed) to Metro's
`nodeModulesPaths` so resolution no longer depends on the root package
manager's hoisting layout.
Native build: GitHub's macos-latest runner now ships Xcode 26.4 / Apple clang
21, which enforces C++20 `consteval` strictly and rejects fmt 11.0.2's
FMT_STRING() usages ("call to consteval function ... is not a constant
expression") in fmt, Yoga and React-logger. React Native 0.81 bundles fmt
11.0.2 and the upstream fix (fmt 12.1.0) only reached RN >= 0.83.9, so patch the
generated Podfile to define FMT_USE_CONSTEVAL=0 across all pods, falling back to
runtime format-string validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T
…il build First CI run showed the Metro-bundle fix works (the job reached xcodebuild), but surfaced two more issues: - fmt consteval still failed: the GCC_PREPROCESSOR_DEFINITIONS FMT_USE_CONSTEVAL=0 define did not reach every fmt-consuming translation unit. Patch the vendored fmt headers directly instead (flip `#define FMT_USE_CONSTEVAL 1` to 0), the approach known to work for RN 0.81 on Xcode 26.4. - "Run test app" failed with "Cannot find module 'mocha'": mocha-remote-server needs mocha at runtime. It resolves via hoisting in the workspace apps, but the standalone macOS app must depend on it explicitly, so add mocha to the deps transferred from apps/test-app. Also add `set -o pipefail` to the xcodebuild step so a build failure is not masked by xcbeautify's exit code (which is what let the previous run limp past a failed archive into the run step). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T
react-native-macos 0.81.8 bumps its vendored fmt from 11.0.2 to 12.1.0 (verified: third-party-podspecs/fmt.podspec pins 11.0.2 at v0.81.1 and 12.1.0 at v0.81.8), which resolves the Xcode 26.4 / Apple clang 21 consteval build failure at its source. Bump REACT_NATIVE_MACOS_VERSION from 0.81.1 to 0.81.8 and remove the manual Podfile header patch that forced FMT_USE_CONSTEVAL off. Core react-native stays at 0.81.5 (facebook's 0.81 line has no 0.81.8; the two packages track independent patch cadences within the same minor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T
….81.8 peer react-native-macos-init failed to install react-native-macos@0.81.8 because it peer-pins react-native 0.81.6 exactly, while REACT_NATIVE_VERSION was still 0.81.5 (the peer for the previous 0.81.1). Bump core to 0.81.6 so the two align. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T
…eeded Experiment: with nodeModulesPaths in place, is the watchFolders push still required for Metro to serve the out-of-tree workspace package sources? Revert if the bundle step fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T
… still needed" This reverts commit 30f8e58.
kraenhansen
merged commit Jul 23, 2026
fd8120c
into
claude/migrate-pnpm-workspaces-qp0m9t
8 of 9 checks passed
kraenhansen
added a commit
that referenced
this pull request
Jul 23, 2026
* chore: migrate from npm workspaces to pnpm Motivation: pnpm's recursive runner (`pnpm -r run`) is fail-fast by default and runs in topological (dependency-graph) order, so the root `bootstrap` and `prerelease` no longer need the non-fail-fast `npm run <s> --workspaces` pattern that buried the real root cause under cascading failures. Workspace + package manager: - Replace the root `workspaces` array with pnpm-workspace.yaml - Add `packageManager: pnpm@10.33.0` and switch devEngines to pnpm ^10 - Allow only esbuild's build script via onlyBuiltDependencies (pnpm 10 blocks dependency lifecycle scripts by default); no shamefully-hoist needed - Replace package-lock.json with pnpm-lock.yaml; keep node_modules isolated Internal deps -> workspace:* protocol (cmake-rn, ferric, gyp-to-cmake, host, node-addon-examples, node-tests, ferric-example, test-app). Add explicit `weak-node-api` edges to node-addon-examples and node-tests so the topological bootstrap sequences weak-node-api (which builds the xcframework/.so they link) before its consumers. Phantom dependencies surfaced by pnpm's isolated node_modules (npm hoisting had masked these): - host: add `@types/babel__core` (used by src/node/babel-plugin/plugin.ts) - host: add `weak-node-api` as a devDependency (used by scripts/generate-injector.mts; previously only a peerDependency) Scripts: - bootstrap: `tsc --build && pnpm -r run bootstrap` (fail-fast, topological) - prerelease/release: make the build explicit instead of relying on npm's implicit prerelease hook (pnpm disables pre/post scripts by default) - test: `pnpm --filter ... run test` - depcheck/run-in-published: replace `npm query .workspace` with `pnpm ls -r` - Pin prettier to 3.6.2: 3.7+ is incompatible with @prettier/plugin-oxc@0.0.4 (regenerating any lockfile floated it to 3.9.5 and crashed the plugin) CI: port check.yml and release.yml to pnpm (pnpm/action-setup, cache: pnpm, `pnpm install --frozen-lockfile`, `--filter`, `pnpm exec`). The ephemeral, non-workspace macOS test app keeps its own `npm install` in scripts/init-macos-test-app.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * ci: fix pnpm github-dep clone on CI and drop redundant --frozen-lockfile pnpm records GitHub git dependencies (node-addon-examples) with an SSH repo URL (git@github.com:...). Stock CI runners have no SSH key, so the clone fails. Add an ad-hoc git config via workflow-level env (GIT_CONFIG_COUNT/KEY_0/VALUE_0) that rewrites git@github.com: to https://github.com/, so the public repo is fetched anonymously over HTTPS in every job without a per-job step. Also drop the explicit `--frozen-lockfile` from `pnpm install`: pnpm enables it by default when the CI environment variable is set, so it was redundant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * fix: declare cmake-rn dependency in weak-node-api weak-node-api's `prebuild:build` script invokes the `cmake-rn` CLI, but the package never declared cmake-rn. Under npm's hoisting every workspace bin was linked into the root node_modules/.bin, so `cmake-rn` was always on PATH. pnpm only links a package's *declared* dependencies' bins, so on CI the bootstrap failed with `cmake-rn: not found` (a phantom bin dependency that only surfaces when the native prebuild runs). Declare `cmake-rn` as a devDependency (workspace:*) so pnpm links its bin into weak-node-api/node_modules/.bin. This introduces a benign dev-time cycle (cmake-rn imports weak-node-api's JS for prebuild paths; weak-node-api's build uses the cmake-rn CLI) which pnpm reports as a warning and handles fine; the topological bootstrap still sequences weak-node-api before its consumers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * ci: adopt main's test-android gating and CMake-pin removal after rebase Reconcile the pnpm-migrated check.yml with two changes that landed on main while this branch was in review: - #382 fixed the CMake 4.2 framework-HEADERS root cause in weak-node-api/CMakeLists.txt (included via the rebase) and removed the now-redundant "Install compatible CMake version" pin from all five macOS jobs. Drop those steps here too; CMAKE_VERSION is retained since test-android still uses it to select the Android SDK cmake package. - #380 gated test-android to labeled PRs only (the ubuntu-self-hosted runner is offline and otherwise leaves the job queued forever on main). With this, check.yml differs from main purely by the pnpm conversion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * fix: resolve unit-test regressions from lockfile regen and workspace filter Two unit-test failures surfaced on CI that are artifacts of the migration, not real behavioural changes: 1. host apple.test.ts (macOS) — `@expo/plist` floated from 0.4.7 (held by main's package-lock) to 0.4.9 when the lockfile was regenerated. 0.4.9's `parse()` returns a null-prototype object, so the test's strict `deepEqual` against a plain object literal fails on the prototype. Pin `@expo/plist` to 0.4.7 to match main's resolved version (same class of fix as the prettier 3.6.2 pin). The null prototype only affects the test's strict equality, not runtime property access. 2. node-addon-examples test (ubuntu/windows) — its `verify-prebuilds` step requires all four Android ABIs, but the unit-tests job only builds x86_64 (no CMAKE_RN_TRIPLETS). This test never actually ran on main: `npm test --workspace node-addon-examples` does not match the package's scoped name (@react-native-node-api/node-addon-examples), so npm silently skipped it. The faithful pnpm `--filter <scoped-name>` translation ran it for the first time and it failed. Drop it from the root `test` filter to preserve main's effective behaviour; properly enabling it would require building every ABI in the job (out of scope for the package-manager migration). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * ci: trigger label-gated jobs Empty commit to start a fresh Check run now that the Apple 🍎 / MacOS 💻 / Ferric 🦀 / weak-node-api labels are applied, so the label-gated iOS, macOS, ferric-apple-triplet and weak-node-api jobs actually run against the pnpm migration. (The workflow only triggers on opened/synchronize/reopened, not on labeling.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * fix: run macOS test-app scaffolding via pnpm dlx (avoid npm devEngines error) The test-macos job failed at `init-macos-test-app`: it scaffolds the app with `npx @react-native-community/cli init` run from the workspace root, whose package.json now declares `devEngines.packageManager: pnpm`. npm 11 refuses to run (EBADDEVENGINES) because it isn't pnpm. Switch that single root-level invocation to `pnpm dlx` — pnpm doesn't enforce devEngines.packageManager (and satisfies it anyway). The remaining steps (`npm install`, `npx react-native-macos-init`) run inside the scaffolded standalone app directory, which isn't linked to the root as a workspace, so they keep using npm/npx unaffected. Keeps the devEngines guardrail intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * fix(macos-test-app): get the label-gated `test-macos` job green (#384) * fix(macos-test-app): unblock the macOS test app bundle and native build Two independent failures kept the label-gated `test-macos` job red. Metro bundle: the babel plugin rewrites `require("*.node")` in the workspace packages into `require("react-native-node-api").requireNodeAddon(...)`. Those files live outside the (intentionally non-workspace) macOS app, so Metro resolves the bare `react-native-node-api` specifier by walking up from the package directory. npm's hoisted workspaces happened to place it in the repo-root node_modules; pnpm's isolated node_modules does not, so the rewritten require failed with "Unable to resolve module react-native-node-api". Add the app's own node_modules (where its `file:` deps are installed) to Metro's `nodeModulesPaths` so resolution no longer depends on the root package manager's hoisting layout. Native build: GitHub's macos-latest runner now ships Xcode 26.4 / Apple clang 21, which enforces C++20 `consteval` strictly and rejects fmt 11.0.2's FMT_STRING() usages ("call to consteval function ... is not a constant expression") in fmt, Yoga and React-logger. React Native 0.81 bundles fmt 11.0.2 and the upstream fix (fmt 12.1.0) only reached RN >= 0.83.9, so patch the generated Podfile to define FMT_USE_CONSTEVAL=0 across all pods, falling back to runtime format-string validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T * fix(macos-test-app): patch fmt header directly, add mocha dep, pipefail build First CI run showed the Metro-bundle fix works (the job reached xcodebuild), but surfaced two more issues: - fmt consteval still failed: the GCC_PREPROCESSOR_DEFINITIONS FMT_USE_CONSTEVAL=0 define did not reach every fmt-consuming translation unit. Patch the vendored fmt headers directly instead (flip `#define FMT_USE_CONSTEVAL 1` to 0), the approach known to work for RN 0.81 on Xcode 26.4. - "Run test app" failed with "Cannot find module 'mocha'": mocha-remote-server needs mocha at runtime. It resolves via hoisting in the workspace apps, but the standalone macOS app must depend on it explicitly, so add mocha to the deps transferred from apps/test-app. Also add `set -o pipefail` to the xcodebuild step so a build failure is not masked by xcbeautify's exit code (which is what let the previous run limp past a failed archive into the run step). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T * chore(macos-test-app): trim inline comments to essentials Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T * fix(macos-test-app): bump react-native-macos to 0.81.8, drop fmt patch react-native-macos 0.81.8 bumps its vendored fmt from 11.0.2 to 12.1.0 (verified: third-party-podspecs/fmt.podspec pins 11.0.2 at v0.81.1 and 12.1.0 at v0.81.8), which resolves the Xcode 26.4 / Apple clang 21 consteval build failure at its source. Bump REACT_NATIVE_MACOS_VERSION from 0.81.1 to 0.81.8 and remove the manual Podfile header patch that forced FMT_USE_CONSTEVAL off. Core react-native stays at 0.81.5 (facebook's 0.81 line has no 0.81.8; the two packages track independent patch cadences within the same minor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T * fix(macos-test-app): bump react-native core to 0.81.6 for the macos 0.81.8 peer react-native-macos-init failed to install react-native-macos@0.81.8 because it peer-pins react-native 0.81.6 exactly, while REACT_NATIVE_VERSION was still 0.81.5 (the peer for the previous 0.81.1). Bump core to 0.81.6 so the two align. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T * test(macos-test-app): drop repo-root watchFolders to check if still needed Experiment: with nodeModulesPaths in place, is the watchFolders push still required for Metro to serve the out-of-tree workspace package sources? Revert if the bundle step fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PM3HdJVbivXpzc2MQJV9T * Revert "test(macos-test-app): drop repo-root watchFolders to check if still needed" This reverts commit 30f8e58. --------- Co-authored-by: Claude <noreply@anthropic.com> * fix: declare react-native-node-api in packages shipping .node addons The react-native-node-api babel plugin rewrites `require("./x.node")` into `require("react-native-node-api").requireNodeAddon(...)`, so every package that ships a Node-API addon has an implicit *runtime* dependency on react-native-node-api once its JS is bundled by Metro. npm hoisted react-native-node-api to the root node_modules, so Metro resolved it from those packages; pnpm's isolated node_modules does not, so the Metro bundle failed with `Unable to resolve module react-native-node-api from packages/ferric-example/ferric_example.js`. This is what made the iOS test app hang for ~6h: Metro errored on the first bundle, but `test:ios:allTests` runs Metro under `mocha-remote -- concurrently`, which never exits on a bundle error and waits for a client that never connects until the job hits GitHub's 6h timeout. It affects the Android app the same way. Declare `react-native-node-api` (workspace:*) in the two addon packages that were missing it — ferric-example and node-addon-examples (node-tests already declares it) — so pnpm links it into their node_modules and Metro can resolve the injected require. Verified locally that it now resolves from each package. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnwodAoNbqPec77191HVXn * fix(node-tests): declare `assert` dependency, fail bundling on unresolved imports (#385) * fix(node-tests): declare assert dependency and fail bundling on unresolved imports The bundled Node.js test for 2_function_arguments requires 'assert', which was previously satisfied as a phantom dependency: npm workspaces hoisted node-addon-examples' assert@2.1.0 ponyfill to the root node_modules, where rolldown resolved and inlined it. Under pnpm's strict node_modules layout the package is no longer reachable from node-tests, so rolldown silently kept a runtime __require("assert") call in the bundle (UNRESOLVED_IMPORT is only a warning), which then fails at runtime on device where Metro cannot resolve it. The failure surfaced as the masked 'test.titlePath(...).forEach is not a function' error, a secondary crash in mocha-remote-server's failure formatter. Declaring assert as a dependency of node-tests lets rolldown inline it again. Also make the bundle step fail hard on unresolved imports, so any future phantom dependency breaks bootstrap loudly instead of failing masked on-device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MBr6N8caYidCuijvV5ak2A * ci: trigger label-gated jobs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MBr6N8caYidCuijvV5ak2A --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goal
Get the
test-macosjob (Test app (macOS), gated behind theMacOS 💻label) green again. It had been red for a couple of months. ✅ It is now green — verified end-to-end on a real macOS runner (scaffold →pod install→react-native bundle→xcodebuild archive→ run tests): run #1096.Three issues had to be fixed; the job died at whichever came first.
1. Metro bundle couldn't resolve
react-native-node-apiThe babel plugin does rewrite
require("./ferric_example.node")→require("react-native-node-api").requireNodeAddon(...). The failure is resolving the resulting bare specifier:ferric_example.jslives in a workspace package outside the (intentionally non-workspace) macOS app, so Metro resolves it by walking up frompackages/ferric-example. Under npm's hoisted workspaces that specifier happened to sit in the repo-rootnode_modules; under pnpm's isolatednode_modulesit doesn't.Fix (
apps/test-app/metro.config.js, macOS-only block): add the app's ownnode_modules— where itsfile:workspace deps are installed bynpm install— to Metro'sresolver.nodeModulesPaths, making resolution independent of the root package manager's hoisting layout.2. Native build failed on
fmtconstevalGitHub's
macos-latestrunner now ships Xcode 26.4 / Apple clang 21, which enforces C++20constevalstrictly and rejects the fmt 11.0.2 that react-native-macos 0.81.1 vendored.Fix (
scripts/init-macos-test-app.ts): bump the scaffolded versions so the fix comes from upstream instead of a local patch —react-native-macos0.81.1 → 0.81.8 (itsfmt.podspecmoves fmt 11.0.2 → 12.1.0, verified at the tag), andreact-native0.81.5 → 0.81.6 because react-native-macos peer-pins an exact core version (0.81.8 → 0.81.6). The two are now kept in lockstep with a comment noting the coupling. No Podfile/header patch needed.3. "Run test app" couldn't find
mochamocha-remote-serverrequiresmochaat runtime. It resolves via hoisting in the workspace apps, but the standalone macOS app must depend on it explicitly.Fix (
scripts/init-macos-test-app.ts): addmochato the dependencies transferred fromapps/test-app.Also
.github/workflows/check.yml: addedset -o pipefailto the Build test app step so anxcodebuildfailure is no longer masked byxcbeautify's exit code.Result
All checks green on the PR:
Lint,Unit tests (ubuntu/windows/macos-latest), and the label-gated Test app (macOS) — with the native build fixed by an upstream version bump rather than a compiler workaround.🤖 Generated with Claude Code