Uh oh!
There was an error while loading. Please reload this page.
feat(rn): add react/react-native version-contract gate - #160
Conversation
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>📝 WalkthroughSummaryAdds a React/React Native version-contract gate.
Riskrisk:low The change adds CI and dependency-management controls. It does not modify application runtime code. Resolver accuracy and external Security-sensitive areas
Test coverage impactVerification covers:
The resolver also supports offline operation. Operational concerns
WalkthroughChangesReact Native contract gate
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
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
📒 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.ymlREADME.mddependabot-templates/react-native-mobile.ymlworkflow-templates/react-native-contract-check.properties.jsonworkflow-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!
| 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" |
There was a problem hiding this comment.
🔒 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' .githubRepository: 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 240Repository: 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 300Repository: 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)PYRepository: 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.
| 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." |
There was a problem hiding this comment.
🔒 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.
| 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 |
There was a problem hiding this comment.
🎯 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.
| 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) |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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 |
There was a problem hiding this comment.
🎯 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.
| 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 |
There was a problem hiding this comment.
🔒 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.ymlRepository: 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.ymlRepository: 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
| on: | ||
| pull_request: | ||
| branches: [main, develop] |
There was a problem hiding this comment.
🎯 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.
Uh oh!
There was an error while loading. Please reload this page.
Why
react-nativeadvertises a loose peer range while its bundled renderer hard-codes an exact React version and throws at runtime:react-native@0.85.1declarespeerDependencies: { react: "^19.2.3" }— so bumpingreactto19.2.8is a legal semver-patch update.mealivery-customer-mobilePR #89 auto-merged exactly that intodevelopon 2026-07-22 and broke the app.On the New Architecture this is a deferred crash, not a launch crash.
renderElement()correctly uses Fabric, butRendererImplementation.jsbinds six APIs to the Paper renderer unconditionally, regardless ofnewArchEnabled—findNodeHandle,unstable_batchedUpdates,sendAccessibilityEvent,findHostInstance_DEPRECATED,unmountComponentAtNodeAndRemoveContainer,isChildPublicInstance.ScrollView,FlatList,TextInputand anyuseNativeDriveranimation reach that set — i.e. the first content screen.Which gates miss it (verified by experiment, not assumed)
yarn install^19.2.3genuinely permits19.2.8tsc --noEmit/eslintjest+react-test-renderernpx react-native bundlethrowinjected into the renderer module body still bundled successfully, exit 0rn-contract / checkreactto the renderer's own literalWhat this adds
.github/actions/check-react-native-contract— composite action + resolver.github/workflows/react-native-contract-gate.yml— reusable workflow, producesrn-contract / checkworkflow-templates/react-native-contract-check.yml— caller templatereact-coregroup independabot-templates/react-native-mobile.ymlDesign notes
pull_request, notpull_request_target— needs the PR's ownpackage.json/lockfile, needs no secrets, never installs or executes PR code. Ayarn installstep must never be added here.package.jsonis the fallback; ranges resolve vianpm view.reconcilerVersionbefore giving up.reactin the tree.ubuntu-latest.Verification
083147e— the real PR #89 breaking commitDRIFT19.2.8 vs 19.2.3mealivery-customer-mobiledevelop todayOKDRIFTviafabric-reconcileryarn.lockDUPLICATE_REACTfail-on-unknown: falseImportant 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 / checkis marked required on the branch Dependabot targets, because auto-merge waits on required checks only.🤖 Generated with Claude Code