Skip to content

Nothing in CI compiles native code, so the mobile app can break without any check failing #118

Description

@toruiwasa

Raised while fixing #117.

Field Contents
Goal A change that breaks the mobile native build fails a check, rather than being discovered by hand months later.
Context #117 was a one-line compile error that made the iOS build impossible, and it sat in main undetected because no job in CI compiles Objective-C++ or Java. The pipeline runs jest, tsc and eslint; all three passed throughout. The bug was found only because a native build was run manually for the first time. The same blind spot covers every native module the app links — react-native-mmkv, react-native-nitro-modules, expo-secure-store — and it is adjacent to #96, which notes that nothing exercises Metro's package-exports resolution either. Both are the same shape: the checks that pass are not the checks that matter for a React Native app.
Scope Decide what to run and how often. Open decisions, all genuine trade-offs rather than ambiguity: (a) a full expo run:ios on a macOS runner is the strongest signal but the slowest and most expensive — plausibly nightly or on-demand rather than per-PR; (b) expo prebuild + pod install alone catches dependency-resolution breaks like #117 without a full compile, and is much cheaper; (c) npx expo-doctor and expo install --check catch SDK version drift in seconds and could run on every PR — measurably not drop-in; see the 2026-09-04 correction below. These are not exclusive — the likely answer is (c) per-PR and one of (a)/(b) on a schedule. Plus the added scope in the correction below: a Dependabot ignore guard for the pair #119 pinned. The ignore guard was split out and delivered as #124 / PR #125 (85eaedf) — see the 2026-09-05 correction. This issue is now scoped to the CI check alone.
Test boundary CI configuration only; no application code. Whatever runs must fail on the #117 diff — that is the concrete acceptance test, and it is reproducible by reverting the overrides block.
Done when Reverting the pin in pnpm-workspace.yaml turns a check red, and the mobile pipeline reports SDK version drift without anyone running a command locally.
Branch feat/ci-mobile-native-check
Dependency #117 should merge first, so the pipeline is built against a tree that actually compiles.
Risk MEDIUM — a macOS runner is billed at a higher rate than Linux and a full native build is slow, so the cost of getting the frequency wrong is real. Starting with the cheap checks and adding the expensive one on a schedule bounds that.

Correction — 2026-09-04 (measured while reviewing #119)

Original assumption, Scope (c): expo install --check / expo-doctor are a seconds-cheap, drop-in per-PR gate reading the local bundledNativeModules.json.

Three measurements on fix/mobile-worklets-version-pin contradict that. All three change the trade-off this issue exists to decide, so they are recorded before the decision is made rather than discovered during implementation.

1. It already exits 1 on the current tree. Eight packages are behind what SDK 57 expects:

expo@57.0.15 → ~57.0.19          expo-secure-store@57.0.2 → ~57.0.3
expo-constants@57.0.13 → ~57.0.17  react-native@0.86.2 → 0.86.3
expo-linking@57.0.7 → ~57.0.9      eslint-config-expo@57.0.1 → ~57.0.2
expo-router@57.0.15 → ~57.0.18     jest-expo@57.0.4 → ~57.0.5

So (c) is not drop-in: it needs an expo install --fix catch-up commit first, or the gate is red from its first run and gets routed around. react-native-worklets and react-native-reanimated are not in this list — the #119 pin matches what Expo expects.

2. It reaches the network. It writes ~/.expo/native-modules-cache and queries the Expo API for the SDK version map — it reported expo@57.0.15 → ~57.0.19, a version newer than the installed expo package's own bundledNativeModules.json contains. The gate is therefore not hermetic and can fail on Expo API availability, which matters for a per-PR check.

3. It validates installed versions, not declared ranges. With apps/mobile/package.json set to react-native-reanimated@4.6.0 while node_modules held 4.5.1 (forced by the overrides block), --check reported nothing about reanimated.

What (3) buys, and what it does not. It does not catch a Dependabot bump of the pinned pair — hence the added scope below. It does catch the inverse and more important case: once an expo SDK bump names newer versions, installed 4.5.1 stops matching and the check turns red. That is the trigger for lifting the pin, and it fires on the correct event — the SDK moving — rather than on an unrelated upstream release.

It also confirms the Done when row is satisfiable by (c) alone: reverting the overrides block installs 0.12.1 / 4.6.0, which no longer match the SDK 57 map, so expo install --check goes red on exactly the #117 diff.

Added scope: Dependabot ignore guard for the pinned pair

#119 made react-native-worklets and react-native-reanimated direct dependencies of apps/mobile, pinned by overrides in pnpm-workspace.yaml. They now fall inside the production-deps group (patterns: "*") in .github/dependabot.yml, which carries no ignore entry for them.

Measured with a minimal repro on pnpm 11.5.2: a workspace overrides entry rewrites the importer's specifier in pnpm-lock.yaml, and pnpm install --frozen-lockfile then passes even though package.json declares a different version. So the weekly Dependabot PR bumping these to 4.6.0 / 0.12.1 goes fully green, and merging it leaves apps/mobile/package.json declaring versions that are neither installed nor linked — destroying the exact property #117 set out to establish. Dependabot does not read the overrides block; this is the same blind spot as dependabot-core #11953 (catalog not updated) and #13165 (minimumReleaseAge conflict).

Both bumps are minor — 4.5.1 → 4.6.0, and 0.10.1 → 0.12.1 is 0.x — so a version-update:semver-major filter does not catch them, unlike the existing typescript guard. It must be a bare dependency-name ignore:

- dependency-name: "react-native-worklets"
- dependency-name: "react-native-reanimated"

Why the guard belongs here and not in #119. On its own it silences these two packages permanently with no forcing function to lift the pin. It is safe only alongside the --check gate, because measurement (3) cuts both ways: --check cannot catch the Dependabot bump, but it is what catches the stale pin. Ignore and check are complementary, not alternatives — they ship together or the guard should not ship at all.

Alternative considered and rejected: ship no ignore and let --check turn the weekly PR red instead. Rejected because (3) shows --check reads installed versions, so the bump PR stays green, not red — the option does not exist. Also rejected: scoping the ignore to version-update:semver-major, which these bumps slip past.

What this gives up: no Dependabot notification for these two packages, ever again. Accepted because the notification fires on the wrong event. The expo bump itself is not ignored and remains the real signal, now backed by a check rather than by someone remembering.

Interim risk until this lands: a Monday run may produce the green-but-misleading group PR described above. Close it by hand, or drop the two packages from it — do not merge it.


Correction — 2026-09-05 (the ignore guard shipped separately, as #124 / PR #125)

Original decision, in the 2026-09-04 correction: the Dependabot ignore guard is added scope of this issue, and "ignore and check are complementary, not alternatives — they ship together or the guard should not ship at all."

That coupling was broken deliberately. It is recorded here rather than quietly dropped, because the reasoning that set it was sound and the event that overrode it was specific.

What forced it. The interim risk the last correction named as hypothetical arrived before the check was even designed. Dependabot opened #123 (production-deps, 14 updates) carrying react-native-worklets 0.10.1 → 0.12.1, react-native-reanimated 4.5.1 → 4.6.0 and react-native 0.86.2 → 0.87.1 against an SDK 57 tree that expects 0.86.x. Two things followed: the failure class is wider than the pinned pair, and waiting for the check to be designed meant hand-closing a loaded PR every Monday in the meantime. A recurring manual close is a worse forcing function than a temporarily absent lift trigger.

One prediction from the last correction was wrong. It stated the poisoned PR would arrive "fully green". #123 arrived red — because react-native 0.87.1 and other unrelated bumps in the same group broke it, not because anything detected the worklets bump. The green-lie mechanism itself (pnpm rewrites the importer specifier, --frozen-lockfile passes) is unchanged and still measured; it was merely masked here by an unrelated failure. Do not expect redness to catch it next time.

What shipped, and how it differs from the design above — issue #124, PR #125, merged to main as 85eaedf:

Designed in the 2026-09-04 correction Shipped in #125
Packages 2 — react-native-worklets, react-native-reanimated 14 — every apps/mobile dependency present in expo/bundledNativeModules.json. Derived, not curated; the regeneration command is in the config comment.
Form bare dependency-name dependency-name + all three version-update:* types spelled out
expo itself not discussed deliberately excluded — its bump is the signal that the others may move, so it must keep arriving

The update-types change corrects a real defect in the design above: a bare dependency-name rule suppresses Dependabot security updates as well as version updates. Naming the three version-update: types scopes each rule to version updates only, so a security advisory for any of the 14 still opens a PR. The last correction's "no Dependabot notification for these two packages, ever again" was accepting more than it needed to.

Observed effect. Once 85eaedf reached the default branch (Dependabot reads its config from there only), Dependabot re-evaluated both open group PRs and closed #123 and #120 itself, deleting their branches. The pending action "comment @dependabot recreate on #123" resolved itself; no open PRs remain.

What #118 is now

Scope is the CI native check alone — the added-scope row above is delivered. But the two obligations the coupling argument created still land on this issue, and are now overdue rather than pending:

  1. The lift trigger is missing. 14 packages are silenced with nothing watching for the SDK to move. expo install --check is what turns red once an expo bump names newer versions — until it runs in CI, the only trigger is someone remembering.
  2. The catch-up commit is still owed. Measurement 1 above stands unchanged: eight packages are behind what SDK 57 expects, so --check is red on the current tree and needs an expo install --fix commit before it can be a gate that is trusted rather than routed around.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    infraCI/CD, Supabase config, env varstaskSDLC task unit — one issue per task-breakdown item

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions