diff --git a/.agents/learnings/promoted/2026-08-28-maestro-stable-login-readiness.md b/.agents/learnings/promoted/2026-08-28-maestro-stable-login-readiness.md new file mode 100644 index 0000000..da1aa23 --- /dev/null +++ b/.agents/learnings/promoted/2026-08-28-maestro-stable-login-readiness.md @@ -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. diff --git a/.agents/learnings/promoted/2026-08-28-mobile-release-source-integrity.md b/.agents/learnings/promoted/2026-08-28-mobile-release-source-integrity.md new file mode 100644 index 0000000..63122e2 --- /dev/null +++ b/.agents/learnings/promoted/2026-08-28-mobile-release-source-integrity.md @@ -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. diff --git a/.codex/skills/release-mobile/SKILL.md b/.codex/skills/release-mobile/SKILL.md index 9096503..f7151cd 100644 --- a/.codex/skills/release-mobile/SKILL.md +++ b/.codex/skills/release-mobile/SKILL.md @@ -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. diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index bf620c3..7dadd75 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -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 { diff --git a/apps/mobile/app.json b/apps/mobile/app.json index bd9f434..2353eb8 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -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", diff --git a/apps/mobile/ios/Sauci/Info.plist b/apps/mobile/ios/Sauci/Info.plist index 604a2c1..0470971 100644 --- a/apps/mobile/ios/Sauci/Info.plist +++ b/apps/mobile/ios/Sauci/Info.plist @@ -47,7 +47,7 @@ CFBundleVersion - 42 + 43 GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED LSMinimumSystemVersion diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8a550cc..2e56507 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -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", diff --git a/apps/mobile/targets/widget/Info.plist b/apps/mobile/targets/widget/Info.plist index e8b5245..8a159fe 100644 --- a/apps/mobile/targets/widget/Info.plist +++ b/apps/mobile/targets/widget/Info.plist @@ -8,7 +8,7 @@ com.apple.widgetkit-extension CFBundleVersion - 42 + 43 CFBundleShortVersionString 1.0.5 diff --git a/docs/releasing.md b/docs/releasing.md index 7a16a8c..6a51f90 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -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= **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 diff --git a/e2e/maestro/login-screen-smoke.yaml b/e2e/maestro/login-screen-smoke.yaml index 9a0b9e9..a725dcf 100644 --- a/e2e/maestro/login-screen-smoke.yaml +++ b/e2e/maestro/login-screen-smoke.yaml @@ -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" diff --git a/e2e/maestro/login-smoke.yaml b/e2e/maestro/login-smoke.yaml index e240366..66a66fe 100644 --- a/e2e/maestro/login-smoke.yaml +++ b/e2e/maestro/login-smoke.yaml @@ -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" diff --git a/scripts/lint-harness.mjs b/scripts/lint-harness.mjs index 1a4c470..9db150f 100644 --- a/scripts/lint-harness.mjs +++ b/scripts/lint-harness.mjs @@ -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;