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
105 changes: 105 additions & 0 deletions app/.gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,111 @@ preview:web-next:
changes:
- app/web/**/*

# Per-branch Dev Tool OTA preview: bundle the devtool variant and stage the Expo
# Updates manifest (see docs/mobile-dev-tool-ota.md), then publish to the existing
# dev-assets bucket under an immutable per-SHA prefix. The installed Dev Tool dev
# client loads it via couchers-devtool://expo-development-client/?url=...
build:mobile-ota:
needs: ["protos"]
stage: build
image: node:22
inherit:
default: false
variables:
# Must build the devtool variant so the fingerprint matches the installed
# Dev Tool dev client; a mismatch makes the client silently ignore the update.
APP_VARIANT: devtool
EXPO_PUBLIC_COUCHERS_ENV: preview
EXPO_PUBLIC_API_BASE_URL: https://dev-api.couchershq.org
EXPO_PUBLIC_WEB_BASE_URL: https://next.couchershq.org
OTA_PLATFORMS: "ios android"
script:
- cd app/mobile
- npm ci
- npm run build:protos
- npx expo config --type public --json > ota-expo-config.json
- |
for P in $OTA_PLATFORMS; do
rm -rf dist
npx expo export --platform "$P"
RV=$(npx expo-updates fingerprint:generate --platform "$P" 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).hash))')
echo "fingerprint($P) = $RV"
node scripts/ota-stage.mjs --platform "$P" --runtime-version "$RV" --base-url "https://$CI_COMMIT_SHORT_SHA--ota.$PREVIEW_DOMAIN" --dist dist --out ota-out --expo-config ota-expo-config.json
# QR encodes the dev-launcher deep link so the PR comment can show a scannable image.
# npx (not a package.json dep) keeps it out of the fingerprint sources.
npx --yes qrcode -o "ota-out/$P/qr.png" "couchers-devtool://expo-development-client/?url=https%3A%2F%2F$CI_COMMIT_SHORT_SHA--ota.$PREVIEW_DOMAIN%2F$P%2Fmanifest" < /dev/null
done
artifacts:
paths:
- app/mobile/ota-out/
expire_in: 1 week
rules:
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH != $RELEASE_BRANCH)
changes:
- app/proto/**/*
- app/mobile/**/*
- app/scripts/**/*

preview:mobile-ota:
needs: ["build:mobile-ota"]
stage: preview
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
inherit:
# no docker login
default: false
variables:
OTA_PLATFORMS: "ios android"
script:
- |
for P in $OTA_PLATFORMS; do
D="app/mobile/ota-out/$P"
CT="$(cat "$D/manifest.content-type")"
# manifest is the protocol-v1 multipart body; its content-type carries the
# boundary. The expo-protocol-version response header is injected by the
# preview-origin-response Lambda@Edge (S3 can't set it).
aws s3 cp "$D/manifest" "s3://$AWS_PREVIEW_BUCKET/ota/$CI_COMMIT_SHORT_SHA/$P/manifest" --content-type "$CT"
aws s3 cp "$D/bundle.hbc" "s3://$AWS_PREVIEW_BUCKET/ota/$CI_COMMIT_SHORT_SHA/$P/bundle.hbc" --content-type application/javascript
aws s3 cp "$D/assets/" "s3://$AWS_PREVIEW_BUCKET/ota/$CI_COMMIT_SHORT_SHA/$P/assets/" --recursive
aws s3 cp "$D/qr.png" "s3://$AWS_PREVIEW_BUCKET/ota/$CI_COMMIT_SHORT_SHA/$P/qr.png" --content-type image/png
aws s3 cp "$D/open.html" "s3://$AWS_PREVIEW_BUCKET/ota/$CI_COMMIT_SHORT_SHA/$P/open.html" --content-type "text/html; charset=utf-8"
echo "Done $P. Manifest: https://$CI_COMMIT_SHORT_SHA--ota.$PREVIEW_DOMAIN/$P/manifest"
echo " deep link: couchers-devtool://expo-development-client/?url=https%3A%2F%2F$CI_COMMIT_SHORT_SHA--ota.$PREVIEW_DOMAIN%2F$P%2Fmanifest"
done
rules:
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH != $RELEASE_BRANCH)
changes:
- app/proto/**/*
- app/mobile/**/*
- app/scripts/**/*

# Aggregate per-PR preview links into one sticky GitHub PR comment. Runs after the
# upload jobs (needs) so every link/QR it posts is already live on CloudFront.
# Currently surfaces the mobile Dev Tool OTA preview; add more sections (web,
# coverage, ...) in app/scripts/pr_preview_comment.py as previews grow. No-ops when
# GITHUB_PREVIEW_TOKEN is unset or there's no open PR, so it never reds the pipeline.
preview:pr-comment:
needs:
- job: preview:mobile-ota
artifacts: false
stage: preview
image: python:3.12-slim
inherit:
default: false
variables:
OTA_PLATFORMS: "ios android"
script:
- pip install --quiet --no-cache-dir requests
- python app/scripts/pr_preview_comment.py
rules:
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH != $RELEASE_BRANCH)
changes:
- app/proto/**/*
- app/mobile/**/*
- app/scripts/**/*

preview:protos:
needs: ["protos"]
stage: preview
Expand Down
9 changes: 9 additions & 0 deletions app/mobile/.fingerprintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# google-services.json is provided only as an EAS file env secret
# (GOOGLE_SERVICES_JSON, preview environment), so it's present on EAS builds but
# absent locally and in CI. Expo hashes it into the fingerprint by contents, which
# makes the runtime version (fingerprint policy) differ between the EAS-built Dev
# Tool client and our per-branch OTA bundles / local builds. Exclude it everywhere
# so the fingerprint is reproducible; a Firebase/FCM config change needs a fresh
# native build regardless.
google-services.json
**/eas-environment-secrets/**
4 changes: 4 additions & 0 deletions app/mobile/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ GoogleService-Info.plist
# Metro
.metro-health-check*

# Per-branch OTA preview pipeline (scripts/ota-stage.mjs, expo export)
/ota-out/
/ota-expo-config.json

# debug
npm-debug.*
yarn-debug.*
Expand Down
53 changes: 48 additions & 5 deletions app/mobile/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Couchers Mobile App

React Native mobile app built with [Expo](https://expo.dev). We maintain two separate apps (staging and production) that can coexist on the same device with different bundle IDs and API endpoints.
React Native mobile app built with [Expo](https://expo.dev). We maintain three separate apps (staging, production, and a development tool) that can coexist on the same device with different bundle IDs and API endpoints.

## Table of Contents

Expand All @@ -10,8 +10,9 @@ React Native mobile app built with [Expo](https://expo.dev). We maintain two sep
4. [Releasing Staging Build](#4-releasing-staging-build)
5. [Releasing Production Build](#5-releasing-production-build)
6. [App Variants](#app-variants)
7. [Updating Dependencies](#updating-dependencies)
8. [Learn More](#learn-more)
7. [Dev Tool (TestFlight)](#dev-tool-testflight)
8. [Updating Dependencies](#updating-dependencies)
9. [Learn More](#learn-more)

## 1. First Time Setup

Expand Down Expand Up @@ -52,6 +53,12 @@ npx expo run:android --device

On iOS: if you get issues about signing, try opening `app/mobile/ios` in Xcode and setting up app signing there.

> **Tip:** If you only work on JavaScript/TypeScript, you can skip the local
> native build (and Xcode/Android Studio) entirely — install the prebuilt
> **Couchers (Dev Tool)** app from TestFlight and run `npx expo start` against it.
> See [Dev Tool (TestFlight)](#dev-tool-testflight). You only
> need a local native build when changing native dependencies or `app.config.js`.

**After the initial build is installed, use this for daily development:**
```bash
npx expo start
Expand Down Expand Up @@ -170,17 +177,53 @@ npm run release:android:production

## App Variants

We maintain **two separate apps** that can coexist on the same device:
We maintain **three separate apps** that can coexist on the same device:

| Variant | App Name | iOS Bundle ID | Android Package | API Server |
|---------|----------|---------------|-----------------|------------|
| **Dev Tool** | Couchers (Dev Tool) | `org.couchers.devtool.ios` | `org.couchers.devtool.android` | `dev-api.couchershq.org` |
| **Staging** | Couchers (Staging) | `org.couchers.staging.ios` | `org.couchers.staging.android` | `dev-api.couchershq.org` |
| **Production** | Couchers | `org.couchers.ios` | `org.couchers.android` | `api.couchers.org` |

**Benefits:** Both apps can be installed simultaneously, separate push notification channels, test staging changes without affecting production users.
**Benefits:** All apps can be installed simultaneously, separate push notification channels, test staging changes without affecting production users.

**How it works:** Build profiles in `eas.json` set an `APP_VARIANT` environment variable, which `app.config.js` reads to configure bundle IDs, app names, and API endpoints dynamically.

The **Dev Tool** variant is a [development build](#dev-tool-testflight) (it reuses the staging icons and backend). The **Dev Tool** and **Staging** apps both point at `dev-api.couchershq.org` but are distinct apps with separate bundle IDs.

## Dev Tool (TestFlight)

The **Couchers (Dev Tool)** variant is a [development build](https://docs.expo.dev/develop/development-builds/introduction/) — essentially "Expo Go, but with our own native modules." It bundles every native dependency in the project plus the Expo dev launcher, and points at the staging backend. Devs install it once from TestFlight and load JavaScript over the air, so they never need Xcode, CocoaPods, or a local native build for day-to-day JS/TS work. The "Dev Tool" name signals it's a developer utility, not another release flavor like staging or production.

**Daily workflow (no Xcode needed):**

```bash
npx expo start
```

Open the **Couchers (Dev Tool)** app and connect to the Metro server (same network), or scan the QR code. JS/TS changes hot-reload exactly as they do with a locally built development build.

**When a new Dev Tool build is required:** only when the set of native dependencies changes (adding/removing a native package, changing `app.config.js`, or bumping the Expo SDK). Pure JS/TS changes never need a rebuild — they load over the air.

### Releasing a new Dev Tool build

```bash
npm run release:ios:devtool # iOS → TestFlight
npm run release:android:devtool # Android → Play internal testing
```

Once submitted, the build appears in TestFlight after Apple's automated processing (no full App Review for internal testers). Invited devs update from the TestFlight app.

### One-time setup (maintainers)

Before the first release, the **Couchers (Dev Tool)** app records must exist:

1. Create the app in [App Store Connect](https://appstoreconnect.apple.com) with bundle ID `org.couchers.devtool.ios`, and create the matching app in [Google Play Console](https://play.google.com/console) with package `org.couchers.devtool.android`.
2. Replace `REPLACE_WITH_DEVTOOL_ASC_APP_ID` in `eas.json` (`submit.devtool.ios.ascAppId`) with the new App Store Connect app ID.
3. Invite developers as internal TestFlight testers — no per-device UDID registration is required (unlike EAS internal/ad-hoc distribution).

> **Coming next:** with the Dev Tool installed, we can wire up per-PR JavaScript previews — a CI job publishes each PR's bundle via `eas update` and posts a QR code on the PR. Scanning it in the Dev Tool loads that branch's changes, the mobile analog of our Vercel web previews. (JS-only; the `runtimeVersion: fingerprint` policy ensures a PR that changes native code won't load a mismatched bundle.)

## Updating Dependencies

Mobile dependencies are **not** managed by Dependabot. Expo requires specific compatible versions of packages tied to each SDK version, so dependencies should be updated through Expo's tooling:
Expand Down
Loading
Loading