mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
chore(release): harden mobile build workflow
Promote the reproduced release lessons into harness enforcement: keep tracked native regeneration separate from artifact builds, separate Android build and upload authorization, use stable login readiness in Maestro, and retain the iOS 43 and Android 46 version increments.
This commit is contained in:
1 parent
91544629fa
commit
3acadeff7c
12 files changed
+160
-39
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.
|
||||||
@@ -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.
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+38
-24
@@ -22,14 +22,30 @@ The `--local` flag runs the EAS build process on your machine instead of Expo's
|
|||||||
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)
|
||||||
|
|
||||||
The production profile creates an AAB (Android App Bundle), which can't be directly installed on devices. To build an APK for testing:
|
The production profile creates an AAB (Android App Bundle), which can't be directly installed on devices. To build 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
|
||||||
{
|
{
|
||||||
@@ -291,12 +303,14 @@ If Play Store rejects with "signed with wrong key":
|
|||||||
### 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
|
||||||
|
|
||||||
Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode:
|
Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user