Skip to content

About

Can I use this react-native library?

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

rn.green

rn.green shows whether a React Native library works on each React Native version. A cell is compatible only when the library's demo app builds on iOS and Android and its test suite passes, if it has one. Patch versions are folded into minor lines (2.1.3 counts as 2.1.x).

The site is static, built with Astro. No cell has a result until the first Codemagic run.

Commands

Requires Node 22.12 or newer.

Command What it does
npm install Install dependencies
npm run dev Start the dev server at http://localhost:4321
npm run build Validate the data and build the site to dist/
npm run preview Serve the built site
npm run check Type check (astro check)
npm test Run the unit tests (vitest)

Data format

Two kinds of file. The build fails, naming the file and field, if either is invalid.

  • green-packages.toml, the catalog: which packages are covered, which versions are checked, and how each package is tested and built. We write it by hand, new releases included for now.
  • compatibility-data/<package name>/compatibility.json: the results. They arrive through the pull requests that compatibility-check opens; nobody edits them by hand.

The catalog: green-packages.toml

schema = 1

[react-native]
versions = ["0.88.0-rc.3", "0.87.1", "0.86.3"]

[[package]]
name = "react-native-screenshot-aware"
repository = "https://github.com/huextrat/react-native-screenshot-aware"
description = "React Native module for real-time screenshot detection on Android and iOS"
license = "MIT"
versions = ["2.1.3", "2.0.0", "1.3.21"]

[package.source]
tag = "v{version}"
dir = "."

[package.setup]
install = ["."]
prepare = [{ dir = ".", run = ["yarn", "prepare"] }]

[package.test]
dir = "."
run = ["yarn", "test"]

[package.demo]
dir = "example"
  • [react-native] versions are the table columns. Each version is exact and stands for its minor line, one version per line. A version with a prerelease tag (-rc.3) is shown as a release candidate.
  • Each [[package]] gets a page at /<name>/ and a badge at /badge/<name>.svg. name follows npm's rules and must be unique. repository is an https:// link. description and license are optional.
  • versions are the library versions that are checked, one per minor line, newest first by convention.
  • enabled = false keeps a package on the site, and the workflow refuses to check it. It is optional; the default is true.
  • Unknown keys are an error at every level, so a misspelt field fails the build.

The source, setup, test and demo tables are the package's settings: how a check builds and tests it. A package without any of them is listed on the site and never checked. Once one is present, test and demo are both required, so a forgotten test can never turn "tests not run" into "compatible".

Setting Default Meaning
source.tag v{version} Release tag. {version} is the only placeholder (@react-navigation/core@{version})
source.dir . Where the package sits in its repository (packages/react-native-reanimated)
setup.install ["."] Folders to install in, in order, each with the package manager it declares. Some demo apps install separately ([".", "example"])
setup.prepare [] Commands run before the tests and before each native build, such as the package's own build: [{ dir = ".", run = ["yarn", "prepare"] }]
setup.env {} Extra environment for every library command ({ RNS_GAMMA_ENABLED = "1" })
setup.node the workflow's (22) Node version for library commands ("24", "24.9"). When the workflow's Node does not match, the check downloads the newest matching release from nodejs.org, checks it against the release's SHASUMS256.txt, and puts it first in PATH for library commands only
test.run required The test command as an argument list, or "none" when the library has no test suite
test.dir Where to run the test command. Required with a command, not allowed with "none"
demo.dir required The demo app that is built (apps/fabric-example). Not .
demo.ios-scheme found from the workspace Only when the app's scheme is not named after its workspace or project (Debug FabricExample, ReactTestApp)

Commands are argument lists run without a shell. Every folder must stay inside the repository.

The results: compatibility.json

{
  "schemaVersion": 2,
  "package": "react-native-screenshot-aware",
  "results": {
    "2.1": {
      "0.87": {
        "status": "compatible",
        "tested": { "library": "2.1.3", "reactNative": "0.87.1" },
        "checks": { "buildIos": "passed", "buildAndroid": "passed", "tests": "passed" },
        "buildUrl": "https://codemagic.io/app/<app>/build/<build>",
        "testedAt": "2026-09-06T02:14:09Z"
      }
    }
  }
}
  • Outer keys are library lines, inner keys React Native lines, without leading zeros (0.80, not 0.080).
  • package must be a package of the catalog and match the folder the file is in.
  • The file is optional. A package without one has no results; its first pull request creates it.
  • A missing cell means the combination has not been tested yet ("Untested" on the site).
  • tested holds the exact versions tested: tested.library is on the outer line and tested.reactNative on the inner line.
  • buildIos and buildAndroid are passed or failed. tests is passed, failed or none, which means the library has no test suite and is judged on its builds alone.
  • status must be compatible exactly when both builds are passed and tests is passed or none.
  • buildUrl (optional) links the cell to its Codemagic build and must start with https://codemagic.io/.
  • Results on a line the catalog no longer lists (either axis) are valid and not shown. They are dropped the next time a result for that package is written. So retiring a React Native column or a library line is a one-line edit of the catalog.

Adding a package

  1. Add a [[package]] entry to green-packages.toml with its versions and settings, and enabled = false. The site lists it straight away; nothing is checked yet.
  2. Try the settings in a local run that opens no pull request (see "Running a check locally"), for one of its versions on one React Native version. Read the logs and adjust the settings until the setup works. A red build is fine; a setup that fails is not.
  3. Remove enabled = false, then start its checks in Codemagic, one per cell. Each one opens or updates the package's pull request.

Compatibility workflow

codemagic.yaml defines one workflow, compatibility-check. Its scripts are in scripts/compat/: plain Node, no build step, with the logic in scripts/compat/lib/ and its tests beside it.

A person starts it by hand, for one cell: one library version on one React Native version. Its first step, "Check the token", checks that GITHUB_TOKEN may push to the repository, before anything is installed, so a token that cannot fails the build in seconds. It then checks the library out at its release tag, points the library's own demo app at that React Native version, installs, runs the library's test suite, builds the demo app for Android and for iOS, and saves the three outcomes as result.json. It follows the package's settings in the catalog. Then it sends the result to this repository as a pull request. Every run opens or updates the package's pull request; there is no run that checks without sending the result.

One pull request per package. Results for a package collect on the branch compat/<package> (compat/@react-navigation/core; a character git does not allow in a branch name is written as % and its hex code). The branch is always the base branch plus exactly one commit, holding every result of that package that the base branch does not have yet. Each new result rebuilds that commit from the base branch and pushes it with a lease on the tip it read, so two builds finishing together never overwrite each other (the later one starts again), and the pull request never conflicts with the base branch. Its title counts the results (compat: react-native-screens: 3 results) and its description lists them, written again from the data each time.

  • Review the pull request and merge it (squash is fine). The next result for that package opens a new one with only the new cells.
  • If you close a pull request without merging, delete its branch too. Otherwise the next result for that package carries the closed cells forward.
  • A result is only written over an older one: a cell the base branch already has with a newer result is left alone.

Which repository. The pull request always goes to codemagic-ci-cd/reactnative.green, fixed as REPOSITORY in scripts/compat/lib/github.mjs. A fork or a rename changes it there.

When the push fails. Before preparing the commit, the step checks with git that the token may write: a dry run of pushing the base commit to the package's branch, which reaches the repository's receive-pack and changes nothing. The push itself is then told apart by git's answer:

  • The branch moved since it was read (the lease no longer matches): the step starts again from the new tip, up to five times.
  • GitHub refused the token (401 or 403, "Permission … denied", authentication failed, repository not found), at the check or at the push: the step fails at once, naming the repository and the account GitHub reported, with the likely causes: the token's resource owner is a personal account and not the organization; the organization has not approved the token; the repository is not among the token's selected repositories; Contents is not read and write. (For a classic token: it lacks the public_repo or repo scope.)
  • Anything else, such as a branch rule: the step fails at once with git's own message.

A run whose setup fails (bad inputs, no such tag, the swap, the Node download, the install) leaves no result and no pull request.

New releases

Detection of new releases is not active. Versions in green-packages.toml are edited by hand for now. The code for detecting them (scripts/compat/watch.mjs and its libraries) is kept in the repository for later; no workflow runs it.

Credentials

One variable group, default, with one variable, GITHUB_TOKEN: a fine-grained GitHub token for this repository only, with Contents and Pull requests set to read and write. Its resource owner must be the organization that owns the repository, not a personal account, and the organization must approve it. The last step uses it to push the package's branch and to open or update its pull request. Mark the value Secret and give the token an expiry date.

How the token is handled:

  • git gets it through a credential helper that reads the variable, so it never appears in a remote URL, in .git/config or in a log. Those git commands ignore the machine's git configuration and run no hooks.
  • The GitHub API gets it only at https://api.github.com, in the Authorization header. Redirects to any other host are not followed.
  • Every command the scripts start for the library (installs, tests, Gradle, CocoaPods, xcodebuild) runs without it in its environment.

The remaining risk. The same build runs the library's own code: its install scripts, tests and build scripts. Keeping the token out of their environment is not isolation. On a build machine a process can read the environment of the processes that started it (ps eww on macOS), and code that runs in an early step can change what a later step runs, including the script that uses the token. So a hostile or compromised library, or one of its dependencies, could take the token and push to this repository or open pull requests with it. What limits the damage:

  • Protect main so that it only changes through a pull request with an approving review, and so that nobody pushes to it directly. Then the token can at most push other branches and open pull requests, which a person reviews before anything reaches the site.
  • Keep the token limited to this repository, with only the two permissions above.
  • Give it an expiry date, and replace it if a check ever misbehaves.

Setting up Codemagic

  1. Add the app to Codemagic.
  2. In the app's settings, create the variable group default with GITHUB_TOKEN (see "Credentials"), and mark the value Secret.
  3. Start compatibility-check by hand for one cell (see "Starting one check by hand"). The first build already opens a pull request for the package. Read the build's logs. If its first step, "Check the token", fails, fix the token as its message says before trying anything else.
  4. Review the pull request and merge it.

There is no schedule, no Codemagic API token and no webhook.

Starting one check by hand

In the Codemagic UI, start compatibility-check and fill in package, library_version and react_native_version. The run opens or updates the package's pull request.

Inputs are checked before anything runs, and again against the base branch's catalog before the result is sent. The package must be in the catalog with settings and not disabled, the versions must be exact, and both must be on lines the catalog lists.

How React Native is swapped in

The library's own demo app is what gets built, so a red cell can be caused by the demo app rather than the library. The swap changes as little as the target needs, in the repository's root package.json, the package's own, the demo app's and every other workspace package's (the folders the root's workspaces names, one-level globs included), so the workspace resolves one React Native version and one React. A sibling package left on another react or react-test-renderer installs a second copy of React, and the tests fail with null hooks. The build log (swap.txt) says which case applied and lists every change.

  • Same version. When the demo app already uses the version under test, no package version changes: the check runs the library's own setup (apart from the Gradle wrapper, below).
  • Same line, other patch. react-native and the @react-native/* packages pinned in step with it move to the target; nothing else.
  • Another line.
    • react-native becomes the version under test.
    • react, react-test-renderer and the @react-native-community/cli packages take the versions from the React Native app template for that line. A line without a template (a very new release candidate) takes react from React Native's own peer range and leaves the others. react-dom takes whatever react is set to: React DOM refuses a react of another version.
    • Every @react-native/* package takes the same version as React Native, or the newest release or release candidate on that line when that exact version was never published. Nightlies are never used.
    • Tests use @react-native/jest-preset when it exists for the line, otherwise React Native's built-in react-native preset (only a jest.preset in package.json is adjusted).
    • Expo demo apps move to the Expo SDK that bundles the target line, with every Expo-managed module at the version that SDK bundles, unless the demo app is already on that SDK, in which case its Expo versions are kept. Bare demo apps keep their Expo packages. When no SDK bundles the line (Expo skips lines: 0.82, 0.84, 0.87), the swap stops as a setup failure and the cell gets no result: an SDK made for another line fails before the library is reached, because its Gradle plugin and its reanimated refuse the React Native version, and that would be recorded as the library's incompatibility. When that moves react-native-reanimated to 4 (or the library already pins 4) and the manifest has no react-native-worklets, the worklets package reanimated 4 needs is added at the version the SDK bundles: a library written for reanimated 3 has no such dependency, and its tests fail to load reanimated without it.
    • Nothing else changes, and nothing is added apart from the jest preset and react-native-worklets.

Lockfiles are regenerated. Each check then runs the package's setup.prepare commands; each platform build runs expo prebuild for Expo demo apps, and iOS runs pod install and builds for the simulator without code signing, while Android builds one architecture in debug. All of that counts as the platform's build.

What the harness changes in a demo app

A failed build may come from the demo app and not from the library; the checks panel shows which check failed, and the build's swap.txt lists every change below that was made. Compared with what the library published at its tag:

  1. Package versions: React Native and what moves with it, by the three cases above.
  2. Jest preset: across lines only. A @react-native/jest-preset dependency moves to the target line's version like the other @react-native/* packages: a preset for 0.87 mocks react-native/setup-env, which 0.86 does not have, so every suite fails to start. When package.json sets jest.preset, the preset is also switched, and the dependency added or removed, as @react-native/jest-preset exists or stops existing for the target line. A preset set in jest.config.js keeps the dependency it names.
  3. Expo SDK: across lines only, and only when the closest SDK is not the one the demo app uses. react-native-worklets is added where reanimated 4 needs it (above).
  4. Gradle wrapper: always set to the Gradle version of the React Native app template for the target line, because the React Native version dictates it and a demo app's committed wrapper can be too old even for its own React Native version. For Expo demo apps it is set right after expo prebuild generates it. A line without a template keeps the demo app's wrapper.
  5. Android SDK: compileSdkVersion, targetSdkVersion and buildToolsVersion in a bare demo app's root android/build.gradle are always set to the template's for the target line, for the same reason: the line's Android Gradle plugin can only find the platforms it knows, and a demo app written for a newer line asks for a newer one (compileSdkVersion 37 on a plugin tested up to 36 fails with "Failed to find target with hash string 'android-37'" before anything compiles). In the same file, kotlinVersion is raised to the template's when the demo app's is older, so the demo app compiles its Kotlin with the version the line was built with, and never lowered: a newer pin reads the older line fine and may be what the library's own Kotlin code needs. Only values written as literals are changed; minSdkVersion is the library's requirement and is left, as is ndkVersion. An Expo demo app's build.gradle is generated by expo prebuild from its SDK, which already matches the line, so it is left alone. A line without a template keeps the demo app's values.
  6. Android Gradle plugin opt-outs: android.builtInKotlin and android.newDsl are added to a bare demo app's android/gradle.properties when the template for the target line sets them and the demo app does not. AGP 9, which React Native 0.87 moved to, compiles Kotlin itself by default, and an app that also applies the Kotlin plugin, as every template up to then did, fails with "Failed to apply plugin 'org.jetbrains.kotlin.android'. Cannot add extension with name 'kotlin'"; the 0.87 template keeps the plugin and opts out with android.builtInKotlin=false. A demo app written for 0.86 has the plugin but not the opt-out. A value the demo app sets itself, either way, is kept, and a demo app written for 0.87 keeps its opt-outs on 0.86, where the template does not set them. An Expo demo app's gradle.properties is generated by expo prebuild and left alone.
  7. Default ProGuard file: getDefaultProguardFile("proguard-android.txt") in a bare demo app's android/app/build.gradle is changed to proguard-android-optimize.txt when the template for the target line names that file. AGP 9 refuses the legacy file even with minification off ("getDefaultProguardFile('proguard-android.txt') is no longer supported since it includes -dontoptimize"); every template up to 0.86 named it, and the 0.87 template names the optimize one, which earlier plugins accept too. It is never changed back.
  8. ios/.xcode.env.local: removed from bare demo apps that commit it. It is per machine by design and points Xcode at its author's own Node.
  9. The library's own build: the setup.prepare commands run before the tests and before each native build, because the demo app consumes the compiled library.
  10. The library itself: when the demo app names the library under test with a registry version or range, that dependency is pointed at the checked-out package (source.dir) with a relative path: link: for yarn and pnpm, file: for npm. Otherwise the demo app would build the version published on npm, not the tagged code. A dependency that is already workspace:, link:, file:, portal: or a path is left alone. This applies in all three cases, "same version" included.
  11. CocoaPods. When the swap changed the React Native version, Podfile.lock and Pods are removed first. They pin the pods from the version the demo app committed, and pod install will not move them. A same-version check leaves them. The demo app's own way comes first: with a Gemfile, bundle install and bundle exec pod install, otherwise plain pod install. If that fails (typically because the repository pins a Ruby version the build machine does not have, in .ruby-version), pod install is tried once more with the machine's own Ruby and CocoaPods, without the Gemfile and without the pin. The machine's own setup is looked up from outside the repository, where no pin applies, and called by its real path, so it does not depend on which Ruby version manager the machine uses: the default Ruby if it has CocoaPods, otherwise the first pod on PATH that works there (such as Homebrew's). That fallback sets COCOAPODS_NO_BUNDLER, because the CocoaPods gem's pod script otherwise looks for a Gemfile inside the gem and refuses to start. The iOS log says which one was used and why the first failed, and checks/buildIos.json records it ("cocoapods": "demo app", "machine" or "none"). If both fail, the iOS build is recorded as failed.

Nothing else in the demo app is touched.

Network fetches inside a check are retried so a download failure is not recorded as the library's failure: the Gradle wrapper's download of Gradle and pod install, bundle install and expo prebuild get three tries, and Gradle retries its own dependency downloads. Network access inside xcodebuild and inside a library's own scripts is not retried.

What is and is not recorded

  • buildIos and buildAndroid record passed or failed. tests records passed or failed, or none when the catalog says the library has no test suite (test.run = "none"). A failing check never stops the others, so the page can show which one failed.
  • If the setup fails (bad inputs, no such tag, the swap, the Node download, the install), the check fails and leaves no result: the cell keeps its previous one. An Expo demo app on a React Native line that no Expo SDK bundles is such a failure, so those cells stay "not tested yet".
  • The pull request step takes the package and both versions from the build's inputs, checked again, and from result.json only the three outcomes, which must be exactly passed or failed (or none for tests, and only where the catalog says so). A small, strictly shaped file is required.
  • Each cell links to its Codemagic build and records when the result was sent.
  • The results file is validated with the site's own rules before it is committed; a rejected push is retried on a fresh copy of the base branch, up to five times.
  • Logs, swap.json (which also records the Node version library commands used) and the per-check outcomes are kept as build artifacts next to result.json.

Running a check locally

You need Node 22, corepack, the Android SDK, Java 17, Xcode and CocoaPods. From the repository, with the work and output folders somewhere outside it:

export COMPAT_WORK_DIR=/tmp/rn-green-work COMPAT_OUT_DIR=/tmp/rn-green-out
export COMPAT_PACKAGE=react-native-screenshot-aware COMPAT_LIBRARY_VERSION=2.1.3
export COMPAT_REACT_NATIVE_VERSION=0.87.1 COMPAT_RECORD=false
node scripts/compat/validate-inputs.mjs && node scripts/compat/checkout.mjs &&
  node scripts/compat/swap.mjs && node scripts/compat/install.mjs &&
  node scripts/compat/check.mjs tests && node scripts/compat/check.mjs android &&
  node scripts/compat/check.mjs ios && node scripts/compat/compose-result.mjs

COMPAT_RECORD exists only for local runs; the workflow does not have it. It is on by default, with the workflow's rules. COMPAT_RECORD=false, as above, checks without a pull request: any exact versions and a disabled package are accepted, which is how draft settings are tried, and open-pr.mjs does nothing.

The outcomes are in $COMPAT_OUT_DIR/result.json and the logs in $COMPAT_OUT_DIR/logs/. With COMPAT_RECORD on and GITHUB_TOKEN set, the same command line can end with node --experimental-strip-types scripts/compat/open-pr.mjs, which pushes the branch and opens the pull request on GitHub like the workflow does. COMPAT_REMOTE_URL sends the branch to another git remote instead; the pull request calls still go to GitHub.

node --experimental-strip-types scripts/compat/validate-data.mjs checks green-packages.toml and compatibility-data/ with the site's rules.

About

Can I use this react-native library?

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages