Merge pull request #1 from jjbrunton/task/mobile-release-hardening

chore(release): harden mobile build workflow
This commit is contained in:
jjbrunton authored and GitHub committed 2026-08-28 18:39:14 +01:00
commit a7470310ae
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 scoped instructions. Confirm target platform and release profile. Run repository
verification plus a native acceptance pass before starting a release build. verification plus a native acceptance pass before starting a release build.
Use the package scripts in `apps/mobile/package.json`; preserve platform signing Before a build from the current upstream branch, fetch, require a clean worktree,
and versioning. Building or uploading to a store is an external mutation: obtain and prove `HEAD` matches its upstream. Sauci tracks its native iOS and Android
explicit authorization for the requested target, then report the build artifact, projects, so routine release builds must package those checked-in sources. Do not
version/build number, upload result, and read-back state. 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' applicationId 'com.sauci.app'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 45 versionCode 46
versionName "1.0.5" versionName "1.0.5"
} }
signingConfigs { signingConfigs {
+2 -2
View File
@@ -21,7 +21,7 @@
"group.com.sauci.app" "group.com.sauci.app"
] ]
}, },
"buildNumber": "42", "buildNumber": "43",
"usesAppleSignIn": true, "usesAppleSignIn": true,
"googleServicesFile": "./GoogleService-Info.plist", "googleServicesFile": "./GoogleService-Info.plist",
"infoPlist": { "infoPlist": {
@@ -183,7 +183,7 @@
"backgroundColor": "#1a1a2e" "backgroundColor": "#1a1a2e"
}, },
"package": "com.sauci.app", "package": "com.sauci.app",
"versionCode": 45, "versionCode": 46,
"googleServicesFile": "./google-services.json", "googleServicesFile": "./google-services.json",
"permissions": [ "permissions": [
"RECEIVE_BOOT_COMPLETED", "RECEIVE_BOOT_COMPLETED",
+1 -1
View File
@@ -47,7 +47,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>42</string> <string>43</string>
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key> <key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
<false/> <false/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
+5 -4
View File
@@ -13,10 +13,11 @@
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:ci": "jest --runInBand --ci", "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": "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:ios:preview": "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": "dotenv -e .env -- eas build --platform android --profile production --local",
"release:android:preview": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile preview --local" "release:android:preview": "dotenv -e .env -- eas build --platform android --profile preview --local",
"release:android:upload": "bundle exec fastlane android upload"
}, },
"dependencies": { "dependencies": {
"@bacons/apple-targets": "^0.1.6", "@bacons/apple-targets": "^0.1.6",
+1 -1
View File
@@ -8,7 +8,7 @@
<string>com.apple.widgetkit-extension</string> <string>com.apple.widgetkit-extension</string>
</dict> </dict>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>42</string> <string>43</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.5</string> <string>1.0.5</string>
</dict> </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 ### Quick Start
```bash ```bash
cd apps/mobile cd apps/mobile
# Android - produces .aab file (for Play Store) # Android - produces .aab file (for Play Store)
eas build --platform android --profile production --local npm run release:android
# iOS - produces .ipa file (for App Store) # iOS - produces .ipa file (for App Store)
eas build --platform ios --profile production --local npm run release:ios
``` ```
Output files are placed in the current directory by default. 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) ### Building an APK (for Testing)
@@ -116,13 +132,11 @@ export JAVA_HOME=$(/usr/libexec/java_home -v 17)
cd android && ./gradlew --stop && cd .. cd android && ./gradlew --stop && cd ..
``` ```
**Clean build after dependency changes:** **Native regeneration after dependency changes:**
```bash
# Remove native folders and rebuild Treat `expo prebuild` as a source change, not a release step. Run it separately
rm -rf android ios only when native dependencies intentionally changed, review the full native diff,
npx expo prebuild --clean verify it, and commit it before starting the release from a clean worktree.
eas build --platform android --profile production --local
```
**Out of memory:** **Out of memory:**
```bash ```bash
@@ -163,13 +177,10 @@ MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS>
**Important:** Never commit these credentials to git. **Important:** Never commit these credentials to git.
### Step 3: Prebuild (if needed) ### Step 3: Confirm tracked native sources
If the `android/` folder doesn't exist or native dependencies changed: The committed `android/` project is the release source. If native dependencies
changed, regenerate and review that project as a separate change before release.
```bash
npx expo prebuild --platform android --clean
```
### Step 4: Build the Release Bundle ### 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 ## iOS Release Build
### Step 1: Prebuild (if needed) ### Step 1: Confirm tracked native sources
```bash The committed `ios/` project is the release source. If native dependencies
npx expo prebuild --platform ios --clean changed, regenerate and review that project as a separate change before release.
```
### Step 2: Install Pods ### Step 2: Install Pods
@@ -245,9 +255,11 @@ cd ..
## Version Management ## 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 ```bash
# Check current version # 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 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 ```json
{ {
@@ -288,14 +300,16 @@ If Play Store rejects with "signed with wrong key":
3. Verify fingerprint matches expected SHA1 3. Verify fingerprint matches expected SHA1
4. Replace your local keystore file 4. Replace your local keystore file
### Build Failures After Dependency Changes ### Build Failures After Dependency Changes
```bash ```bash
# Clean and rebuild # Clean the existing tracked Android project and rebuild it
cd android && ./gradlew clean && cd .. cd android && ./gradlew clean && cd ..
npx expo prebuild --platform android --clean cd android && ./gradlew bundleRelease
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 ### iOS Signing Issues
+1 -1
View File
@@ -12,7 +12,7 @@ name: Standalone-backend build reaches the Auth login screen
- tapOn: - tapOn:
point: "94%,44%" point: "94%,44%"
- extendedWaitUntil: - extendedWaitUntil:
visible: "Explore intimacy together" visible: "Password"
timeout: 30000 timeout: 30000
- assertVisible: "Sauci" - assertVisible: "Sauci"
- assertVisible: "Password" - assertVisible: "Password"
+1 -1
View File
@@ -12,7 +12,7 @@ name: Hosted Auth fixture reaches the standalone API-backed app
- tapOn: - tapOn:
point: "94%,44%" point: "94%,44%"
- extendedWaitUntil: - extendedWaitUntil:
visible: "Explore intimacy together" visible: "Password"
timeout: 30000 timeout: 30000
- tapOn: "Password" - tapOn: "Password"
- tapOn: "Enter your email" - tapOn: "Enter your email"
+21
View File
@@ -37,6 +37,27 @@ for (const file of allFiles) {
if (!scoped) { if (!scoped) {
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')); 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())) { 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'); const packageFile = path.join(root, 'apps', app.name, 'package.json');
if (!(await exists(packageFile))) continue; if (!(await exists(packageFile))) continue;