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.
This commit is contained in:
2026-05-12 07:32:52 -05:00
parent bb0d3c253a
commit 298f73b02b
10 changed files with 772 additions and 0 deletions

167
codemagic.yaml Normal file
View File

@@ -0,0 +1,167 @@
workflows:
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 (melos bootstrap)
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
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 (melos bootstrap)
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_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 (melos bootstrap)
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
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh android production
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 (melos bootstrap)
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
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh ios production

View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
melos run analyze --no-select
melos run test --no-select

View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
cd apps/mobile_app/android
./gradlew --version

View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
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

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -uo pipefail
APP_PLATFORM=$1
ENVIRONMENT=$2
if [ -f ~/SUCCESS ]; then
echo "Build finished successfully ($APP_PLATFORM / $ENVIRONMENT)"
exit 0
fi
echo "Codemagic pipeline failed in $ENVIRONMENT workflow ($APP_PLATFORM)"
if [ -n "${GOOGLE_BUILDS_CHAT_URL:-}" ]; then
curl -fsS -X POST -H 'Content-Type: application/json' \
-d '{
"text": "*Codemagic* failed in '"$ENVIRONMENT"' / '"$APP_PLATFORM"' for commit '"$CM_COMMIT"' (build '"$BUILD_NUMBER"').\nBuild: https://codemagic.io/app/'"$CM_PROJECT_ID"'/build/'"$CM_BUILD_ID"'"
}' \
"$GOOGLE_BUILDS_CHAT_URL" || echo "(google chat webhook post failed)"
fi
exit 1

View File

@@ -0,0 +1,52 @@
#!/bin/bash
set -euo pipefail
APP_PLATFORM=$1
ENVIRONMENT=$2
APP_MODE=${3:-legacy}
APP_DIR="apps/mobile_app"
PUBSPEC="$APP_DIR/pubspec.yaml"
case "$ENVIRONMENT" in
"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 (esperado staging o production)"; 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
;;
*) echo "APP_PLATFORM inválido: $APP_PLATFORM (esperado android o ios)"; exit 1 ;;
esac
echo "Build OK: $APP_PLATFORM / $ENVIRONMENT / mode=$APP_MODE (v$VERSION_NUMBER+$ENV_BUILD_NUMBER)"

View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
gem install cocoapods -v "$COCOAPODS_VERSION"
xcode-project use-profiles --project apps/mobile_app/ios/Runner.xcodeproj

View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -euo pipefail
APP_PLATFORM=$1
ENVIRONMENT=$2
PUBSPEC="apps/mobile_app/pubspec.yaml"
case "$ENVIRONMENT" in
"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/')
;;
*)
echo "ENVIRONMENT inválido: $ENVIRONMENT"; exit 1 ;;
esac
printf "\033[32m%s\033[0m %s\n" "Flutter version:" "${FLUTTER_VERSION:-unknown}"
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"
printf "\033[32m%s\033[0m %s\n" "Environment:" "$ENVIRONMENT"
if [ "$APP_PLATFORM" = "ios" ]; then
printf "\033[32m%s\033[0m\n" "CocoaPods version:"
pod --version
printf "\033[32m%s\033[0m\n" "Xcode version:"
xcodebuild -version
fi

View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -euo pipefail
dart pub global activate melos 7.5.1
melos bootstrap

View File

@@ -0,0 +1,469 @@
# 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`