Skip to content

feat(rn): add react/react-native version-contract gate - #160

Merged
MusaMisto merged 1 commit into
mainfrom
fix/react-native-version-contract-gate
Aug 11, 2026
Merged

feat(rn): add react/react-native version-contract gate#160
MusaMisto merged 1 commit into
mainfrom
fix/react-native-version-contract-gate

Conversation

@MusaMisto

Copy link
Copy Markdown
Member

Why

react-native advertises a loose peer range while its bundled renderer hard-codes an exact React version and throws at runtime:

if("19.2.3"!==isomorphicReactPackageVersion)throwError('Incompatible React versions: ...')

react-native@0.85.1 declares peerDependencies: { react: "^19.2.3" } — so bumping react to 19.2.8 is a legal semver-patch update. mealivery-customer-mobile PR #89 auto-merged exactly that into develop on 2026-07-22 and broke the app.

On the New Architecture this is a deferred crash, not a launch crash. renderElement() correctly uses Fabric, but RendererImplementation.js binds six APIs to the Paper renderer unconditionally, regardless of newArchEnabledfindNodeHandle, unstable_batchedUpdates, sendAccessibilityEvent, findHostInstance_DEPRECATED, unmountComponentAtNodeAndRemoveContainer, isChildPublicInstance. ScrollView, FlatList, TextInput and any useNativeDriver animation reach that set — i.e. the first content screen.

Which gates miss it (verified by experiment, not assumed)

GateCatches it?Why
yarn install^19.2.3 genuinely permits 19.2.8
tsc --noEmit / eslinttypes and lint unaffected
jest + react-test-rendererrendering uses Fabric; Paper never loads
npx react-native bundleMetro builds a static graph — an unconditional throw injected into the renderer module body still bundled successfully, exit 0
rn-contract / checkcompares resolved react to the renderer's own literal

What this adds

  • .github/actions/check-react-native-contract — composite action + resolver
  • .github/workflows/react-native-contract-gate.yml — reusable workflow, produces rn-contract / check
  • workflow-templates/react-native-contract-check.yml — caller template
  • react-core group in dependabot-templates/react-native-mobile.yml

Design notes

  • Plain pull_request, not pull_request_target — needs the PR's own package.json/lockfile, needs no secrets, never installs or executes PR code. A yarn install step must never be added here.
  • Reads the lockfile first (it decides what actually installs); package.json is the fallback; ranges resolve via npm view.
  • Fails closed when the contract can't be determined. RN ≥ 0.86 deletes the Paper renderer, so it falls back to Fabric's reconcilerVersion before giving up.
  • Also fails on duplicate react in the tree.
  • Reads ~200KB of renderer source from the CDN, never the ~32MB tarball. Seconds on ubuntu-latest.

Verification

CaseResult
083147e — the real PR #89 breaking commitexit 1, DRIFT 19.2.8 vs 19.2.3
mealivery-customer-mobile develop todayexit 0, OK
RN 0.86.2 (Paper renderer deleted)exit 1, DRIFT via fabric-reconciler
duplicate react in yarn.lockexit 1, DUPLICATE_REACT
non-RN repo / caret RN rangeexit 0
fail-on-unknown: falsedowngrades to warning, exit 0

Important caveat

Grouping and exact-pinning are not the fix. Dependabot opens react-only PRs when no matching react-native release exists, and it rewrites exact pins — it did so twice in this repo. The gate is the fix, and it blocks nothing until rn-contract / check is marked required on the branch Dependabot targets, because auto-merge waits on required checks only.

🤖 Generated with Claude Code

react-native advertises a loose peer range while its bundled renderer
hard-codes an exact React version and throws at runtime:
if ("19.2.3" !== isomorphicReactPackageVersion) throw Error(...)
react-native@0.85.1 declares peerDependencies { react: "^19.2.3" }, so
bumping react to 19.2.8 is a legal semver-PATCH update. It installs, type-
checks, lints, passes jest and bundles with metro -- then throws on the
first real screen. mealivery-customer-mobile PR #89 auto-merged exactly
this combination into develop on 2026-07-22.
On the New Architecture it is a DEFERRED crash, not a launch crash:
renderElement() correctly uses Fabric, but RendererImplementation.js binds
six APIs to the Paper renderer unconditionally regardless of newArchEnabled
(findNodeHandle, unstable_batchedUpdates, sendAccessibilityEvent,
findHostInstance_DEPRECATED, unmountComponentAtNodeAndRemoveContainer,
isChildPublicInstance). ScrollView, FlatList, TextInput and any
useNativeDriver animation reach that set.
Verified by experiment which gates miss it: react-test-renderer render
passes under a simulated mismatch, and `npx react-native bundle` completes
successfully even with an unconditional throw injected into the renderer
module body (Metro builds a static graph and never evaluates it). tsc and
eslint are unaffected. Only a version-contract assertion or a real
native/E2E run catches this class.
Adds:
- .github/actions/check-react-native-contract (composite)
- .github/workflows/react-native-contract-gate.yml (reusable)
- workflow-templates/react-native-contract-check.yml (caller)
- react-core group in dependabot-templates/react-native-mobile.yml
Design notes:
- Plain `pull_request`, not pull_request_target: needs the PR's own
package.json/lockfile, needs no secrets, never installs or executes PR
code. A `yarn install` step must never be added here.
- Reads the lockfile first (it decides what installs), package.json only
as fallback; resolves ranges via npm view.
- Fails closed when the contract can't be determined. RN >= 0.86 removes
the Paper renderer, so it falls back to Fabric's reconcilerVersion
before giving up -- a gate that silently passes manufactures confidence.
- Also fails on duplicate react in the tree.
Verified against real repos and the real breaking commit:
083147e (PR #89) -> exit 1, DRIFT 19.2.8 vs 19.2.3
mealivery develop (current) -> exit 0, OK
RN 0.86.2 (no Paper renderer) -> exit 1, DRIFT via fabric-reconciler
duplicate react in yarn.lock -> exit 1, DUPLICATE_REACT
non-RN repo / caret RN range -> exit 0
fail-on-unknown=false -> downgrades to warning, exit 0
Grouping and exact-pinning are NOT the fix: Dependabot opens react-only PRs
when no matching react-native release exists, and it rewrites exact pins
(it did so twice in this repo). The gate is the fix, and it only blocks
once `rn-contract / check` is marked required on the Dependabot target
branch -- auto-merge waits on required checks only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary

Adds a React/React Native version-contract gate.

  • Resolves versions from lockfiles first, then package.json.
  • Checks React against the exact renderer version required by React Native.
  • Supports Paper and Fabric renderer contracts.
  • Detects duplicate React installations.
  • Fails closed when the contract is unknown or unresolved.
  • Adds a composite action, reusable workflow, workflow template, documentation, and react-core Dependabot group.
  • Runs on pull requests without installing or executing pull-request code.
  • Uses read-only repository access and requires no secrets.

Risk

risk:low

The change adds CI and dependency-management controls. It does not modify application runtime code. Resolver accuracy and external npm view availability can affect CI results.

Security-sensitive areas

  • The workflow checks out pull-request content without persisted credentials.
  • The workflow uses read-only contents permission.
  • The gate does not install or execute pull-request code.
  • Renderer metadata may be fetched from npm through npm view; network availability and registry responses affect resolution.

Test coverage impact

Verification covers:

  • The reported breaking commit.
  • The current application state.
  • React Native 0.86.2.
  • Duplicate React versions.
  • Non-React Native repositories.
  • Warning mode.
  • Unknown and unresolved contracts.

The resolver also supports offline operation.

Operational concerns

  • Mark the gate as a required check on the Dependabot target branch.
  • Dependabot grouping reduces, but does not eliminate, incompatible React updates.
  • The gate may fail closed when lockfiles, renderer metadata, or network resolution are unavailable.
  • Rollback requires reverting the workflow, action, template, and Dependabot changes.

Walkthrough

Changes

React Native contract gate

Layer / File(s)Summary
Version and renderer resolution
.github/actions/check-react-native-contract/resolve_versions.py
The CLI resolves React and React Native versions from manifests and lockfiles, discovers renderer expectations locally or from the CDN, and emits structured results.
Contract result evaluation
.github/actions/check-react-native-contract/resolve_versions.py
The CLI reports unresolved, duplicate, unknown, matching, and drift states.
Composite action status handling
.github/actions/check-react-native-contract/action.yml
The action invokes the resolver, publishes outputs, logs results, and handles each status.
Workflow and repository adoption
.github/workflows/react-native-contract-gate.yml, workflow-templates/react-native-contract-check.yml, workflow-templates/react-native-contract-check.properties.json, dependabot-templates/react-native-mobile.yml, README.md
Reusable workflow, template metadata, dependency grouping, and contract-check documentation were added.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels:security, infra, risk:high

Suggested reviewers:omarghatasheh

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description check✅ PassedThe description clearly explains the React and React Native contract gate and its operational behavior.
Title check✅ PassedThe title clearly summarizes the primary change: adding a React and React Native version-contract gate.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/actions/check-react-native-contract/action.yml:
- Around line 121-125: Update the FAIL_ON_UNKNOWN condition in the React Native
contract check so only the exact string "false" selects the warning path; all
other values, including empty or invalid inputs, must enter the blocking branch
and exit 1. Preserve the existing error and warning messages.
- Around line 86-97: Validate every parsed resolver field—status, reactVersion,
expectedReact, expectationSource, reason, and reactNativeVersion—immediately
after retrieval and before any GITHUB_OUTPUT writes or annotation emission;
reject values containing carriage returns or newlines and terminate with an
error. Ensure the validation covers all output and annotation paths in the
action.
In @.github/actions/check-react-native-contract/resolve_versions.py:
- Around line 125-133: Update the lockfile resolution flow around the loop over
yarn.lock and package-lock.json to determine the active package manager
explicitly rather than choosing by iteration order. When both lockfiles exist or
the active lockfile cannot be determined, return the existing unknown status;
otherwise parse only the selected lockfile and preserve the current found-result
behavior.
- Around line 240-248: Update the React version handling near rn_version and
react_version to resolve manifest-only React ranges through npm_resolve_range
when not offline, storing the resolved value in result["resolved"]["react"]. If
resolution fails, return an unknown status rather than comparing the unresolved
range and reporting DRIFT; preserve the existing behavior for already-resolved
React versions.
- Around line 165-175: Update the version-resolution flow around the
pattern.search call so a missing local renderer with allow_network disabled
returns CONTRACT_UNKNOWN instead of passing None to pattern.search. Preserve the
existing network lookup behavior when allow_network is enabled and continue
normal pattern matching when text is available.
In @.github/workflows/react-native-contract-gate.yml:
- Around line 48-53: Pin every remote uses reference in
.github/workflows/react-native-contract-gate.yml lines 48-53 and
workflow-templates/react-native-contract-check.yml line 41 to immutable commit
SHAs, replacing the mutable actions/checkout@v5 and
check-react-native-contract@main references while preserving the existing
actions and workflow behavior.
In `@workflow-templates/react-native-contract-check.yml`:
- Around line 32-34: Update the pull_request trigger in the workflow
configuration to run for every target branch by removing the branches filter
under on.pull_request. Preserve the existing rn-contract / check workflow
behavior while ensuring Dependabot updates targeting arbitrary branches receive
the gate.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: simplify9/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c94a0998-5fa3-4087-9404-f0173282b2a5

📥 Commits

Reviewing files that changed from the base of the PR and between 1e2ee61 and d04869f.

📒 Files selected for processing (7)
  • .github/actions/check-react-native-contract/action.yml
  • .github/actions/check-react-native-contract/resolve_versions.py
  • .github/workflows/react-native-contract-gate.yml
  • README.md
  • dependabot-templates/react-native-mobile.yml
  • workflow-templates/react-native-contract-check.properties.json
  • workflow-templates/react-native-contract-check.yml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
.github/workflows/**

⚙️ CodeRabbit configuration file

.github/workflows/**: Treat GitHub Actions changes as supply-chain sensitive.

Check for:

  • Overbroad permissions
  • Missing explicit permissions blocks
  • Unpinned third-party actions
  • Unsafe pull_request_target usage
  • Secret exposure
  • Shell injection risks
  • Untrusted input used in scripts
  • Dangerous artifact upload/download behavior
  • Missing least-privilege permissions

Files:

  • .github/workflows/react-native-contract-gate.yml
🪛 ast-grep (0.45.1)
.github/actions/check-react-native-contract/resolve_versions.py

[warning] 60-60: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 74-74: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 160-160: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(local, encoding="utf-8", errors="replace")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[info] 208-208: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"status": "NO_PACKAGE_JSON", "dir": root})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 213-213: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"status": "NOT_A_REACT_NATIVE_REPO"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 228-228: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 236-236: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 257-257: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 269-269: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[warning] 170-170: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen(req, timeout=60)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(urlopen-unsanitized-data)


[error] 186-189: Command coming from incoming request
Context: subprocess.run(
["npm", "view", "react-native@" + spec, "version", "--json"],
capture_output=True, text=True, timeout=120, check=False,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.16.1)
.github/actions/check-react-native-contract/resolve_versions.py

[warning] 111-111: Missing return type annotation for private function walk

Add return type annotation: None

(ANN202)


[warning] 142-142: Boolean default positional argument in function definition

(FBT002)


[error] 167-170: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 171-171: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 187-187: subprocess call: check for execution of untrusted input

(S603)


[error] 188-188: Starting a process with a partial executable path

(S607)


[warning] 255-256: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


[warning] 267-268: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)

🪛 zizmor (1.29.0)
.github/workflows/react-native-contract-gate.yml

[error] 48-48: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 53-53: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🔇 Additional comments (3)
workflow-templates/react-native-contract-check.properties.json (1)

1-10: LGTM!

dependabot-templates/react-native-mobile.yml (1)

19-41: LGTM!

README.md (1)

1205-1272: LGTM!

Comment on lines +86 to +97
status="$(get status)"
react="$(get reactVersion)"
expected="$(get expectedReact)"
source="$(get expectationSource)"
reason="$(get reason)"
rn="$(get reactNativeVersion)"

{
echo "status=${status}"
echo "react=${react}"
echo "expected-react=${expected}"
} >> "$GITHUB_OUTPUT"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -eu
file=".github/actions/check-react-native-contract/action.yml"printf'%s\n'"== target file ==" cat -n "$file"| sed -n '1,170p'printf'%s\n'"== related output/command handling ==" rg -n -C 3 'GITHUB_OUTPUT|::(error|warning|notice)|get status|get reactVersion|get expectedReact|get expectationSource|get reason|get reactNativeVersion' .github

Repository: simplify9/.github

Length of output: 50374


🏁 Script executed:

#!/bin/bashset -eu
printf'%s\n'"== resolver files =="
fd -i 'resolve_versions'.printf'%s\n'"== resolver outline =="
ast-grep outline .github/actions/check-react-native-contract/resolve_versions.py --view compact
printf'%s\n'"== resolver source =="
cat -n .github/actions/check-react-native-contract/resolve_versions.py | sed -n '1,280p'printf'%s\n'"== all consumers of this action =="
rg -n -C 5 'check-react-native-contract|expected-react|steps\.check\.(outputs|conclusion)|steps\.[A-Za-z0-9_-]+\.(outputs|conclusion)' .github/workflows .github/actions | head -n 300
printf'%s\n'"== local documentation on output and command encoding =="
rg -n -C 3 'GITHUB_OUTPUT|workflow command|control character|newline|heredoc delimiter' .github README.md 2>/dev/null | head -n 240

Repository: simplify9/.github

Length of output: 350


🏁 Script executed:

#!/bin/bashset -u
printf'%s\n'"== tracked resolver paths =="
git ls-files | grep -Ei '(^|/)resolve_versions(\.|$)|check-react-native-contract'printf'%s\n'"== resolver path metadata =="if [ -e".github/actions/check-react-native-contract/resolve_versions.py" ];then
wc -l ".github/actions/check-react-native-contract/resolve_versions.py"
ast-grep outline ".github/actions/check-react-native-contract/resolve_versions.py" --view signatures ||true
cat -n ".github/actions/check-react-native-contract/resolve_versions.py"| sed -n '1,320p'elseprintf'%s\n'"resolver path is absent"fiprintf'%s\n'"== action consumers =="
rg -n -C 4 --glob '*.yml' --glob '*.yaml''check-react-native-contract|expected-react' .github 2>/dev/null | head -n 300

Repository: simplify9/.github

Length of output: 16288


🏁 Script executed:

#!/bin/bashset -euo pipefail
python3 - <<'PY'import jsonimport subprocess# This is a valid JSON shape for the package-lock parser: the resolver only# checks that meta["version"] is truthy before returning it.payload = { "packages": { "": {}, "node_modules/react": {"version": "19.2.3\nforged=output"}, "node_modules/react-native": {"version": "0.85.1\r\n::error title=FORGED::annotation"}, }}result = json.dumps({ "status": "DRIFT", "reactVersion": payload["packages"]["node_modules/react"]["version"], "expectedReact": "19.2.3", "expectationSource": "paper-assertion", "reason": "react-native@0.85.1 requires react 19.2.3, but this repo resolves react " + payload["packages"]["node_modules/react"]["version"], "reactNativeVersion": payload["packages"]["node_modules/react-native"]["version"],})script = r'''set -uo pipefailresult="$1"get() { printf '%s' "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$1','') or '')"; }status="$(get status)"react="$(get reactVersion)"expected="$(get expectedReact)"source="$(get expectationSource)"reason="$(get reason)"rn="$(get reactNativeVersion)"printf '%s\n' '--- output-file bytes ---'{ echo "status=${status}" echo "react=${react}" echo "expected-react=${expected}"} | python3 -c 'import sys; print(repr(sys.stdin.buffer.read()))'printf '%s\n' '--- command-stream lines ---'{ echo "::notice title=OK::react ${react} matches what react-native ${rn} requires (via ${source})." echo "::error title=DRIFT::${reason}."} | python3 -c 'import sys; print(repr(sys.stdin.buffer.read().splitlines()))'printf '%s\n' '--- proposed guard result ---'case "$status$react$expected$source$reason$rn" in *$'\n'*|*$'\r'*) echo "REJECT" ;; *) echo "ACCEPT" ;;esac'''subprocess.run(["bash", "-c", script, "probe", result], check=True)PY

Repository: simplify9/.github

Length of output: 597


Reject CR/LF in resolver fields before output or annotation emission.

package-lock.json versions accept embedded control characters. These characters create extra GITHUB_OUTPUT records and can emit forged ::error or ::notice commands. Reject \r and \n in all parsed fields before writing outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/actions/check-react-native-contract/action.yml around lines 86 - 97,
Validate every parsed resolver field—status, reactVersion, expectedReact,
expectationSource, reason, and reactNativeVersion—immediately after retrieval
and before any GITHUB_OUTPUT writes or annotation emission; reject values
containing carriage returns or newlines and terminate with an error. Ensure the
validation covers all output and annotation paths in the action.

Comment on lines +121 to +125
if [[ "$FAIL_ON_UNKNOWN" == "true" ]]; then
echo "::error title=❌ [RN-CONTRACT] Could not verify the version contract::${msg}. Failing closed: this check cannot confirm the app is safe, and passing here would manufacture false confidence. Set fail-on-unknown: false to downgrade this to a warning."
exit 1
fi
echo "::warning title=⚠️ [RN-CONTRACT] Could not verify the version contract::${msg}. fail-on-unknown is false, so this is not blocking."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed unless the input is explicitly false.

Any value other than the exact string true, including a typo or an empty expression, takes the warning path. This disables the gate when the contract is unknown. Use [[ "$FAIL_ON_UNKNOWN" != "false" ]] for the blocking branch, or reject values other than true and false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/actions/check-react-native-contract/action.yml around lines 121 -
125, Update the FAIL_ON_UNKNOWN condition in the React Native contract check so
only the exact string "false" selects the warning path; all other values,
including empty or invalid inputs, must enter the blocking branch and exit 1.
Preserve the existing error and warning messages.

Comment on lines +125 to +133
for lockfile, parser in (
("yarn.lock", versions_from_yarn_lock),
("package-lock.json", versions_from_npm_lock),
):
path = os.path.join(root, lockfile)
if os.path.exists(path):
found = parser(path, name)
if found:
return found, lockfile

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail closed when multiple lockfiles exist.

Lines 125-133 always select yarn.lock before package-lock.json. A repository that contains both files can resolve a different dependency tree with npm. The gate can then report OK or DRIFT for the wrong installation.

Select the active package manager explicitly. If the active lockfile cannot be determined, return an unknown status instead of selecting one by order. This preserves the fail-closed contract for the required workflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/actions/check-react-native-contract/resolve_versions.py around lines
125 - 133, Update the lockfile resolution flow around the loop over yarn.lock
and package-lock.json to determine the active package manager explicitly rather
than choosing by iteration order. When both lockfiles exist or the active
lockfile cannot be determined, return the existing unknown status; otherwise
parse only the selected lockfile and preserve the current found-result behavior.

Comment on lines +165 to +175
if text is None and allow_network:
try:
req = urllib.request.Request(
UNPKG.format(ver=rn_version, impl=impl),
headers={"User-Agent": "simplify9-rn-contract-gate"},
)
with urllib.request.urlopen(req, timeout=60) as resp:
text = resp.read().decode("utf-8", errors="replace")
except (urllib.error.URLError, urllib.error.HTTPError, OSError):
continue
m = pattern.search(text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle an unavailable local renderer in offline mode.

If --offline is set and the local renderer file does not exist, text remains None. Line 175 then calls pattern.search(text), which raises TypeError. The documented offline mode must return CONTRACT_UNKNOWN so the caller can apply fail-on-unknown.

Proposed fix
 if text is None and allow_network:
try:
req = urllib.request.Request(
UNPKG.format(ver=rn_version, impl=impl),
headers={"User-Agent": "simplify9-rn-contract-gate"},
@@
except (urllib.error.URLError, urllib.error.HTTPError, OSError):
continue
+ if text is None:+ continue
m = pattern.search(text)
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
iftextisNoneandallow_network:
try:
req=urllib.request.Request(
UNPKG.format(ver=rn_version, impl=impl),
headers={"User-Agent": "simplify9-rn-contract-gate"},
)
withurllib.request.urlopen(req, timeout=60) asresp:
text=resp.read().decode("utf-8", errors="replace")
except (urllib.error.URLError, urllib.error.HTTPError, OSError):
continue
m=pattern.search(text)
iftextisNoneandallow_network:
try:
req=urllib.request.Request(
UNPKG.format(ver=rn_version, impl=impl),
headers={"User-Agent": "simplify9-rn-contract-gate"},
)
withurllib.request.urlopen(req, timeout=60) asresp:
text=resp.read().decode("utf-8", errors="replace")
except (urllib.error.URLError, urllib.error.HTTPError, OSError):
continue
iftextisNone:
continue
m=pattern.search(text)
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 170-170: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen(req, timeout=60)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(urlopen-unsanitized-data)

🪛 Ruff (0.16.1)

[error] 167-170: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 171-171: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/actions/check-react-native-contract/resolve_versions.py around lines
165 - 175, Update the version-resolution flow around the pattern.search call so
a missing local renderer with allow_network disabled returns CONTRACT_UNKNOWN
instead of passing None to pattern.search. Preserve the existing network lookup
behavior when allow_network is enabled and continue normal pattern matching when
text is available.

Comment on lines +240 to +248
rn_version = rn_versions[0]
if not re.fullmatch(r"\d+\.\d+\.\d+.*", rn_version) and not args.offline:
resolved = npm_resolve_range(rn_version)
if resolved:
rn_version = resolved
result["resolved"]["react-native"] = [resolved]

react_version = react_versions[0]
result["reactVersion"] = react_version

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve the React range before comparing versions.

Lines 240-245 resolve a manifest-only react-native range, but Line 247 leaves a manifest-only React range unchanged. For example, react: "^19.2.3" is compared directly with "19.2.3" and always produces DRIFT. The react output is also not a resolved version.

Resolve react_version through npm view when react_src is package.json (unresolved range). If resolution fails, return an unknown status instead of DRIFT.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/actions/check-react-native-contract/resolve_versions.py around lines
240 - 248, Update the React version handling near rn_version and react_version
to resolve manifest-only React ranges through npm_resolve_range when not
offline, storing the resolved value in result["resolved"]["react"]. If
resolution fails, return an unknown status rather than comparing the unresolved
range and reporting DRIFT; preserve the existing behavior for already-resolved
React versions.

Comment on lines +48 to +53
uses: actions/checkout@v5
with:
persist-credentials: false

- name: Check react / react-native version contract
uses: simplify9/.github/.github/actions/check-react-native-contract@main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
rg -n '^\s*uses:\s+.+@(main|v[0-9]+)$' \
.github/workflows/react-native-contract-gate.yml \
workflow-templates/react-native-contract-check.yml

Repository: simplify9/.github

Length of output: 510


🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'%s\n''--- workflow references ---'
rg -n '^\s*uses:' \
.github/workflows/react-native-contract-gate.yml \
workflow-templates/react-native-contract-check.yml
printf'%s\n''--- relevant workflow contents ---'
sed -n '1,90p' .github/workflows/react-native-contract-gate.yml
sed -n '1,70p' workflow-templates/react-native-contract-check.yml

Repository: simplify9/.github

Length of output: 4544


Pin all remote uses: references to immutable commit SHAs.

Replace the @v5 and @main references in .github/workflows/react-native-contract-gate.yml and workflow-templates/react-native-contract-check.yml. Mutable references can change pull-request execution without a reviewed change.

🧰 Tools
🪛 zizmor (1.29.0)

[error] 48-48: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 53-53: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

📍 Affects 2 files
  • .github/workflows/react-native-contract-gate.yml#L48-L53 (this comment)
  • workflow-templates/react-native-contract-check.yml#L41-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/react-native-contract-gate.yml around lines 48 - 53, Pin
every remote uses reference in .github/workflows/react-native-contract-gate.yml
lines 48-53 and workflow-templates/react-native-contract-check.yml line 41 to
immutable commit SHAs, replacing the mutable actions/checkout@v5 and
check-react-native-contract@main references while preserving the existing
actions and workflow behavior.

Sources: Path instructions, Linters/SAST tools

Comment on lines +32 to +34
on:
pull_request:
branches: [main, develop]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Run the gate for every pull-request target branch.

dependabot-templates/react-native-mobile.yml accepts an arbitrary {{TARGET_BRANCH}}, but this template only runs for main and develop. If Dependabot targets another branch, rn-contract / check is absent and cannot block an incompatible update. Remove the branches filter, or make the target branch configurable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@workflow-templates/react-native-contract-check.yml` around lines 32 - 34,
Update the pull_request trigger in the workflow configuration to run for every
target branch by removing the branches filter under on.pull_request. Preserve
the existing rn-contract / check workflow behavior while ensuring Dependabot
updates targeting arbitrary branches receive the gate.

@MusaMistoMusaMisto self-assigned this Aug 11, 2026
@MusaMistoMusaMisto added bug Something isn't working documentation Improvements or additions to documentation labels Aug 11, 2026
@MusaMisto
MusaMisto merged commit a0d06ac into mainAug 11, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MusaMisto