Skip to content

fix(iOS): stage Hermes headers in prebuild compose job so ReactNativeHeaders resolves <hermes/...>#57661

Open
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/prebuild-hermes-headers-compose
Open

fix(iOS): stage Hermes headers in prebuild compose job so ReactNativeHeaders resolves <hermes/...>#57661
chrfalch wants to merge 1 commit into
mainfrom
chrfalch/prebuild-hermes-headers-compose

Conversation

@chrfalch

@chrfalch chrfalch commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

The iOS prebuild workflow (.github/workflows/prebuild-ios-core.yml) splits into two jobs on separate runners:

  • build-rn-slice stages the hermes-ios headers during setup (.build/artifacts/hermes/destroot/include/hermes), but uploads only .build/headers + the SPM Products — not the hermes artifact.
  • compose-xcframework (fresh runner) runs the compose (-cbuildXCFrameworks), which computed the hermes include path from .build/artifacts/hermes/destroot/include. That directory never exists on the compose runner, so hermesHeaders resolved to null and the hermes-header fold in headers-compose.js was silently skipped.

Net effect: the published ReactNativeHeaders.xcframework shipped without the hermes/ namespace, so consumers (e.g. Expo prebuilt) couldn't resolve <hermes/...>. Because the value was null rather than a bad path, not even the existing warning fired — every nightly regressed silently.

Fix

Build-time staging, matching the artifact's self-contained design (the orphaned consumer-side sidecar + health check in download-spm-artifacts.js confirm the bake was intended to happen at compose time):

  • Workflow: compose-xcframework now re-stages the hermes-ios headers before composing — a Set Hermes version step ($GITHUB_ENV doesn't cross jobs) and a Stage Hermes headers step that extracts the tarball into .build/artifacts/hermes. Both are guarded by the same cache-hit condition as the sibling steps.
  • Fail-closed guard: the inline hermes resolution is extracted into an exported resolveHermesHeaders(buildFolder, required) with a findFirst fallback (mirroring the consumer-side stager). When a version-stamped CI cut can't find the headers it now throws instead of silently shipping without hermes/. Gated behind a new --require-hermes flag, which the workflow passes only when version-type is set — so local -c keeps the previous no-fold behavior.

Changelog:

[IOS] [FIXED] - Prebuilt ReactNativeHeaders.xcframework now ships the Hermes public headers so consumers resolve <hermes/...> out of the box

Test plan

  • New unit tests for resolveHermesHeaders (__tests__/xcframework-test.js): resolves at the standard path, resolves via the findFirst fallback, returns null when absent + not required, throws when absent + required.
  • yarn jest packages/react-native/scripts/ios-prebuild/__tests__/xcframework-test.js --no-cache -i → 4/4 pass (red before the resolver was exported).

🤖 Generated with Claude Code

…Headers resolves <hermes/...>

The iOS prebuild workflow splits into two jobs on separate runners.
`build-rn-slice` stages the hermes-ios headers during setup
(`.build/artifacts/hermes/destroot/include/hermes`) but never hands them
to the `compose-xcframework` job. Compose (`-c` -> `buildXCFrameworks`)
computed the hermes include path from a directory that never exists on the
compose runner, silently got `null`, and skipped the hermes-header fold in
`headers-compose.js`. The published ReactNativeHeaders.xcframework therefore
shipped without `hermes/`, so consumers couldn't resolve `<hermes/...>`. No
warning fired, so every nightly regressed silently.

Fix (build-time, matches the artifact's self-contained design):

- compose-xcframework now re-stages the hermes-ios headers before composing
  ("Set Hermes version" + "Stage Hermes headers"), guarded by the same
  cache-hit condition as its sibling steps.
- Extract the hermes-header resolution into an exported
  `resolveHermesHeaders(buildFolder, required)` with a `findFirst` fallback,
  and fail closed (throw) when a version-stamped CI cut can't find the
  headers instead of shipping without them. Gated behind a new
  `--require-hermes` flag, passed by the workflow only when `version-type`
  is set (local dev keeps the previous no-fold behavior).

Adds unit tests for the resolver (found / findFirst fallback /
absent-not-required -> null / absent-required -> throws).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 24, 2026
@chrfalch
chrfalch requested a review from cipolleschi July 24, 2026 08:52
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 24, 2026
@meta-codesync

meta-codesync Bot commented Jul 24, 2026

Copy link
Copy Markdown

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this in D113554720.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants