Demonstrates the expo-brownfield Android publish bug filed at
expo/expo#45749.
expo-brownfield's POM rewriter (introduced in PR #43693, present in 55.0.22 / 55.0.23)
emits two <version> elements inside the react-android dependency of any
subproject that declares implementation "com.facebook.react:react-android:$reactNativeVersion".
Maven rejects the duplicate → publishBrownfieldReleasePublicationToMavenLocal fails.
react-native-keyboard-controller@1.20.7 is one such subproject (used here to trigger the bug).
bunx create-expo-app@latest app --template default@sdk-55 --no-install
cd app
bun install
bunx expo install expo-brownfield react-native-keyboard-controller
bunx expo prebuild --clean --platform android
bunx expo-brownfield build:android --repo MavenLocal -r --verboseNo source code changes — only a default create-expo-app SDK 55 project with two
packages installed and the expo-brownfield plugin auto-registered via expo install.
Resulting POM at app/node_modules/react-native-keyboard-controller/android/build/publications/brownfieldRelease/pom-default.xml
<dependency>
<groupId>com.facebook.react</groupId>
<artifactId>react-android</artifactId>
<version>+</version>
<scope>runtime</scope>
<version>0.83.6</version> <!-- appended by setVersion — bug -->
</dependency>> Task :react-native-keyboard-controller:publishBrownfieldReleasePublicationToMavenLocal FAILED
* What went wrong:
Execution failed for task ':react-native-keyboard-controller:publishBrownfieldReleasePublicationToMavenLocal'.
> Failed to publish publication 'brownfieldRelease' to repository 'mavenLocal'
> Invalid publication 'brownfieldRelease': POM file is invalid. Check any modifications you have made to the POM file.
packages/expo-brownfield/gradle-plugins/publish/src/main/kotlin/expo/modules/plugin/extensions.kt:107:
internal fun Node.setVersion(version: String) {
val versionNode = this.children().firstOrNull { it is Node && it.name() == "version" } as? Node
if (versionNode != null) {
versionNode.setValue(version)
} else {
this.appendNode("version", version)
}
}Node.name() returns a QName-like object that doesn't == a String, so
firstOrNull always returns null and appendNode runs every time — even when
a <version> element is already present in the dependency node.
Sibling helpers (groupId(), artifactId()) in the same file use the correct
get(...) / NodeList pattern.
internal fun Node.setVersion(version: String) {
val node = when (val v = this.get("version")) {
is Node -> v
is NodeList -> v.firstOrNull() as? Node
else -> null
}
if (node != null) node.setValue(version) else this.appendNode("version", version)
}- expo-brownfield 55.0.23 (also reproduces on 55.0.22)
- Expo SDK 55, react-native 0.83.6
- Gradle 9.0.1
- AGP from SDK 55 default
- macOS 26.4, Node 22.22.2