Skip to content

Commit 538e800

Browse files
aapelivclaude
andcommitted
Add staging OTA publish pipeline and fix staging manifest path
Adds a develop-only GitLab CI build+deploy pair that bundles the staging variant, stages the (unsigned) Expo Updates output, uploads bundle+assets to dev-assets, and prints the native_ota_bundles JSON for the staging GrowthBook flag. Also fixes the staging app's update URL to the renamed /native/ota/manifest endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2b6a872 commit 538e800

3 files changed

Lines changed: 118 additions & 1 deletion

File tree

app/.gitlab-ci.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,72 @@ preview:pr-comment:
939939
- app/mobile/**/*
940940
- app/scripts/**/*
941941

942+
# Staging OTA auto-deploy. Mirrors the web staging deploy (build a static bundle,
943+
# publish to dev-assets, serve from there): bundle the staging variant, stage the
944+
# (unsigned) Expo Updates output, upload bundle+assets to the dev-assets CDN under
945+
# an immutable per-SHA prefix, and print the native_ota_bundles JSON. The staging
946+
# backend (dev-api.couchershq.org/native/ota/manifest) builds the served manifest
947+
# from that flag, with asset URLs pointing at what this job uploaded. Runs on
948+
# develop only. Production publishing goes through the tools/ ops deploy.
949+
build:mobile-ota-staging:
950+
needs: ["protos"]
951+
stage: build
952+
image: node:22
953+
inherit:
954+
default: false
955+
variables:
956+
APP_VARIANT: staging
957+
EXPO_PUBLIC_COUCHERS_ENV: preview
958+
EXPO_PUBLIC_API_BASE_URL: https://dev-api.couchershq.org
959+
EXPO_PUBLIC_WEB_BASE_URL: https://next.couchershq.org
960+
OTA_PLATFORMS: "ios android"
961+
script:
962+
- cd app/mobile
963+
- npm ci
964+
- npm run build:protos
965+
- npx expo config --type public --json > ota-expo-config.json
966+
- |
967+
for P in $OTA_PLATFORMS; do
968+
rm -rf dist
969+
npx expo export --platform "$P"
970+
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))')
971+
echo "fingerprint($P) = $RV"
972+
# The asset URLs baked into the manifest must resolve to the upload location
973+
# below; the CDN host maps <sha>--ota-staging -> the /ota-staging/<sha>/ prefix.
974+
node scripts/ota-stage.mjs --platform "$P" --runtime-version "$RV" --base-url "https://$CI_COMMIT_SHORT_SHA--ota-staging.$PREVIEW_DOMAIN" --dist dist --out ota-staging-out --expo-config ota-expo-config.json
975+
done
976+
- node scripts/ota-growthbook-flag.mjs --out ota-staging-out --platforms "$OTA_PLATFORMS" | tee ota-staging-out/native_ota_bundles.json
977+
artifacts:
978+
paths:
979+
- app/mobile/ota-staging-out/
980+
expire_in: 1 week
981+
rules:
982+
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
983+
984+
deploy:mobile-ota-staging:
985+
needs: ["build:mobile-ota-staging"]
986+
stage: deploy
987+
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
988+
inherit:
989+
# no docker login
990+
default: false
991+
variables:
992+
OTA_PLATFORMS: "ios android"
993+
script:
994+
- |
995+
for P in $OTA_PLATFORMS; do
996+
D="app/mobile/ota-staging-out/$P"
997+
# Only the bundle + assets go on the CDN; the manifest is served by the backend.
998+
aws s3 cp "$D/bundle.hbc" "s3://$AWS_PREVIEW_BUCKET/ota-staging/$CI_COMMIT_SHORT_SHA/$P/bundle.hbc" --content-type application/javascript
999+
if [ -d "$D/assets" ]; then
1000+
aws s3 cp "$D/assets/" "s3://$AWS_PREVIEW_BUCKET/ota-staging/$CI_COMMIT_SHORT_SHA/$P/assets/" --recursive
1001+
fi
1002+
done
1003+
- echo "Bundle + assets uploaded. Paste this into the staging GrowthBook 'native_ota_bundles' flag:"
1004+
- cat app/mobile/ota-staging-out/native_ota_bundles.json
1005+
rules:
1006+
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
1007+
9421008
preview:protos:
9431009
needs: ["protos"]
9441010
stage: preview

app/mobile/app.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const updates =
8585
APP_VARIANT === "devtool"
8686
? { enabled: true }
8787
: APP_VARIANT === "staging"
88-
? { url: "https://dev-api.couchershq.org/mobile/ota/manifest" }
88+
? { url: "https://dev-api.couchershq.org/native/ota/manifest" }
8989
: { url: "https://u.expo.dev/fb4fc9aa-d8b2-45a5-82aa-be05e99b0413" };
9090

9191
export default {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env node
2+
// Turn staged `ota-stage.mjs` output into the `native_ota_bundles` GrowthBook
3+
// flag value the backend's OTA manifest endpoint reads
4+
// (couchers/servicers/bugs.py: GetMobileUpdateManifest). The backend builds the
5+
// served manifest from this value, so the launch asset / asset URLs here are what
6+
// the device fetches — they must point at wherever the bundle was uploaded.
7+
//
8+
// Usage:
9+
// node scripts/ota-growthbook-flag.mjs --out ota-staging-out --platforms "ios android"
10+
//
11+
// Prints a JSON object keyed by platform; paste it into the GrowthBook
12+
// `native_ota_bundles` flag for the target environment.
13+
14+
import fs from "node:fs";
15+
import path from "node:path";
16+
17+
function parseArgs(argv) {
18+
const args = {};
19+
for (let i = 0; i < argv.length; i += 2) {
20+
const key = argv[i];
21+
if (!key.startsWith("--")) throw new Error(`Unexpected argument: ${key}`);
22+
args[key.slice(2)] = argv[i + 1];
23+
}
24+
return args;
25+
}
26+
27+
function main() {
28+
const args = parseArgs(process.argv.slice(2));
29+
const outRoot = path.resolve(args.out ?? "ota-out");
30+
const platforms = (args.platforms ?? "ios android")
31+
.split(/\s+/)
32+
.filter(Boolean);
33+
34+
const flag = {};
35+
for (const platform of platforms) {
36+
const manifestPath = path.join(outRoot, platform, "manifest.json");
37+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
38+
flag[platform] = {
39+
id: manifest.id,
40+
// For operator reference: the published bundle's fingerprint. The installed
41+
// store build must share it, or the JS won't be compatible.
42+
runtime_version: manifest.runtimeVersion,
43+
launch_asset: manifest.launchAsset,
44+
assets: manifest.assets,
45+
};
46+
}
47+
48+
process.stdout.write(JSON.stringify(flag, null, 2) + "\n");
49+
}
50+
51+
main();

0 commit comments

Comments
 (0)