Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/mobile-eas-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Mobile EAS Preview

on:
pull_request:

jobs:
preview:
name: EAS Preview
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: write
env:
APP_VARIANT: preview
NODE_OPTIONS: --max-old-space-size=8192
MOBILE_VERSION_POLICY: fingerprint
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing concurrency can race deploys

Medium Severity

The new EAS preview workflow has no concurrency group for the pull request, so several pushes in quick succession can run overlapping deploy/update jobs against the same pr-<number> branch at once, unlike other PR workflows in this repo that cancel in-progress runs per PR.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d122aae. Configure here.

steps:
- id: expo-token
name: Check for EXPO_TOKEN
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
if [ -n "$EXPO_TOKEN" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "EXPO_TOKEN is not available; skipping EAS preview."
fi

- name: Checkout
if: steps.expo-token.outputs.present == 'true'
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Vite+
if: steps.expo-token.outputs.present == 'true'
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true
Comment thread
cursor[bot] marked this conversation as resolved.

- name: Expose pnpm
if: steps.expo-token.outputs.present == 'true'
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version

- name: Setup EAS
if: steps.expo-token.outputs.present == 'true'
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
packager: pnpm

- name: Pull preview environment variables
if: steps.expo-token.outputs.present == 'true'
working-directory: apps/mobile
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas env:pull preview --non-interactive

- name: Deploy with fingerprint check
if: steps.expo-token.outputs.present == 'true'
uses: expo/expo-github-action/continuous-deploy-fingerprint@main
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
with:
Comment thread
Yash-Singh1 marked this conversation as resolved.
profile: preview:dev
branch: pr-${{ github.event.pull_request.number }}
platform: all
environment: preview
working-directory: apps/mobile
github-token: ${{ secrets.GITHUB_TOKEN }}
24 changes: 24 additions & 0 deletions apps/mobile/.fingerprintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Expo artifacts
.expo/
.eas/

# Build outputs
android/
ios/
dist/
web-build/

# Test files
**/*.test.ts
**/*.test.tsx

# Docs
*.md

# Local env
.env
.env.local
.env*.local

# TypeScript
*.tsbuildinfo
9 changes: 9 additions & 0 deletions apps/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin.

## EAS Builds

CI uses Expo fingerprinting with the `preview:dev` profile to reuse an existing compatible build when possible, or start a new internal EAS build when native runtime inputs change. Production and default local builds continue to use the `appVersion` runtime policy.

Create a PR preview dev-client build manually:

```bash
vp run eas:ios:preview:dev
```

Create a cloud dev-client build:

```bash
Expand All @@ -77,5 +85,6 @@ Android equivalents:

```bash
vp run eas:android:dev
vp run eas:android:preview:dev
vp run eas:android:preview
```
3 changes: 2 additions & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ const variant = VARIANT_CONFIG[APP_VARIANT];
const config: ExpoConfig = {
name: variant.appName,
slug: "t3-code",
platforms: ["ios", "android"],
scheme: variant.scheme,
version: "0.1.0",
runtimeVersion: {
policy: "appVersion",
policy: process.env.MOBILE_VERSION_POLICY ?? "appVersion",
},
orientation: "portrait",
icon: "./assets/icon.png",
Expand Down
15 changes: 15 additions & 0 deletions apps/mobile/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@
"APP_VARIANT": "preview"
},
"channel": "preview",
"environment": "preview",
"distribution": "internal"
},
"preview:dev": {
"env": {
"APP_VARIANT": "preview",
"MOBILE_VERSION_POLICY": "fingerprint"
},
"channel": "preview",
"environment": "preview",
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"env": {
"APP_VARIANT": "production"
},
"channel": "production",
"environment": "production",
"autoIncrement": true
}
},
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
"android:prod": "APP_VARIANT=production EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
"eas:android:dev": "eas build --profile development -p android",
"eas:android:preview": "eas build --profile preview -p android",
"eas:android:preview:dev": "eas build --profile preview:dev -p android",
"eas:android:prod": "eas build --profile production -p android",
"ios": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios",
"ios:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios",
"ios:preview": "APP_VARIANT=preview EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios",
"ios:prod": "APP_VARIANT=production EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform ios && expo run:ios",
"eas:ios:dev": "eas build --profile development -p ios",
"eas:ios:preview": "eas build --profile preview -p ios",
"eas:ios:preview:dev": "eas build --profile preview:dev -p ios",
"eas:ios:prod": "eas build --profile production -p ios",
"eas:dev": "eas build --profile development",
"eas:preview": "eas build --profile preview",
"eas:preview:dev": "eas build --profile preview:dev",
"eas:prod": "eas build --profile production",
"config:dev": "APP_VARIANT=development expo config",
"config:preview": "APP_VARIANT=preview expo config",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"sync:repos": "node scripts/sync-reference-repos.ts"
},
"devDependencies": {
"@babel/plugin-transform-react-jsx": "7.28.6",
"@effect/tsgo": "catalog:",
"@oxlint/plugins": "^1.63.0",
"@types/node": "catalog:",
Expand Down Expand Up @@ -99,6 +100,7 @@
]
},
"patchedDependencies": {
"@expo/metro-config@56.0.13": "patches/@expo%2Fmetro-config@56.0.13.patch",
Comment thread
cursor[bot] marked this conversation as resolved.
"@pierre/diffs@1.1.20@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch",
"effect@4.0.0-beta.73@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch",
"react-native-nitro-modules@0.35.9@0.35.9": "patches/react-native-nitro-modules@0.35.9.patch"
Expand Down
93 changes: 93 additions & 0 deletions patches/@expo%2Fmetro-config@56.0.13.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
diff --git a/build/serializer/sourceMap.js b/build/serializer/sourceMap.js
index 4cc9aa4..703dfe0 100644
--- a/build/serializer/sourceMap.js
+++ b/build/serializer/sourceMap.js
@@ -20,6 +20,20 @@ function loadRemapping() {
}
return _remapping;
}
+let _sourceMapCodec;
+function loadSourceMapCodec() {
+ if (!_sourceMapCodec) {
+ _sourceMapCodec = require('@jridgewell/sourcemap-codec');
+ }
+ return _sourceMapCodec;
+}
+let _traceMapping;
+function loadTraceMapping() {
+ if (!_traceMapping) {
+ _traceMapping = require('@jridgewell/trace-mapping');
+ }
+ return _traceMapping;
Comment thread
cursor[bot] marked this conversation as resolved.
+}
let _Generator;
function loadGenerator() {
if (!_Generator) {
@@ -199,6 +213,58 @@ function patchMetroSourceMapStringForPackedMaps() {
stock.sourceMapString = sourceMapString;
stock.sourceMapStringNonBlocking = sourceMapStringNonBlocking;
}
+// Hermes can emit mappings for Metro trailer/debug lines that have no
+// corresponding Metro source-map line. @jridgewell/remapping assumes every
+// referenced generated line exists, so strip those invalid bridge segments.
+function getGeneratedLineCount(map) {
+ const { TraceMap, decodedMappings } = loadTraceMapping();
+ return decodedMappings(new TraceMap(map)).length;
+}
+function dropInvalidOriginalSegments(map, maxOriginalLine) {
+ const { TraceMap, decodedMappings } = loadTraceMapping();
+ const decoded = decodedMappings(new TraceMap(map));
+ let didChange = false;
+ const filtered = decoded.map((line) => {
+ const filteredLine = line.filter((segment) => {
+ if (segment.length < 4) {
+ return true;
+ }
+ const sourceIndex = segment[1];
+ const sourceLine = segment[2];
+ const sourceColumn = segment[3];
+ return (sourceIndex >= 0 &&
+ sourceIndex < map.sources.length &&
+ sourceLine >= 0 &&
+ sourceLine < maxOriginalLine &&
+ sourceColumn >= 0);
+ });
+ if (filteredLine.length !== line.length) {
+ didChange = true;
+ }
+ return filteredLine;
+ });
+ if (!didChange) {
+ return map;
+ }
+ return {
+ ...map,
+ mappings: loadSourceMapCodec().encode(filtered),
+ };
+}
+function sanitizeSourceMapsForComposition(maps) {
+ let sanitized = maps;
+ for (let index = 1; index < maps.length; index++) {
+ const maxOriginalLine = getGeneratedLineCount(sanitized[index - 1]);
+ const next = dropInvalidOriginalSegments(sanitized[index], maxOriginalLine);
+ if (next !== sanitized[index]) {
+ if (sanitized === maps) {
+ sanitized = maps.slice();
+ }
+ sanitized[index] = next;
+ }
+ }
+ return sanitized;
+}
// `maps[0]` is the original-most transform; `maps[maps.length - 1]` is
// the most recent. Built on `@jridgewell/remapping` instead of mozilla's
// `SourceMapConsumer`-based composer.
@@ -226,7 +292,7 @@ function composeSourceMaps(maps) {
return { ...map, ignoreList: map.x_google_ignoreList };
});
// Metro convention is original-first; remapping is most-recent first.
- const reversed = normalized.slice().reverse();
+ const reversed = sanitizeSourceMapsForComposition(normalized).slice().reverse();
const composed = loadRemapping()(reversed, () => null);
// Re-emit as a plain object — remapping returns a `SourceMap` class
// instance, which doesn't round-trip JSON cleanly.
33 changes: 28 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading