From e2056951f831bed3494faf1ca26dce11ffb04cd9 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Sun, 19 Jul 2026 22:15:04 +0200 Subject: [PATCH] fix(iOS): hard-fail the prebuild when composed headers ship the version sentinel Follow-up to #57603: add a --require-stamped-version gate to headers-verify.js that fails the compose verification when any ReactNativeVersion.h in the composed artifacts still contains the 1000.0.0 dev sentinel, and pass the flag from the workflow whenever a version-type is set. Turns any future regression of the version stamping into a red prebuild instead of silently broken community libraries. Co-Authored-By: Claude Fable 5 --- .github/workflows/prebuild-ios-core.yml | 2 +- .../scripts/ios-prebuild/headers-verify.js | 60 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prebuild-ios-core.yml b/.github/workflows/prebuild-ios-core.yml index 36b2e13aada2..957445a5e065 100644 --- a/.github/workflows/prebuild-ios-core.yml +++ b/.github/workflows/prebuild-ios-core.yml @@ -200,7 +200,7 @@ jobs: # privileged-consumer/Expo fixtures). Catches consumer-facing header # regressions here instead of in downstream builds. cd packages/react-native - node scripts/ios-prebuild/headers-verify.js --flavor "${{ matrix.flavor }}" + node scripts/ios-prebuild/headers-verify.js --flavor "${{ matrix.flavor }}" ${{ inputs.version-type != '' && '--require-stamped-version' || '' }} - name: Compress and Rename XCFramework if: steps.restore-ios-xcframework.outputs.cache-hit != 'true' run: | diff --git a/packages/react-native/scripts/ios-prebuild/headers-verify.js b/packages/react-native/scripts/ios-prebuild/headers-verify.js index 79bebc800348..30a9ce778954 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-verify.js +++ b/packages/react-native/scripts/ios-prebuild/headers-verify.js @@ -35,6 +35,7 @@ * Usage: * node scripts/ios-prebuild/headers-verify.js [--flavor Debug|Release] * [--artifacts ] [--skip-compile] [--update-baseline] + * [--require-stamped-version] */ const {computeInventory} = require('./headers-inventory'); @@ -410,6 +411,49 @@ function runCompileGates( } } +// --------------------------------------------------------------------------- +// Version stamp gate +// --------------------------------------------------------------------------- + +/** + * Release/nightly artifacts must not ship ReactNativeVersion.h with the + * 1000.0.0 dev sentinel: the compose step copies headers from the source + * tree, so a compose job that forgot to run set-rn-artifacts-version.js + * would silently publish a sentinel header, breaking every library that + * gates code on REACT_NATIVE_VERSION_MAJOR/MINOR. + */ +function verifyVersionStamp(artifactsDir /*: string */) /*: void */ { + const copies = []; + const walk = (dir /*: string */) => { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + const name = String(entry.name); + const full = path.join(dir, name); + if (entry.isDirectory()) { + walk(full); + } else if (name === 'ReactNativeVersion.h') { + copies.push(full); + } + } + }; + walk(artifactsDir); + if (copies.length === 0) { + throw new Error( + `no ReactNativeVersion.h found under ${artifactsDir} — cannot verify the version stamp.`, + ); + } + const unstamped = copies.filter(f => + /REACT_NATIVE_VERSION_MAJOR\s+1000\b/.test(fs.readFileSync(f, 'utf8')), + ); + if (unstamped.length > 0) { + throw new Error( + `ReactNativeVersion.h still contains the 1000.0.0 dev sentinel — run ` + + `scripts/releases/set-rn-artifacts-version.js before composing:\n ` + + unstamped.join('\n '), + ); + } + log(`version stamp OK (${copies.length} copies checked).`); +} + // --------------------------------------------------------------------------- // CLI // --------------------------------------------------------------------------- @@ -419,11 +463,13 @@ function parseArgs(argv /*: Array */) /*: { artifacts: ?string, skipCompile: boolean, updateBaseline: boolean, + requireStampedVersion: boolean, } */ { let flavor = 'Debug'; let artifacts /*: ?string */ = null; let skipCompile = false; let updateBaseline = false; + let requireStampedVersion = false; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--flavor') { flavor = argv[++i]; @@ -433,9 +479,17 @@ function parseArgs(argv /*: Array */) /*: { skipCompile = true; } else if (argv[i] === '--update-baseline') { updateBaseline = true; + } else if (argv[i] === '--require-stamped-version') { + requireStampedVersion = true; } } - return {flavor, artifacts, skipCompile, updateBaseline}; + return { + flavor, + artifacts, + skipCompile, + updateBaseline, + requireStampedVersion, + }; } function main(argv /*:: ?: Array */) /*: void */ { @@ -460,6 +514,10 @@ function main(argv /*:: ?: Array */) /*: void */ { `(node scripts/ios-prebuild -c -f ${args.flavor}).`, ); } + if (args.requireStampedVersion) { + verifyVersionStamp(artifactsDir); + } + const {reactSlice, rnhHeaders} = verifyStructural(plan, artifactsDir); if (args.skipCompile) {