Files
sf-app-platform/docs/codemagic-integration.md
JulianAlcala 298f73b02b ci(codemagic): add staging + production workflows for legacy store builds
Codemagic se usa como herramienta de release (Play Store + App Store),
no como CI general. La validación de develop / feature branches sigue
siendo local.

Workflows:
- staging_android / staging_ios — push a fusion-app → Play Internal Track + TestFlight
- production_android / production_ios — tag 1.0.0(N) → Play (draft) + App Store (manual review)

Todos los workflows publicables buildean en APP_MODE=legacy mientras
el roadmap premium de Treezor no esté listo para producción.

Scripts en codemagic_scripts/:
- project_setup.sh — melos bootstrap (build_runner no es necesario,
  los .freezed.dart y .g.dart están commiteados)
- analyze_and_test.sh — melos run analyze + test con --no-select
  obligatorio (sin TTY melos crashea con StdinException)
- android_dependencies.sh — verifica gradle wrapper
- android_key_properties.sh — escribe key.properties desde CM_KEYSTORE_*
- ios_signing_cocoapods.sh — gem install cocoapods + xcode-project use-profiles
- flutter_build.sh — build aab/ipa con --dart-define=APP_MODE; parser
  para tags formato 1.0.0(N) o v1.0.0(N) en producción
- print_versions.sh — log de versiones y herramientas
- build_status.sh — exit + notificación Google Chat ante fallo

Setup detallado y bloqueantes externos en docs/codemagic-integration.md.
2026-05-12 07:32:52 -05:00

470 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Codemagic integration plan
Guía para integrar Codemagic en `sf-app-platform`. **Codemagic se usa exclusivamente como herramienta de release a tiendas** (Play Store + App Store), no como CI general. La validación de develop / feature branches sigue siendo local (`melos run analyze`, `melos run test`, `flutter build` manual).
El plan está adaptado a la realidad de este repo (monorepo Melos + pub workspace, configs gitignored, generated files commiteados, fusión legacy↔payment en curso).
## Decisiones tomadas
| Tema | Decisión | Por qué |
| --- | --- | --- |
| Alcance | **Solo 4 workflows**: staging + production × Android + iOS | Codemagic = herramienta de release. Development no se corre en CI — la validación es local |
| Triggers | `fusion-app` → staging, tag → prod | La rama `main` no está activa; `fusion-app` es el integration branch durante la fusión legacy↔payment |
| Formato de tag prod | `v1.0.0(N)` / `1.0.0(N)` (formato actual) | Mantener convención existente — última release fue `v1.0.0(8)` |
| Configs JSON | **Codemagic Encrypted Files** (solo staging + production) | Más seguro y auditable que env vars; el repo los tiene gitignored y Codemagic los materializa antes del build |
| Code generation | **No correr `build_runner` en CI** | 355 `.freezed.dart`/`.g.dart` están commiteados — solo `melos bootstrap` |
| Quality gates | `melos run analyze --no-select` + `melos run test --no-select` bloqueantes | Si alguien pushea código roto a `fusion-app`, no queremos que llegue a Play Internal Track ni TestFlight. **`--no-select` es obligatorio** (sin TTY melos crashea) |
| Versionado | `version` desde `pubspec.yaml` (manual), build number desde tag en prod | Como hoy. CI no toca pubspec.yaml |
| **APP_MODE en builds a tiendas** | **Siempre `legacy`** | Aunque la rama `fusion-app` integra legacy+payment, los AAB/IPA publicados a App Store y Play Store deben arrancar en flujo legacy hasta que el roadmap premium de Treezor esté listo para producción |
| Web | No incluido | sf-app-platform es mobile-only |
## Cosas del repo que importan (chequeadas, no asumidas)
- **Entry points**: `apps/mobile_app/lib/main_{development,staging,production}.dart`
- **Configs (gitignored)**: `apps/mobile_app/config/{development,staging,production}.json` — claves: `env`, `apiBaseUrl`, `apiOrigin`, `wsUrl`, `juphoonAppKey`
- **iOS schemes por flavor**: `development.xcscheme`, `staging.xcscheme`, `production.xcscheme` (ya existen)
- **iOS Firebase configs por flavor**: `apps/mobile_app/ios/flavors/{dev,stag,prod}/GoogleService-Info.plist` (ya existen — el script Build Phase los copia)
- **Android Firebase configs**: ya están configuradas. `apps/mobile_app/android/app/google-services.json` (root) contiene los packages `com.savefamily.app.dev` Y `com.savefamily.app.stag` (project `sf-platform-pre`); `src/production/google-services.json` override para `com.savefamily.app` (project `sf-platform-pro`). Commits de referencia: `81284d7e`, `e83adbfd`
- **Bundle IDs**:
- iOS / Android: `com.savefamily.app.dev`, `com.savefamily.app.stag`, `com.savefamily.app`
- **Bridges nativos**: `flutter_treezor_entrust_sdk_bridge`, `sca_treezor` son `path:` dependencies locales. No requieren maven privado ni Pod source extra
- **Native repos**: solo `google()` + `mavenCentral()`. Cero credenciales adicionales en Gradle
- **AntelopAppDelegate caveat**: el swizzling de FCM se rompe si se regenera `Runner.xcodeproj`. CI no debe regenerar el proyecto Xcode
- **iOS deployment target**: 15.0 mínimo
- **Dart SDK**: `^3.9.2` → Flutter ≥ 3.27 (requerido por `resolution: workspace` en pubspec raíz)
- **App Store Connect / Apple Developer**: Team `KQ73NP6L4Q`, Account Holder Jorge Alvarez — la API key para Codemagic la tiene que generar él
- **App Store IDs**: staging `6759873957`, prod `6759875039`
- **Play Internal track**: prod Android track id `4700720707717247246`
## Estrategia de configs vía Encrypted Files
En Codemagic UI → **Environment variables → Add encrypted file**. Subir cada JSON con `Path on virtual machine` = path relativo al build dir:
| Archivo local | Path en VM |
| --- | --- |
| `apps/mobile_app/config/staging.json` | `apps/mobile_app/config/staging.json` |
| `apps/mobile_app/config/production.json` | `apps/mobile_app/config/production.json` |
Codemagic los desencripta en disco antes de los scripts. **No hace falta script de materialización**. Si una key rota (ej. `juphoonAppKey`), se re-sube el archivo desde la UI sin tocar el repo ni env vars.
## Archivos a agregar al repo
```
sf-app-platform/
├── codemagic.yaml
└── codemagic_scripts/
├── project_setup.sh # melos bootstrap (NO build_runner)
├── android_dependencies.sh # verifica gradle wrapper, flutter doctor
├── android_key_properties.sh # escribe key.properties desde CM_KEYSTORE_*
├── ios_signing_cocoapods.sh # gem install cocoapods, xcode-project use-profiles
├── flutter_build.sh # build aab / ipa con parser para 1.0.0(N)
├── print_versions.sh # log de versiones
├── analyze_and_test.sh # melos run analyze && melos run test
└── build_status.sh # report final + Google Chat fallo
```
Permisos: `chmod +x codemagic_scripts/*.sh`.
## Scripts
### `project_setup.sh`
```sh
#!/bin/bash
set -eu
dart pub global activate melos 7.5.1
melos bootstrap
```
Nada de `build_runner`: los `.freezed.dart` y `.g.dart` ya están commiteados (355 archivos). Si en el futuro decidimos dejar de commitearlos, se agrega `melos run generate` acá.
### `analyze_and_test.sh`
```sh
#!/bin/bash
set -euo pipefail
melos run analyze --no-select
melos run test --no-select
```
Los dos targets ya están definidos en el bloque `melos.scripts` del `pubspec.yaml` raíz.
**Críticos** (aprendidos en Fase 0):
- `--no-select` es obligatorio en non-TTY. Sin esto, melos prompts interactivamente y crashea con `StdinException`
- `set -o pipefail` evita que pipes downstream enmascaren un exit code de fallo
### `android_dependencies.sh`
```sh
#!/bin/bash
set -eu
cd apps/mobile_app/android
./gradlew --version
```
Diferencia con effi: no se regenera el wrapper (`gradle wrapper --gradle-version 7.4`). El repo ya commitea el wrapper actual; regenerarlo en cada build es ruido.
### `android_key_properties.sh`
```sh
#!/bin/bash
set -eu
cat > "$CM_BUILD_DIR/apps/mobile_app/android/key.properties" <<EOF
storePassword=$CM_KEYSTORE_PASSWORD
keyPassword=$CM_KEY_PASSWORD
keyAlias=$CM_KEY_ALIAS
storeFile=$CM_KEYSTORE_PATH
EOF
```
El `build.gradle.kts` busca `rootProject.file("key.properties")``apps/mobile_app/android/key.properties`.
### `ios_signing_cocoapods.sh`
```sh
#!/bin/bash
set -eu
gem install cocoapods -v $COCOAPODS_VERSION
xcode-project use-profiles --project apps/mobile_app/ios/Runner.xcodeproj
```
`xcode-project` viene preinstalado en imágenes Mac mini de Codemagic.
### `flutter_build.sh`
Parser adaptado al formato real `1.0.0(N)` y `pubspec.yaml: version: 1.0.0+10`. `APP_MODE` por flavor: staging y production fuerzan `legacy` (lo que se publica a tiendas); development queda en el default del código (`legacy`, pero se puede sobreescribir vía un 3er argumento si en el futuro queremos un smoke test del flujo payment).
```sh
#!/bin/bash
set -eu
APP_PLATFORM=$1 # "android" o "ios"
ENVIRONMENT=$2 # "development", "staging" o "production"
APP_MODE=${3:-legacy} # "legacy" o "payment". Default legacy (publicable a tiendas)
APP_DIR="apps/mobile_app"
PUBSPEC="$APP_DIR/pubspec.yaml"
case "$ENVIRONMENT" in
"development"|"staging")
PUBSPEC_VERSION=$(grep '^version:' "$PUBSPEC" | sed -E 's/^version:[[:space:]]+//')
VERSION_NUMBER=${PUBSPEC_VERSION%+*}
ENV_BUILD_NUMBER=${PUBSPEC_VERSION#*+}
;;
"production")
VERSION_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?([0-9]+(\.[0-9]+)+)\([0-9]+\)$/\1/')
ENV_BUILD_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?[0-9]+(\.[0-9]+)+\(([0-9]+)\)$/\2/')
if [ -z "$VERSION_NUMBER" ] || [ -z "$ENV_BUILD_NUMBER" ]; then
echo "Tag inválido (esperado 1.0.0(N) o v1.0.0(N)): $CM_TAG"; exit 1
fi
;;
*)
echo "ENVIRONMENT inválido: $ENVIRONMENT"; exit 1 ;;
esac
cd "$APP_DIR"
COMMON_FLAGS=(
--release
--flavor "$ENVIRONMENT"
-t "lib/main_$ENVIRONMENT.dart"
--build-name="$VERSION_NUMBER"
--build-number="$ENV_BUILD_NUMBER"
--dart-define-from-file="config/$ENVIRONMENT.json"
--dart-define=APP_MODE="$APP_MODE"
)
case "$APP_PLATFORM" in
"android")
flutter build appbundle "${COMMON_FLAGS[@]}"
;;
"ios")
flutter build ipa \
"${COMMON_FLAGS[@]}" \
--export-options-plist=/Users/builder/export_options.plist
;;
*) exit 1 ;;
esac
echo "Build OK: $APP_PLATFORM / $ENVIRONMENT / mode=$APP_MODE (v$VERSION_NUMBER+$ENV_BUILD_NUMBER)"
```
### `print_versions.sh`
Mismo concepto que effi, ajustado al parser:
```sh
#!/bin/bash
set -eu
APP_PLATFORM=$1
ENVIRONMENT=$2
case "$ENVIRONMENT" in
"development"|"staging")
PUBSPEC_VERSION=$(grep '^version:' apps/mobile_app/pubspec.yaml | sed -E 's/^version:[[:space:]]+//')
VERSION_NUMBER=${PUBSPEC_VERSION%+*}
ENV_BUILD_NUMBER=${PUBSPEC_VERSION#*+}
;;
"production")
VERSION_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?([0-9]+(\.[0-9]+)+)\([0-9]+\)$/\1/')
ENV_BUILD_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?[0-9]+(\.[0-9]+)+\(([0-9]+)\)$/\2/')
;;
esac
printf "\033[32m%s\033[0m %s\n" "Flutter version:" "$FLUTTER_VERSION"
printf "\033[32m%s\033[0m %s\n" "App version:" "$VERSION_NUMBER"
printf "\033[32m%s\033[0m %s\n" "Build number:" "$ENV_BUILD_NUMBER"
if [ "$APP_PLATFORM" = "ios" ]; then
pod --version
xcodebuild -version
fi
```
### `build_status.sh`
Idéntico a effi (notificación Google Chat ante fallo).
## `codemagic.yaml`
Cuatro workflows, organizados por (flavor × plataforma). Sin workflows de development — la validación de develop branch se hace local.
```yaml
workflows:
# Staging: build legacy publicable a Play Internal Track / TestFlight desde fusion-app
staging_android:
name: Staging Android
instance_type: linux_x2
environment:
flutter: $FLUTTER_VERSION
android_signing: [upload_keystore]
groups:
- codemagic-staging
- codemagic-common-vars
triggering:
events: [push]
branch_patterns:
- pattern: fusion-app
include: true
scripts:
- name: Setup project
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh android staging
- name: Android dependencies
script: sh ./codemagic_scripts/android_dependencies.sh
- name: Set up key.properties
script: sh ./codemagic_scripts/android_key_properties.sh
- name: Build Android app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh android staging legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/app/outputs/bundle/stagingRelease/app-staging-release.aab
publishing:
google_play:
track: internal
credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
submit_as_draft: false
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh android staging
staging_ios:
name: Staging iOS
instance_type: mac_mini_m2
integrations:
app_store_connect: SaveFamily ASC Key # crear en CM con la key generada por Jorge Alvarez
environment:
flutter: $FLUTTER_VERSION
xcode: latest
ios_signing:
distribution_type: app_store
bundle_identifier: com.savefamily.app.stag
groups:
- codemagic-staging
- codemagic-common-vars
triggering:
events: [push]
branch_patterns:
- pattern: fusion-app
include: true
scripts:
- name: Setup project
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: iOS signing + CocoaPods
script: sh ./codemagic_scripts/ios_signing_cocoapods.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh ios staging
- name: Build iOS app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh ios staging legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/ios/ipa/*.ipa
publishing:
app_store_connect:
auth: integration
submit_to_testflight: true
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh ios staging
# Production: build legacy disparado por tag 1.0.0(N) o v1.0.0(N)
production_android:
name: Production Android
instance_type: linux_x2
environment:
flutter: $FLUTTER_VERSION
android_signing: [upload_keystore]
groups:
- codemagic-production
- codemagic-common-vars
triggering:
events: [tag]
scripts:
- name: Setup project
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh android production
- name: Android dependencies
script: sh ./codemagic_scripts/android_dependencies.sh
- name: Set up key.properties
script: sh ./codemagic_scripts/android_key_properties.sh
- name: Build Android app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh android production legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/app/outputs/bundle/productionRelease/app-production-release.aab
publishing:
google_play:
track: internal
credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
submit_as_draft: true # producción se manda como draft para revisión humana
production_ios:
name: Production iOS
instance_type: mac_mini_m2
integrations:
app_store_connect: SaveFamily ASC Key
environment:
flutter: $FLUTTER_VERSION
xcode: latest
ios_signing:
distribution_type: app_store
bundle_identifier: com.savefamily.app
groups:
- codemagic-production
- codemagic-common-vars
triggering:
events: [tag]
scripts:
- name: Setup project
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: iOS signing + CocoaPods
script: sh ./codemagic_scripts/ios_signing_cocoapods.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh ios production
- name: Build iOS app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh ios production legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/ios/ipa/*.ipa
publishing:
app_store_connect:
auth: integration
submit_to_app_store: true
release_type: MANUAL
cancel_previous_submissions: true
copyright: 2026 SaveFamily
```
## Environment groups en Codemagic
### `codemagic-common-vars`
- `FLUTTER_VERSION` = `3.27.0` (mínimo para `resolution: workspace`)
- `COCOAPODS_VERSION` = `1.15.2`
- `GOOGLE_BUILDS_CHAT_URL` (opcional, webhook fallos)
- `GOOGLE_CHAT_URL` (opcional, webhook releases)
### `codemagic-staging`
- `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`
### `codemagic-production`
- `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`
- `RELEASE_NOTES_LANGUAGE` = `es-ES` (o lo que defina el equipo)
- `RELEASE_NOTES` = texto multilinea de release notes
### Code Signing (UI separada de groups)
- **Android keystore** → uploaded como `upload_keystore`. Codemagic expone `CM_KEYSTORE_PATH`, `CM_KEYSTORE_PASSWORD`, `CM_KEY_PASSWORD`, `CM_KEY_ALIAS`
- **App Store Connect API integration** → la genera Jorge Alvarez desde App Store Connect → Users and Access → Keys. En CM se llama `SaveFamily ASC Key` (referenciado desde `integrations.app_store_connect`)
### Encrypted Files (UI separada también)
- `apps/mobile_app/config/staging.json`
- `apps/mobile_app/config/production.json`
## Bloqueantes antes de prender CI
Cosas que hay que resolver antes de que el primer build dev verde sea posible:
1. **App Store Connect API key** — Jorge Alvarez (Account Holder) tiene que generar la key desde App Store Connect → Users and Access → Integrations → Team Keys, descargar el `.p8` (solo se puede descargar una vez), y configurar la integration en Codemagic
2. **Provisioning profiles**:
- `com.savefamily.app.stag` → tipo **app_store** (para TestFlight)
- `com.savefamily.app` → tipo **app_store** (para release)
- El perfil `com.savefamily.app.dev` no se usa en CI (solo desarrollo local en Xcode)
3. **Upload keystore Android** — verificar si ya existe el upload key actual del equipo. Si no, generar uno (`keytool -genkey -v -keystore upload.keystore ...`) y registrarlo en Play Console Internal App Sharing
4. **Google Play service account** — necesita `androidpublisher` API enabled + permisos en Play Console (Release Manager) para `com.savefamily.app.stag` (staging) y `com.savefamily.app` (production)
## Pasos para arrancar (orden)
1. **Resolver bloqueantes** (sección anterior)
2. **Crear archivos** en el repo:
- `codemagic.yaml`
- `codemagic_scripts/*.sh` (8 scripts) con `chmod +x`
- Commitear en feature branch + PR a `fusion-app`
3. **Conectar repo a Codemagic UI** (`SaveFamily/sf-app-platform`)
4. **Crear los 3 Environment Groups** (common-vars, staging, production) y poblar variables
5. **Subir Encrypted Files** (2 JSON: staging + production)
6. **Subir keystore Android** + configurar App Store Connect integration
7. **Disparar `staging_android` MANUAL** desde la UI (sin esperar push) para validar `melos bootstrap` + analyze + test + build end-to-end. Validar el AAB en Play Internal Track con un tester
8. **Disparar `staging_ios` MANUAL** — más puntos de falla por signing/Pods/CocoaPods, esperar fricción. Validar el IPA en TestFlight
9. **Activar trigger automático** en push a `fusion-app` solo cuando ambos staging estén verdes
10. **Tag de RC para producción** (ej. `0.99.0(1)`) para validar el pipeline de tag-driven release sin afectar producción real
11. **Primer release real con tag** `1.0.0(N)` solo después de coordinación con stakeholders y validación del RC
## Riesgos y caveats específicos
- **Tiempo de build estimado**: `melos bootstrap` (~1 min) + `melos run analyze` (~2-3 min, 30+ paquetes) + `melos run test` (depende, ~2-5 min) + build (~5-10 min). Total: 10-20 min por workflow. Mac mini m2 escala mejor que linux_x2 para iOS
- **AntelopAppDelegate**: cualquier `pod install` con `--repo-update` puede regenerar archivos que rompen el swizzling. El `Podfile.lock` ya está commiteado — CI debe usar `pod install` plain, no `pod update`
- **`resolution: workspace`** requiere Flutter ≥ 3.27. Pinear `FLUTTER_VERSION` exacto en `codemagic-common-vars`, no `latest`
- **`flutter_secure_storage`** está pinned a `9.2.4` vía `dependency_overrides`. CI no debería romper esto pero si melos hace algo raro con overrides en workspace, validar localmente primero
- **Tests con backend mock**: hay tests que llaman APIs (`questia_api_test`?). Si fallan en CI sin red, hay que skiparlos con `@Skip()` o un tag de melos. Validar antes del primer push
- **Flag `--no-select` obligatorio en `melos run`**: en non-TTY (cualquier CI) melos prompts y crashea con `StdinException`. Validado en Fase 0
- **`set -o pipefail` en todos los scripts**: pipes como `melos run test | tail -50` enmascaran el exit code del comando upstream. Sin pipefail, un fallo de melos se reporta como success. Validado en Fase 0
- **Release notes en prod**: actualmente se hace manual. Si automatizamos via `android_release.sh` (estilo effi), las release notes vienen de la env var `RELEASE_NOTES`. Cambiar la convención del equipo o seguir manual via Play Console UI
## Referencias
- Setup base: `~/Desktop/apps/effi-app-flutter/codemagic.yaml` + `codemagic_scripts/`
- Release workflow manual actual: `memory/project_savefamily_release_workflow.md`
- Apple Dev / Account Holder: `memory/project_apple_developer_team.md`, `memory/project_apns_setup.md`
- App Store / Play Store IDs: `memory/project_app_store_ids.md`
- iOS deployment target: `memory/project_ios_deployment_target.md`
- AppDelegate Antelop caveat: `memory/project_appdelegate_antelop_firebase.md`