Merge pull request #2 from jjbrunton/staging

chore(release): promote mobile build hardening
This commit is contained in:
jjbrunton authored and GitHub committed 2026-08-28 18:44:58 +01:00
commit 1be54175e4
12 files changed
+177 -56

No files matched your search

@@ -0,0 +1,37 @@
---
id: 2026-08-28-maestro-stable-login-readiness
date: 2026-08-28
status: promoted
scope: native Auth-shell acceptance flows
evidence: iOS simulator login-shell acceptance on current main
destination: e2e/maestro
review_after: 2027-02-28
---
## Symptom
The credential-free native smoke failed even though the current app reached the
login screen and displayed its required controls.
## Reproduction
Run `e2e/maestro/login-screen-smoke.yaml` against current main. The flow waited
for the retired tagline `Explore intimacy together`; the app displayed updated
marketing copy while `Sauci` and `Password` were present.
## Cause
Login readiness was coupled to mutable marketing copy rather than a stable
required control.
## Remediation
Wait for the password control that defines login readiness, while retaining the
brand and password assertions. Apply the same readiness condition to the
authenticated and credential-free flows.
## Evidence
Simulator read-back confirmed the current login shell exposes `Sauci` and
`Password`. The maintained Maestro flows now wait for that stable control rather
than the tagline.
@@ -0,0 +1,40 @@
---
id: 2026-08-28-mobile-release-source-integrity
date: 2026-08-28
status: promoted
scope: mobile release build and upload workflow
evidence: clean-source local EAS builds for iOS 43 and Android 46
destination: scripts/lint-harness.mjs
review_after: 2027-02-28
---
## Symptom
A build-only request initially regenerated tracked native projects. The generated
Android widget provider replaced the checked-in implementation, and the Android
release script would also have uploaded after building.
## Reproduction
Run the former mobile release scripts or run `expo prebuild` before a local EAS
build, then inspect `git diff`. The reproduced diff included the Android widget
provider, widget XML, and Xcode project rather than version metadata alone.
## Cause
The release scripts combined three independently reviewed operations: native
regeneration, artifact creation, and store upload. Sauci already tracks native
projects, and EAS skips prebuild when those directories exist.
## Remediation
Keep native regeneration explicit, make release scripts artifact-only, expose
upload separately, and lint the scripts so those responsibilities cannot be
silently recombined.
## Evidence
Corrected local EAS logs reported `PREBUILD` skipped. Both production artifacts
built successfully from the clean tracked sources; the post-build worktree
contained only the four expected build-number files. `npm run lint:harness`
enforces the release-script boundary.
+12 -4
View File
@@ -9,7 +9,15 @@ Read `docs/releasing.md`, `apps/mobile/RELEASING.md` when present, and the mobil
scoped instructions. Confirm target platform and release profile. Run repository
verification plus a native acceptance pass before starting a release build.
Use the package scripts in `apps/mobile/package.json`; preserve platform signing
and versioning. Building or uploading to a store is an external mutation: obtain
explicit authorization for the requested target, then report the build artifact,
version/build number, upload result, and read-back state.
Before a build from the current upstream branch, fetch, require a clean worktree,
and prove `HEAD` matches its upstream. Sauci tracks its native iOS and Android
projects, so routine release builds must package those checked-in sources. Do not
run `expo prebuild` first; native regeneration is a separate reviewed change.
Use the package scripts in `apps/mobile/package.json`; they create artifacts only.
Never infer upload or submission authority from build authorization. Store upload
uses a separately authorized command. Preserve platform signing and local EAS
versioning. After a build, verify that tracked changes are limited to the expected
version metadata, then independently read the artifact version and signature.
Report the artifact, version/build number, upload result when authorized, and
read-back state.
+1 -1
View File
@@ -91,7 +91,7 @@ android {
applicationId 'com.sauci.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 45
versionCode 46
versionName "1.0.5"
}
signingConfigs {
+2 -2
View File
@@ -21,7 +21,7 @@
"group.com.sauci.app"
]
},
"buildNumber": "42",
"buildNumber": "43",
"usesAppleSignIn": true,
"googleServicesFile": "./GoogleService-Info.plist",
"infoPlist": {
@@ -183,7 +183,7 @@
"backgroundColor": "#1a1a2e"
},
"package": "com.sauci.app",
"versionCode": 45,
"versionCode": 46,
"googleServicesFile": "./google-services.json",
"permissions": [
"RECEIVE_BOOT_COMPLETED",
+1 -1
View File
@@ -47,7 +47,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>42</string>
<string>43</string>
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
<false/>
<key>LSMinimumSystemVersion</key>
+5 -4
View File
@@ -13,10 +13,11 @@
"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --runInBand --ci",
"release:ios": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile production --local",
"release:ios:preview": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile preview --local",
"release:android": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile production --local && bundle exec fastlane android upload",
"release:android:preview": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile preview --local"
"release:ios": "dotenv -e .env -- eas build --platform ios --profile production --local",
"release:ios:preview": "dotenv -e .env -- eas build --platform ios --profile preview --local",
"release:android": "dotenv -e .env -- eas build --platform android --profile production --local",
"release:android:preview": "dotenv -e .env -- eas build --platform android --profile preview --local",
"release:android:upload": "bundle exec fastlane android upload"
},
"dependencies": {
"@bacons/apple-targets": "^0.1.6",
+1 -1
View File
@@ -8,7 +8,7 @@
<string>com.apple.widgetkit-extension</string>
</dict>
<key>CFBundleVersion</key>
<string>42</string>
<string>43</string>
<key>CFBundleShortVersionString</key>
<string>1.0.5</string>
</dict>
+55 -41
View File
@@ -18,17 +18,33 @@ The `--local` flag runs the EAS build process on your machine instead of Expo's
### Quick Start
```bash
cd apps/mobile
# Android - produces .aab file (for Play Store)
eas build --platform android --profile production --local
# iOS - produces .ipa file (for App Store)
eas build --platform ios --profile production --local
```
Output files are placed in the current directory by default.
```bash
cd apps/mobile
# Android - produces .aab file (for Play Store)
npm run release:android
# iOS - produces .ipa file (for App Store)
npm run release:ios
```
Output files are placed in the current directory by default.
These scripts create signed artifacts only. They do not upload or submit them.
Store mutation always requires a separate, explicitly authorized command.
### Source integrity
Sauci commits its native `ios/` and `android/` projects. Start a release from a
clean worktree whose `HEAD` matches its fetched upstream, and do not run
`expo prebuild` before a routine release build. EAS detects the native projects
and packages the checked-in sources directly.
After the build, inspect `git diff`. Only the expected local EAS version fields
may change: `app.json`, the platform native version file, and the iOS widget
`Info.plist` for an iOS build. Any widget implementation, resource, or Xcode
project rewrite means the source was regenerated and the artifact must be rebuilt
from the clean tracked sources.
### Building an APK (for Testing)
@@ -116,13 +132,11 @@ export JAVA_HOME=$(/usr/libexec/java_home -v 17)
cd android && ./gradlew --stop && cd ..
```
**Clean build after dependency changes:**
```bash
# Remove native folders and rebuild
rm -rf android ios
npx expo prebuild --clean
eas build --platform android --profile production --local
```
**Native regeneration after dependency changes:**
Treat `expo prebuild` as a source change, not a release step. Run it separately
only when native dependencies intentionally changed, review the full native diff,
verify it, and commit it before starting the release from a clean worktree.
**Out of memory:**
```bash
@@ -163,13 +177,10 @@ MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS>
**Important:** Never commit these credentials to git.
### Step 3: Prebuild (if needed)
If the `android/` folder doesn't exist or native dependencies changed:
```bash
npx expo prebuild --platform android --clean
```
### Step 3: Confirm tracked native sources
The committed `android/` project is the release source. If native dependencies
changed, regenerate and review that project as a separate change before release.
### Step 4: Build the Release Bundle
@@ -206,11 +217,10 @@ Expected SHA1: `C3:70:C5:8C:85:E5:B5:92:97:82:A9:B0:47:98:40:99:AC:C8:47:43`
## iOS Release Build
### Step 1: Prebuild (if needed)
```bash
npx expo prebuild --platform ios --clean
```
### Step 1: Confirm tracked native sources
The committed `ios/` project is the release source. If native dependencies
changed, regenerate and review that project as a separate change before release.
### Step 2: Install Pods
@@ -245,9 +255,11 @@ cd ..
## Version Management
Version is managed by EAS with `appVersionSource: "remote"` in `eas.json`.
Version is managed locally by EAS with `appVersionSource: "local"` in
`apps/mobile/eas.json`. Production builds auto-increment the platform build
number and synchronize the tracked native version file.
To sync or bump versions:
To inspect or deliberately set versions:
```bash
# Check current version
@@ -258,7 +270,7 @@ eas build:version:set --platform android --version 1.0.1
eas build:version:set --platform ios --version 1.0.1
```
For local builds, you may need to update `app.json` version manually:
The user-facing version remains in `app.json`:
```json
{
@@ -288,14 +300,16 @@ If Play Store rejects with "signed with wrong key":
3. Verify fingerprint matches expected SHA1
4. Replace your local keystore file
### Build Failures After Dependency Changes
```bash
# Clean and rebuild
cd android && ./gradlew clean && cd ..
npx expo prebuild --platform android --clean
cd android && ./gradlew bundleRelease
```
### Build Failures After Dependency Changes
```bash
# Clean the existing tracked Android project and rebuild it
cd android && ./gradlew clean && cd ..
cd android && ./gradlew bundleRelease
```
If native regeneration is genuinely required, do it as a separate reviewed and
committed source change before running the release build.
### iOS Signing Issues
+1 -1
View File
@@ -12,7 +12,7 @@ name: Standalone-backend build reaches the Auth login screen
- tapOn:
point: "94%,44%"
- extendedWaitUntil:
visible: "Explore intimacy together"
visible: "Password"
timeout: 30000
- assertVisible: "Sauci"
- assertVisible: "Password"
+1 -1
View File
@@ -12,7 +12,7 @@ name: Hosted Auth fixture reaches the standalone API-backed app
- tapOn:
point: "94%,44%"
- extendedWaitUntil:
visible: "Explore intimacy together"
visible: "Password"
timeout: 30000
- tapOn: "Password"
- tapOn: "Enter your email"
+21
View File
@@ -37,6 +37,27 @@ for (const file of allFiles) {
if (!scoped) {
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
const mobilePackageFile = path.join(root, 'apps', 'mobile', 'package.json');
const mobilePackageJson = JSON.parse(await readFile(mobilePackageFile, 'utf8'));
const releaseBuildScripts = Object.entries(mobilePackageJson.scripts ?? {})
.filter(([name]) => /^release:(ios|android)(:preview)?$/.test(name));
for (const [name, command] of releaseBuildScripts) {
if (/\bexpo\s+prebuild\b/.test(command)) {
fail(
mobilePackageFile,
`${name} regenerates tracked native projects before packaging`,
'keep native regeneration as an explicit reviewed change and let the release script build the checked-in native sources',
);
}
if (/\b(?:fastlane|eas)\b[^\n]*(?:upload|submit)|\bupload_to_/.test(command)) {
fail(
mobilePackageFile,
`${name} combines build creation with store mutation`,
'keep build scripts artifact-only and expose upload or submission as a separately authorized command',
);
}
}
for (const app of (await readdir(path.join(root, 'apps'), { withFileTypes: true })).filter((entry) => entry.isDirectory())) {
const packageFile = path.join(root, 'apps', app.name, 'package.json');
if (!(await exists(packageFile))) continue;